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.
77 lines
2.3 KiB
Dart
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;
|
|
}
|
|
}
|