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
76
spike/block2_spike/test/derivation_test.dart
Normal file
76
spike/block2_spike/test/derivation_test.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bip340/bip340.dart' as bip340;
|
||||
import 'package:block2_spike/block2_spike.dart';
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// Q1 — Identity derivation: a deterministic, one-way secp256k1 (Nostr) key from
|
||||
/// the Duniter/Ğ1-style root seed, so the user still backs up ONE thing.
|
||||
void main() {
|
||||
group('NostrKey.deriveFromSeed', () {
|
||||
// A fixed seed so expectations are reproducible across runs/machines.
|
||||
final seed = Uint8List.fromList(List.generate(32, (i) => i));
|
||||
|
||||
test('is reproducible: same seed → identical key', () {
|
||||
final a = NostrKey.deriveFromSeed(seed);
|
||||
final b = NostrKey.deriveFromSeed(seed);
|
||||
expect(a.privateKeyHex, b.privateKeyHex);
|
||||
expect(a.publicKeyHex, b.publicKeyHex);
|
||||
expect(a.npub, b.npub);
|
||||
});
|
||||
|
||||
test('produces a well-formed x-only key and npub/nsec', () {
|
||||
final key = NostrKey.deriveFromSeed(seed);
|
||||
expect(key.privateKeyHex, matches(RegExp(r'^[0-9a-f]{64}$')));
|
||||
expect(key.publicKeyHex, matches(RegExp(r'^[0-9a-f]{64}$')));
|
||||
// Cross-check: bip340 derives the same pubkey from our private hex.
|
||||
expect(bip340.getPublicKey(key.privateKeyHex), key.publicKeyHex);
|
||||
expect(key.npub, startsWith('npub1'));
|
||||
expect(key.nsec, startsWith('nsec1'));
|
||||
});
|
||||
|
||||
test('the derived key actually signs & verifies (BIP340)', () {
|
||||
final key = NostrKey.deriveFromSeed(seed);
|
||||
final message = 'a' * 64; // 32-byte hex message
|
||||
final sig = bip340.sign(key.privateKeyHex, message, 'b' * 64);
|
||||
expect(bip340.verify(key.publicKeyHex, message, sig), isTrue);
|
||||
});
|
||||
|
||||
test('different seeds → different keys (avalanche on a 1-bit flip)', () {
|
||||
final flipped = Uint8List.fromList(seed)..[0] ^= 0x01;
|
||||
final a = NostrKey.deriveFromSeed(seed);
|
||||
final b = NostrKey.deriveFromSeed(flipped);
|
||||
expect(a.privateKeyHex, isNot(b.privateKeyHex));
|
||||
expect(a.publicKeyHex, isNot(b.publicKeyHex));
|
||||
});
|
||||
|
||||
test('one-wayness: neither key contains the seed, and HKDF is not '
|
||||
'invertible', () {
|
||||
final key = NostrKey.deriveFromSeed(seed);
|
||||
final seedHex = seed
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
// The seed does not appear verbatim in the derived material.
|
||||
expect(key.privateKeyHex, isNot(contains(seedHex)));
|
||||
expect(key.publicKeyHex, isNot(contains(seedHex)));
|
||||
// Structural argument (documented in findings §1): recovery of the seed
|
||||
// would require inverting HMAC-SHA256. We assert the property we CAN test:
|
||||
// two unrelated seeds never collide to the same key.
|
||||
final other = NostrKey.deriveFromSeed(
|
||||
Uint8List.fromList(List.generate(32, (i) => 255 - i)),
|
||||
);
|
||||
expect(key.privateKeyHex, isNot(other.privateKeyHex));
|
||||
});
|
||||
|
||||
test('integrates with the real IdentityService seed length', () {
|
||||
final rootSeed = IdentityService(
|
||||
random: Random(42),
|
||||
).generateRootSeed();
|
||||
expect(rootSeed.length, IdentityService.rootSeedLengthBytes);
|
||||
final key = NostrKey.deriveFromSeed(rootSeed);
|
||||
expect(key.publicKeyHex, matches(RegExp(r'^[0-9a-f]{64}$')));
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue