/// 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> _out = {}; // issuer -> subjects final Map> _in = {}; // subject -> issuers /// Builds a graph from valid certifications as of [now] (drops expired / /// revoked, ignores self-certifications). factory WebOfTrust.fromCertifications( Iterable 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 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 members({ required Set 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 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 _distancesFromSeeds(Set seeds) { final dist = {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 {}) { if (!dist.containsKey(next)) { dist[next] = d + 1; queue.add(next); } } } return dist; } }