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.
92 lines
3.3 KiB
Dart
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);
|
|
});
|
|
}
|