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
97
spike/block2_spike/lib/src/nostr_trust_transport.dart
Normal file
97
spike/block2_spike/lib/src/nostr_trust_transport.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'nostr_connection.dart';
|
||||
import 'nostr_event.dart';
|
||||
import 'trust_transport.dart';
|
||||
import 'web_of_trust.dart';
|
||||
|
||||
/// Duniter-style certifications carried as Nostr events, on the shared
|
||||
/// [NostrConnection]. There is no settled NIP for a web of trust (NIP-85 is
|
||||
/// unsettled), so the spike uses a **custom addressable kind** mapped to the
|
||||
/// Duniter model — exactly the "own WoT, Duniter-compatible" path
|
||||
/// g1-integration.md §"WoT propia pero compatible" anticipates.
|
||||
///
|
||||
/// Addressable by (kind, issuer, `d`=subject): one live certification per
|
||||
/// (certifier, subject), so re-certifying renews and revoking replaces — the
|
||||
/// relay keeps only the latest, like Ğ1 keeps one active cert per pair.
|
||||
class NostrTrustTransport implements TrustTransport {
|
||||
NostrTrustTransport(this._conn);
|
||||
|
||||
final NostrConnection _conn;
|
||||
|
||||
/// Custom Tanemaki certification kind (addressable range).
|
||||
static const kindCertification = 30777;
|
||||
|
||||
int get _now => DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
@override
|
||||
Future<void> certify({
|
||||
required String subjectPubkey,
|
||||
Duration validity = const Duration(days: 365),
|
||||
String note = '',
|
||||
}) =>
|
||||
_publishCert(subjectPubkey, note: note, validity: validity, revoked: false);
|
||||
|
||||
@override
|
||||
Future<void> revoke({required String subjectPubkey}) =>
|
||||
_publishCert(subjectPubkey, note: '', validity: Duration.zero, revoked: true);
|
||||
|
||||
Future<void> _publishCert(
|
||||
String subject, {
|
||||
required String note,
|
||||
required Duration validity,
|
||||
required bool revoked,
|
||||
}) async {
|
||||
final event = NostrEvent(
|
||||
pubkey: _conn.publicKeyHex,
|
||||
createdAt: _now,
|
||||
kind: kindCertification,
|
||||
tags: [
|
||||
['p', subject],
|
||||
['d', subject], // one addressable cert per (issuer, subject)
|
||||
['status', revoked ? 'revoked' : 'valid'],
|
||||
if (!revoked) ['expiration', '${_now + validity.inSeconds}'],
|
||||
],
|
||||
content: note,
|
||||
)..signWith(_conn.privateKeyHex);
|
||||
final r = await _conn.publish(event);
|
||||
if (!r.accepted) throw StateError('relay rejected cert: ${r.message}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Certification>> allCertifications() async {
|
||||
final events = await _conn.reqOnce({
|
||||
'kinds': [kindCertification],
|
||||
});
|
||||
return events.map(_toCertification).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<String>> certifiersOf(String subjectPubkey) async {
|
||||
final events = await _conn.reqOnce({
|
||||
'kinds': [kindCertification],
|
||||
'#p': [subjectPubkey],
|
||||
});
|
||||
final now = DateTime.now();
|
||||
return events
|
||||
.map(_toCertification)
|
||||
.where((c) => c.subject == subjectPubkey && c.isValidAt(now))
|
||||
.map((c) => c.issuer)
|
||||
.toSet();
|
||||
}
|
||||
|
||||
Certification _toCertification(NostrEvent e) {
|
||||
final exp = e.tag('expiration');
|
||||
return Certification(
|
||||
issuer: e.pubkey,
|
||||
subject: e.tag('p') ?? e.tag('d') ?? '',
|
||||
issuedAt: DateTime.fromMillisecondsSinceEpoch(e.createdAt * 1000),
|
||||
expiresAt: exp == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(int.parse(exp) * 1000),
|
||||
revoked: e.tag('status') == 'revoked',
|
||||
note: e.content,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() => _conn.close();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue