tane/spike/block2_spike/lib/src/nostr_event.dart
vjrj 49b872b405 spike(block2): de-risk social layer — key derivation, OfferTransport, NIP-99 round-trip
Throwaway research spike (spike/ branch, outside pub workspace, no prod deps
touched). Validates the open Block 2 decisions before committing a funded round:

1. Deterministic one-way secp256k1 (Nostr) key derived from the Ğ1 root seed
   via HKDF — user still backs up ONE thing. Reproducible + signs (BIP340).
2. OfferTransport abstraction over Nostr NIP-99 (kind 30402); Offer stays
   inventory/location-agnostic, geohash coarsened on the wire (tested).
3. publish->discover-by-geohash proven end-to-end against an in-process
   hermetic mini-relay (~34ms round-trip).

Findings + risks + recommendation in docs/design/spike-block2-findings.md.
Block 1 suite untouched. 14 tests green, analyzer clean.
2026-07-10 01:57:25 +02:00

77 lines
2.3 KiB
Dart

import 'dart:convert';
import 'package:bip340/bip340.dart' as bip340;
import 'package:crypto/crypto.dart';
/// A minimal Nostr event (NIP-01): enough to build, id, sign and serialise
/// NIP-99 classified listings. Throwaway — production would use a vetted client.
class NostrEvent {
NostrEvent({
required this.pubkey,
required this.createdAt,
required this.kind,
required this.tags,
required this.content,
this.id = '',
this.sig = '',
});
final String pubkey; // x-only hex
final int createdAt; // unix seconds
final int kind;
final List<List<String>> tags;
final String content;
String id;
String sig;
/// NIP-01 id: sha256 of the compact JSON array
/// `[0, pubkey, created_at, kind, tags, content]`.
String computeId() {
final serialized = jsonEncode([0, pubkey, createdAt, kind, tags, content]);
final digest = sha256.convert(utf8.encode(serialized));
return digest.toString(); // lower-case hex
}
/// Fills [id] and [sig] (BIP340 Schnorr over the id) with [privateKeyHex].
void signWith(String privateKeyHex, {String? auxRandHex}) {
id = computeId();
// Deterministic aux in the spike (derived from id) so round-trip tests are
// reproducible; production uses fresh randomness per BIP340.
final aux = auxRandHex ?? id;
sig = bip340.sign(privateKeyHex, id, aux);
}
/// Verifies the signature against [pubkey]. Cross-checks that a real relay /
/// peer would accept what we produced.
bool verify() => id == computeId() && bip340.verify(pubkey, id, sig);
Map<String, dynamic> toJson() => {
'id': id,
'pubkey': pubkey,
'created_at': createdAt,
'kind': kind,
'tags': tags,
'content': content,
'sig': sig,
};
static NostrEvent fromJson(Map<String, dynamic> j) => NostrEvent(
id: j['id'] as String,
pubkey: j['pubkey'] as String,
createdAt: j['created_at'] as int,
kind: j['kind'] as int,
tags: (j['tags'] as List)
.map((t) => (t as List).map((e) => e as String).toList())
.toList(),
content: j['content'] as String,
sig: j['sig'] as String,
);
/// First value of the first tag named [name], or null.
String? tag(String name) {
for (final t in tags) {
if (t.isNotEmpty && t[0] == name && t.length > 1) return t[1];
}
return null;
}
}