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
115
spike/block2_spike/test/trust_test.dart
Normal file
115
spike/block2_spike/test/trust_test.dart
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
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<NostrConnection> 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();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue