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:
vjrj 2026-07-10 02:17:35 +02:00
parent 2cafc7fc12
commit cc88f3d688
7 changed files with 522 additions and 27 deletions

View file

@ -0,0 +1,119 @@
/// One "A vouches for B" certification (Duniter/Ğ1 style). Public by design
/// like the on-chain Ğ1 WoT and signed by the issuer.
class Certification {
const Certification({
required this.issuer,
required this.subject,
required this.issuedAt,
this.expiresAt,
this.revoked = false,
this.note = '',
});
final String issuer; // certifier pubkey
final String subject; // certified pubkey
final DateTime issuedAt;
final DateTime? expiresAt;
final bool revoked;
final String note;
bool isValidAt(DateTime now) =>
!revoked && (expiresAt == null || expiresAt!.isAfter(now));
}
/// Pure, Flutter-free web-of-trust computation. This is the piece that would
/// live in `commons_core` (identity/trust is generic). It answers "who counts
/// as a known member" from a set of certification edges, using Duniter's two
/// rules: **N certifications from existing members** and **within distance D of
/// the bootstrap referents**. TDD target no I/O, deterministic.
class WebOfTrust {
WebOfTrust();
final Map<String, Set<String>> _out = {}; // issuer -> subjects
final Map<String, Set<String>> _in = {}; // subject -> issuers
/// Builds a graph from valid certifications as of [now] (drops expired /
/// revoked, ignores self-certifications).
factory WebOfTrust.fromCertifications(
Iterable<Certification> certs, {
required DateTime now,
}) {
final wot = WebOfTrust();
for (final c in certs) {
if (c.issuer == c.subject) continue;
if (!c.isValidAt(now)) continue;
wot.addEdge(c.issuer, c.subject);
}
return wot;
}
void addEdge(String issuer, String subject) {
(_out[issuer] ??= {}).add(subject);
(_in[subject] ??= {}).add(issuer);
}
/// Distinct certifiers of [subject].
Set<String> certifiersOf(String subject) => _in[subject] ?? const {};
int certifierCount(String subject) => certifiersOf(subject).length;
/// The set of known members: [seeds] (bootstrap referents, certified face to
/// face at a fair) plus everyone reachable by the rules. Iterated to a
/// fixpoint, because becoming a member can let you push others over the
/// threshold.
///
/// - [threshold]: min certifications **from current members** (Duniter sigQty,
/// ~5).
/// - [maxDistance]: max certification hops from any seed (Duniter stepMax, ~5).
Set<String> members({
required Set<String> seeds,
required int threshold,
required int maxDistance,
}) {
final members = {...seeds};
final distance = _distancesFromSeeds(seeds);
var changed = true;
while (changed) {
changed = false;
for (final subject in _in.keys) {
if (members.contains(subject)) continue;
final d = distance[subject];
if (d == null || d > maxDistance) continue;
final memberCertifiers =
certifiersOf(subject).where(members.contains).length;
if (memberCertifiers >= threshold) {
members.add(subject);
changed = true;
}
}
}
return members;
}
bool isMember(
String pubkey, {
required Set<String> seeds,
required int threshold,
required int maxDistance,
}) =>
members(seeds: seeds, threshold: threshold, maxDistance: maxDistance)
.contains(pubkey);
/// BFS hop-distance from the nearest seed over certification edges.
Map<String, int> _distancesFromSeeds(Set<String> seeds) {
final dist = <String, int>{for (final s in seeds) s: 0};
final queue = [...seeds];
var head = 0;
while (head < queue.length) {
final node = queue[head++];
final d = dist[node]!;
for (final next in _out[node] ?? const <String>{}) {
if (!dist.containsKey(next)) {
dist[next] = d + 1;
queue.add(next);
}
}
}
return dist;
}
}