tane/spike/block2_spike/test/web_of_trust_test.dart
vjrj cc88f3d688 spike(block2): de-risk web of trust — the last big unknown
Adds TrustTransport as the third interface on the shared NostrConnection,
completing the social-layer happy path in the spike:

- WebOfTrust: pure, Flutter-free Duniter membership rule (N certs from members +
  within distance D of bootstrap referents), iterated to a fixpoint. The
  commons_core-worthy piece; TDD'd (threshold, distance cutoff, transitive
  growth, expired/revoked/self exclusion).
- NostrTrustTransport: certifications as a custom addressable kind (30777,
  keyed by issuer+subject) — certify renews, revoke replaces, certs expire.
- Trust filters spam (network-trust §2): seeds certify Alice; Eve stays
  uncertified; Bob discovers both offers and annotates authors — Alice known,
  Eve unknown — all over ONE shared connection.

Findings updated: WoT risk High -> Medium (policy/bootstrap, not feasibility);
overall scope Medium-High -> Medium — all three transport contracts now proven.
30 tests green, analyzer clean, Block 1 untouched.
2026-07-10 02:17:35 +02:00

92 lines
3.3 KiB
Dart

import 'package:block2_spike/block2_spike.dart';
import 'package:test/test.dart';
/// Pure web-of-trust rules (Duniter: N certs from members + within distance D).
/// This is the `commons_core`-worthy piece: deterministic, no I/O.
void main() {
final now = DateTime(2026, 7, 10);
Certification cert(String a, String b, {DateTime? expires, bool revoked = false}) =>
Certification(
issuer: a,
subject: b,
issuedAt: DateTime(2026),
expiresAt: expires,
revoked: revoked,
);
test('seeds are always members', () {
final wot = WebOfTrust.fromCertifications([], now: now);
expect(
wot.members(seeds: {'s1'}, threshold: 5, maxDistance: 5),
contains('s1'),
);
});
test('reaches the threshold from member certifiers → becomes known', () {
final seeds = {'s1', 's2', 's3', 's4', 's5'};
final certs = [for (final s in seeds) cert(s, 'newcomer')];
final wot = WebOfTrust.fromCertifications(certs, now: now);
final members = wot.members(seeds: seeds, threshold: 5, maxDistance: 5);
expect(members, contains('newcomer'));
});
test('below the threshold stays unknown', () {
final seeds = {'s1', 's2', 's3', 's4', 's5'};
final certs = [cert('s1', 'x'), cert('s2', 'x')]; // only 2 of 5
final wot = WebOfTrust.fromCertifications(certs, now: now);
expect(
wot.members(seeds: seeds, threshold: 5, maxDistance: 5),
isNot(contains('x')),
);
});
test('membership is transitive via a fixpoint (needs a to count for b)', () {
final seeds = {'s1', 's2'};
final certs = [
cert('s1', 'a'), cert('s2', 'a'), // a: 2 member certs → member
cert('a', 'b'), cert('s2', 'b'), // b: certified by a (now member) + s2
];
final wot = WebOfTrust.fromCertifications(certs, now: now);
final members = wot.members(seeds: seeds, threshold: 2, maxDistance: 5);
expect(members, containsAll(['a', 'b']));
});
test('distance rule cuts off far-away nodes even if well-certified', () {
// s → a → b → c → d → e chain; only s is a seed; threshold 1.
final seeds = {'s'};
final certs = [
cert('s', 'a'),
cert('a', 'b'),
cert('b', 'c'),
cert('c', 'd'),
cert('d', 'e'),
];
final wot = WebOfTrust.fromCertifications(certs, now: now);
final members = wot.members(seeds: seeds, threshold: 1, maxDistance: 2);
expect(members, containsAll(['a', 'b'])); // dist 1 and 2
expect(members, isNot(contains('c'))); // dist 3 > maxDistance
expect(members, isNot(contains('e')));
});
test('expired and revoked certifications do not count', () {
final seeds = {'s1', 's2', 's3', 's4', 's5'};
final certs = [
cert('s1', 'x'),
cert('s2', 'x'),
cert('s3', 'x'),
cert('s4', 'x', expires: DateTime(2020)), // expired
cert('s5', 'x', revoked: true), // revoked
];
final wot = WebOfTrust.fromCertifications(certs, now: now);
expect(wot.certifierCount('x'), 3, reason: 'expired+revoked dropped');
expect(
wot.members(seeds: seeds, threshold: 5, maxDistance: 5),
isNot(contains('x')),
);
});
test('self-certification is ignored', () {
final wot = WebOfTrust.fromCertifications([cert('a', 'a')], now: now);
expect(wot.certifierCount('a'), 0);
});
}