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.
This commit is contained in:
parent
6eb1517ffb
commit
49b872b405
16 changed files with 1284 additions and 0 deletions
77
spike/block2_spike/lib/src/nostr_event.dart
Normal file
77
spike/block2_spike/lib/src/nostr_event.dart
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue