import 'dart:typed_data'; import 'package:block2_spike/block2_spike.dart'; import 'package:test/test.dart'; /// Q4 (last big unknown) — Duniter-style web of trust over Nostr, on the SAME /// shared connection as offers and messaging. Proves certifications publish & /// discover, the membership rule computes from discovered events, and trust can /// filter offers (the network-trust §2 spam defence). void main() { NostrKey keyFor(int fill) => NostrKey.deriveFromSeed(Uint8List(32)..fillRange(0, 32, fill)); late MiniRelay relay; setUp(() async => relay = await MiniRelay.start()); tearDown(() async => relay.stop()); Future connFor(NostrKey k) => NostrConnection.connect( relay.url, privateKeyHex: k.privateKeyHex, publicKeyHex: k.publicKeyHex, ); test('certifications publish, discover, and make a newcomer known', () async { final seeds = [for (var i = 1; i <= 3; i++) keyFor(i)]; final newcomer = keyFor(50); // Three seed members each certify the newcomer. for (final s in seeds) { final t = NostrTrustTransport(await connFor(s)); await t.certify(subjectPubkey: newcomer.publicKeyHex); await t.close(); } // Anyone can read the certs and compute membership. final reader = NostrTrustTransport(await connFor(keyFor(99))); final certifiers = await reader.certifiersOf(newcomer.publicKeyHex); expect(certifiers, hasLength(3)); final wot = WebOfTrust.fromCertifications( await reader.allCertifications(), now: DateTime.now(), ); final seedSet = seeds.map((s) => s.publicKeyHex).toSet(); expect( wot.isMember(newcomer.publicKeyHex, seeds: seedSet, threshold: 3, maxDistance: 5), isTrue, ); await reader.close(); }); test('revoking a certification drops the certifier (addressable replace)', () async { final alice = keyFor(1); final bob = keyFor(2); final aliceTrust = NostrTrustTransport(await connFor(alice)); await aliceTrust.certify(subjectPubkey: bob.publicKeyHex); var certs = await aliceTrust.certifiersOf(bob.publicKeyHex); expect(certs, contains(alice.publicKeyHex)); await aliceTrust.revoke(subjectPubkey: bob.publicKeyHex); certs = await aliceTrust.certifiersOf(bob.publicKeyHex); expect(certs, isNot(contains(alice.publicKeyHex)), reason: 'revoked cert replaces the valid one and is filtered out'); await aliceTrust.close(); }); test('trust filters offers: a known author ranks; an unknown one is flagged', () async { // Bootstrap: 3 seeds certify Alice. Eve is uncertified (a stranger/spammer). final seeds = [for (var i = 1; i <= 3; i++) keyFor(i)]; final alice = keyFor(20); final eve = keyFor(66); for (final s in seeds) { final t = NostrTrustTransport(await connFor(s)); await t.certify(subjectPubkey: alice.publicKeyHex); await t.close(); } // Both Alice and Eve publish offers in the same area. for (final who in [alice, eve]) { final offers = NostrOfferTransport(await connFor(who)); await offers.publish(Offer( id: 'o-${who.publicKeyHex.substring(0, 6)}', authorPubkeyHex: who.publicKeyHex, summary: 'Semilla', type: OfferType.gift, approxGeohash: 'sp3e9', )); await offers.close(); } // Bob discovers offers, then annotates each author with trust — over the // SAME shared connection (one socket, offers + trust both on top of it). final bobConn = await connFor(keyFor(7)); final found = await NostrOfferTransport(bobConn) .discoverUntilEose(const DiscoveryQuery(geohashPrefix: 'sp3e9')); final trust = NostrTrustTransport(bobConn); final wot = WebOfTrust.fromCertifications( await trust.allCertifications(), now: DateTime.now(), ); final seedSet = seeds.map((s) => s.publicKeyHex).toSet(); bool known(String pk) => wot.isMember(pk, seeds: seedSet, threshold: 3, maxDistance: 5); expect(found, hasLength(2)); expect(known(alice.publicKeyHex), isTrue); expect(known(eve.publicKeyHex), isFalse, reason: 'uncertified author stays "unknown" and is deprioritised'); await trust.close(); }); }