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_key.dart
Normal file
77
spike/block2_spike/lib/src/nostr_key.dart
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bip340/bip340.dart' as bip340;
|
||||
|
||||
import 'bech32.dart';
|
||||
import 'hkdf.dart';
|
||||
|
||||
/// A Nostr identity (secp256k1 / BIP340 x-only) deterministically derived from
|
||||
/// the Duniter/Ğ1-style root seed. This is Spike Question 1: prove we can keep
|
||||
/// "one identity" (user backs up ONE seed) while still having a viable Nostr
|
||||
/// transport key.
|
||||
///
|
||||
/// Derivation is one-way by construction: the root seed feeds HKDF, and HKDF's
|
||||
/// PRF (HMAC-SHA256) is not invertible, so neither the private nor the public
|
||||
/// Nostr key leaks the root seed. See spike-block2-findings.md §1.
|
||||
class NostrKey {
|
||||
NostrKey._({required this.privateKeyHex, required this.publicKeyHex});
|
||||
|
||||
/// 32-byte secp256k1 scalar, lower-case hex (64 chars).
|
||||
final String privateKeyHex;
|
||||
|
||||
/// 32-byte BIP340 x-only public key, lower-case hex (64 chars). This is the
|
||||
/// Nostr identity that appears on events and in `npub`.
|
||||
final String publicKeyHex;
|
||||
|
||||
/// NIP-19 `npub…` encoding of [publicKeyHex] — what a user would share.
|
||||
String get npub => bech32Encode('npub', _hexToBytes(publicKeyHex));
|
||||
|
||||
/// NIP-19 `nsec…` encoding of [privateKeyHex] — secret, never shared.
|
||||
String get nsec => bech32Encode('nsec', _hexToBytes(privateKeyHex));
|
||||
|
||||
/// Domain-separated derivation label. Versioned so a future scheme can coexist
|
||||
/// with old backups (same discipline as BackupBox's HKDF info string).
|
||||
static const derivationInfo = 'org.comunes.tane/nostr/secp256k1/v1';
|
||||
|
||||
/// secp256k1 group order n.
|
||||
static final BigInt _n = BigInt.parse(
|
||||
'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141',
|
||||
radix: 16,
|
||||
);
|
||||
|
||||
/// Deterministically derives the Nostr key from a root [seed] (the 32 bytes
|
||||
/// [IdentityService.generateRootSeed] produces).
|
||||
///
|
||||
/// Reduces to a valid scalar in `[1, n-1]` by rejection: on the (astronomically
|
||||
/// rare) miss it bumps a counter in the HKDF info, so the result stays a pure
|
||||
/// function of the seed.
|
||||
factory NostrKey.deriveFromSeed(List<int> seed) {
|
||||
var counter = 0;
|
||||
while (true) {
|
||||
final okm = hkdfSha256(ikm: seed, info: '$derivationInfo/$counter');
|
||||
final d = _bytesToBigInt(okm);
|
||||
if (d != BigInt.zero && d < _n) {
|
||||
final privHex = d.toRadixString(16).padLeft(64, '0');
|
||||
final pubHex = bip340.getPublicKey(privHex);
|
||||
return NostrKey._(privateKeyHex: privHex, publicKeyHex: pubHex);
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BigInt _bytesToBigInt(List<int> bytes) {
|
||||
var result = BigInt.zero;
|
||||
for (final b in bytes) {
|
||||
result = (result << 8) | BigInt.from(b);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Uint8List _hexToBytes(String hex) {
|
||||
final out = Uint8List(hex.length ~/ 2);
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
out[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue