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:
parent
a03154030b
commit
1253f0c632
11 changed files with 528 additions and 0 deletions
|
|
@ -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}$')));
|
||||
});
|
||||
});
|
||||
}
|
||||
82
packages/commons_core/test/social/web_of_trust_test.dart
Normal file
82
packages/commons_core/test/social/web_of_trust_test.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// Pure web-of-trust rules (Duniter: N certs from members + within distance D).
|
||||
void main() {
|
||||
final now = DateTime(2026, 7, 10);
|
||||
Certification cert(String a, String b, {DateTime? expires, bool revoked = false}) =>
|
||||
Certification(
|
||||
issuer: a,
|
||||
subject: b,
|
||||
issuedAt: DateTime(2026),
|
||||
expiresAt: expires,
|
||||
revoked: revoked,
|
||||
);
|
||||
|
||||
test('seeds are always members', () {
|
||||
final wot = WebOfTrust.fromCertifications([], now: now);
|
||||
expect(wot.members(seeds: {'s1'}, threshold: 5, maxDistance: 5),
|
||||
contains('s1'));
|
||||
});
|
||||
|
||||
test('reaches the threshold from member certifiers → becomes known', () {
|
||||
final seeds = {'s1', 's2', 's3', 's4', 's5'};
|
||||
final certs = [for (final s in seeds) cert(s, 'newcomer')];
|
||||
final wot = WebOfTrust.fromCertifications(certs, now: now);
|
||||
expect(wot.members(seeds: seeds, threshold: 5, maxDistance: 5),
|
||||
contains('newcomer'));
|
||||
});
|
||||
|
||||
test('below the threshold stays unknown', () {
|
||||
final seeds = {'s1', 's2', 's3', 's4', 's5'};
|
||||
final certs = [cert('s1', 'x'), cert('s2', 'x')];
|
||||
final wot = WebOfTrust.fromCertifications(certs, now: now);
|
||||
expect(wot.members(seeds: seeds, threshold: 5, maxDistance: 5),
|
||||
isNot(contains('x')));
|
||||
});
|
||||
|
||||
test('membership is transitive via a fixpoint (needs a to count for b)', () {
|
||||
final seeds = {'s1', 's2'};
|
||||
final certs = [
|
||||
cert('s1', 'a'), cert('s2', 'a'),
|
||||
cert('a', 'b'), cert('s2', 'b'),
|
||||
];
|
||||
final wot = WebOfTrust.fromCertifications(certs, now: now);
|
||||
expect(wot.members(seeds: seeds, threshold: 2, maxDistance: 5),
|
||||
containsAll(['a', 'b']));
|
||||
});
|
||||
|
||||
test('distance rule cuts off far-away nodes even if well-certified', () {
|
||||
final seeds = {'s'};
|
||||
final certs = [
|
||||
cert('s', 'a'),
|
||||
cert('a', 'b'),
|
||||
cert('b', 'c'),
|
||||
cert('c', 'd'),
|
||||
];
|
||||
final wot = WebOfTrust.fromCertifications(certs, now: now);
|
||||
final members = wot.members(seeds: seeds, threshold: 1, maxDistance: 2);
|
||||
expect(members, containsAll(['a', 'b']));
|
||||
expect(members, isNot(contains('c')));
|
||||
});
|
||||
|
||||
test('expired and revoked certifications do not count', () {
|
||||
final seeds = {'s1', 's2', 's3', 's4', 's5'};
|
||||
final certs = [
|
||||
cert('s1', 'x'),
|
||||
cert('s2', 'x'),
|
||||
cert('s3', 'x'),
|
||||
cert('s4', 'x', expires: DateTime(2020)),
|
||||
cert('s5', 'x', revoked: true),
|
||||
];
|
||||
final wot = WebOfTrust.fromCertifications(certs, now: now);
|
||||
expect(wot.certifierCount('x'), 3);
|
||||
expect(wot.members(seeds: seeds, threshold: 5, maxDistance: 5),
|
||||
isNot(contains('x')));
|
||||
});
|
||||
|
||||
test('self-certification is ignored', () {
|
||||
final wot = WebOfTrust.fromCertifications([cert('a', 'a')], now: now);
|
||||
expect(wot.certifierCount('a'), 0);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue