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}$'))); }); }); }