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.
This commit is contained in:
parent
2cafc7fc12
commit
cc88f3d688
7 changed files with 522 additions and 27 deletions
92
spike/block2_spike/test/web_of_trust_test.dart
Normal file
92
spike/block2_spike/test/web_of_trust_test.dart
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
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);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue