feat(block2): commons_core social foundation — derivation, types, interfaces, WoT

First production slice of Block 2 (transport foundation), on the vetted
pure-Dart nostr package (LGPL-3.0, AGPL-compatible, Flutter-free):

- NostrKeyDerivation: seed -> secp256k1 Nostr identity via domain-separated
  HKDF (reuses cryptography's Hkdf like BackupBox), one-way, versioned. Wraps
  nostr's Keys for signing. Tested (reproducible, avalanche, one-way, integrates
  with IdentityService seed).
- Agnostic value types: Offer/OfferType/OfferStatus/DiscoveryQuery/PublishResult,
  Certification.
- Transport interfaces: OfferTransport, MessageTransport (+PrivateMessage),
  TrustTransport — the three sibling contracts.
- WebOfTrust: pure Duniter membership rule (threshold + distance, fixpoint).
  Promoted from the spike; TDD (7 cases).

No network yet (that's the next slice). commons_core: 50 tests green, analyzer
clean. Block 1 APIs unchanged.
This commit is contained in:
vjrj 2026-07-10 02:31:02 +02:00
parent 05a9d45a82
commit edb480b21b
11 changed files with 528 additions and 0 deletions

View file

@ -0,0 +1,59 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:commons_core/commons_core.dart';
import 'package:test/test.dart';
void main() {
group('NostrKeyDerivation.deriveFromSeed', () {
final seed = Uint8List.fromList(List.generate(32, (i) => i));
test('is reproducible: same seed → identical key', () async {
final a = await NostrKeyDerivation.deriveFromSeed(seed);
final b = await NostrKeyDerivation.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', () async {
final id = await NostrKeyDerivation.deriveFromSeed(seed);
expect(id.privateKeyHex, matches(RegExp(r'^[0-9a-f]{64}$')));
expect(id.publicKeyHex, matches(RegExp(r'^[0-9a-f]{64}$')));
expect(id.npub, startsWith('npub1'));
expect(id.nsec, startsWith('nsec1'));
});
test('the derived key signs & verifies (BIP340, via nostr)', () async {
final id = await NostrKeyDerivation.deriveFromSeed(seed);
final message = 'a' * 64; // 32-byte hex
final sig = id.keys.sign(message: message);
expect(sig, matches(RegExp(r'^[0-9a-f]{128}$')));
});
test('different seeds → different keys (avalanche on a 1-bit flip)',
() async {
final flipped = Uint8List.fromList(seed)..[0] ^= 0x01;
final a = await NostrKeyDerivation.deriveFromSeed(seed);
final b = await NostrKeyDerivation.deriveFromSeed(flipped);
expect(a.privateKeyHex, isNot(b.privateKeyHex));
expect(a.publicKeyHex, isNot(b.publicKeyHex));
});
test('one-wayness: the seed does not appear in the derived material',
() async {
final id = await NostrKeyDerivation.deriveFromSeed(seed);
final seedHex =
seed.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
expect(id.privateKeyHex, isNot(contains(seedHex)));
expect(id.publicKeyHex, isNot(contains(seedHex)));
});
test('integrates with the real IdentityService root seed', () async {
final rootSeed = IdentityService(random: Random(42)).generateRootSeed();
expect(rootSeed.length, IdentityService.rootSeedLengthBytes);
final id = await NostrKeyDerivation.deriveFromSeed(rootSeed);
expect(id.publicKeyHex, matches(RegExp(r'^[0-9a-f]{64}$')));
});
});
}