feat(block2): commons_core social foundation — derivation, types, interfaces, WoT

First production slice of Block 2 (transport foundation), on the vetted
pure-Dart nostr package (LGPL-3.0, AGPL-compatible, Flutter-free):

- NostrKeyDerivation: seed -> secp256k1 Nostr identity via domain-separated
  HKDF (reuses cryptography's Hkdf like BackupBox), one-way, versioned. Wraps
  nostr's Keys for signing. Tested (reproducible, avalanche, one-way, integrates
  with IdentityService seed).
- Agnostic value types: Offer/OfferType/OfferStatus/DiscoveryQuery/PublishResult,
  Certification.
- Transport interfaces: OfferTransport, MessageTransport (+PrivateMessage),
  TrustTransport — the three sibling contracts.
- WebOfTrust: pure Duniter membership rule (threshold + distance, fixpoint).
  Promoted from the spike; TDD (7 cases).

No network yet (that's the next slice). commons_core: 50 tests green, analyzer
clean. Block 1 APIs unchanged.
This commit is contained in:
vjrj 2026-07-10 02:31:02 +02:00
parent a03154030b
commit 1253f0c632
11 changed files with 528 additions and 0 deletions

View file

@ -0,0 +1,82 @@
import 'package:commons_core/commons_core.dart';
import 'package:test/test.dart';
/// Pure web-of-trust rules (Duniter: N certs from members + within distance D).
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);
expect(wot.members(seeds: seeds, threshold: 5, maxDistance: 5),
contains('newcomer'));
});
test('below the threshold stays unknown', () {
final seeds = {'s1', 's2', 's3', 's4', 's5'};
final certs = [cert('s1', 'x'), cert('s2', 'x')];
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'),
cert('a', 'b'), cert('s2', 'b'),
];
final wot = WebOfTrust.fromCertifications(certs, now: now);
expect(wot.members(seeds: seeds, threshold: 2, maxDistance: 5),
containsAll(['a', 'b']));
});
test('distance rule cuts off far-away nodes even if well-certified', () {
final seeds = {'s'};
final certs = [
cert('s', 'a'),
cert('a', 'b'),
cert('b', 'c'),
cert('c', 'd'),
];
final wot = WebOfTrust.fromCertifications(certs, now: now);
final members = wot.members(seeds: seeds, threshold: 1, maxDistance: 2);
expect(members, containsAll(['a', 'b']));
expect(members, isNot(contains('c')));
});
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)),
cert('s5', 'x', revoked: true),
];
final wot = WebOfTrust.fromCertifications(certs, now: now);
expect(wot.certifierCount('x'), 3);
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);
});
}