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
132
spike/block2_spike/test/roundtrip_test.dart
Normal file
132
spike/block2_spike/test/roundtrip_test.dart
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:block2_spike/block2_spike.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// Q3 — publish → discover a real offer by geohash against a running relay
|
||||
/// (in-process, hermetic). Proves the flow works end to end and measures it.
|
||||
void main() {
|
||||
late MiniRelay relay;
|
||||
|
||||
setUp(() async => relay = await MiniRelay.start());
|
||||
tearDown(() async => relay.stop());
|
||||
|
||||
NostrKey keyFor(int fill) =>
|
||||
NostrKey.deriveFromSeed(Uint8List(32)..fillRange(0, 32, fill));
|
||||
|
||||
test('offer published by one identity is discovered by another via geohash',
|
||||
() async {
|
||||
final alice = keyFor(1);
|
||||
final bob = keyFor(2);
|
||||
|
||||
final aliceTransport = await NostrOfferTransport.connect(
|
||||
relay.url,
|
||||
privateKeyHex: alice.privateKeyHex,
|
||||
pubkeyHex: alice.publicKeyHex,
|
||||
);
|
||||
final bobTransport = await NostrOfferTransport.connect(
|
||||
relay.url,
|
||||
privateKeyHex: bob.privateKeyHex,
|
||||
pubkeyHex: bob.publicKeyHex,
|
||||
);
|
||||
|
||||
final offer = Offer(
|
||||
id: 'tomate-1',
|
||||
authorPubkeyHex: alice.publicKeyHex,
|
||||
summary: 'Tomate rosa de Barbastro',
|
||||
type: OfferType.gift,
|
||||
category: 'Solanaceae',
|
||||
approxGeohash: 'sp3e9xyz', // will be coarsened to sp3e9
|
||||
);
|
||||
|
||||
final published = await aliceTransport.publish(offer);
|
||||
expect(published.accepted, isTrue,
|
||||
reason: 'relay OK: ${published.message}');
|
||||
|
||||
// Bob queries a coarser area and still finds it (ladder match).
|
||||
final found = await bobTransport.discoverUntilEose(
|
||||
const DiscoveryQuery(geohashPrefix: 'sp3'),
|
||||
);
|
||||
expect(found, hasLength(1));
|
||||
expect(found.single.summary, 'Tomate rosa de Barbastro');
|
||||
expect(found.single.authorPubkeyHex, alice.publicKeyHex);
|
||||
|
||||
await aliceTransport.close();
|
||||
await bobTransport.close();
|
||||
});
|
||||
|
||||
test('a query for a different area returns nothing (geohash scoping works)',
|
||||
() async {
|
||||
final alice = keyFor(1);
|
||||
final t = await NostrOfferTransport.connect(
|
||||
relay.url,
|
||||
privateKeyHex: alice.privateKeyHex,
|
||||
pubkeyHex: alice.publicKeyHex,
|
||||
);
|
||||
await t.publish(Offer(
|
||||
id: 'x',
|
||||
authorPubkeyHex: alice.publicKeyHex,
|
||||
summary: 'Pimiento',
|
||||
type: OfferType.gift,
|
||||
approxGeohash: 'sp3e9',
|
||||
));
|
||||
final elsewhere = await t.discoverUntilEose(
|
||||
const DiscoveryQuery(geohashPrefix: 'u09'),
|
||||
);
|
||||
expect(elsewhere, isEmpty);
|
||||
await t.close();
|
||||
});
|
||||
|
||||
test('re-publishing the same offer id replaces (addressable), not duplicates',
|
||||
() async {
|
||||
final alice = keyFor(3);
|
||||
final t = await NostrOfferTransport.connect(
|
||||
relay.url,
|
||||
privateKeyHex: alice.privateKeyHex,
|
||||
pubkeyHex: alice.publicKeyHex,
|
||||
);
|
||||
Offer o(String summary) => Offer(
|
||||
id: 'lot-42',
|
||||
authorPubkeyHex: alice.publicKeyHex,
|
||||
summary: summary,
|
||||
type: OfferType.exchange,
|
||||
exchangeTerms: 'a cambio de legumbre',
|
||||
approxGeohash: 'sp3e9',
|
||||
);
|
||||
await t.publish(o('v1'));
|
||||
await t.publish(o('v2'));
|
||||
final found = await t.discoverUntilEose(
|
||||
const DiscoveryQuery(geohashPrefix: 'sp3e9'),
|
||||
);
|
||||
expect(found, hasLength(1), reason: 'addressable replace by (kind,pubkey,d)');
|
||||
expect(found.single.summary, 'v2');
|
||||
await t.close();
|
||||
});
|
||||
|
||||
test('measures publish→discover latency (informational)', () async {
|
||||
final alice = keyFor(4);
|
||||
final t = await NostrOfferTransport.connect(
|
||||
relay.url,
|
||||
privateKeyHex: alice.privateKeyHex,
|
||||
pubkeyHex: alice.publicKeyHex,
|
||||
);
|
||||
final sw = Stopwatch()..start();
|
||||
await t.publish(Offer(
|
||||
id: 'latency',
|
||||
authorPubkeyHex: alice.publicKeyHex,
|
||||
summary: 'Calabaza',
|
||||
type: OfferType.gift,
|
||||
approxGeohash: 'sp3e9',
|
||||
));
|
||||
final found = await t.discoverUntilEose(
|
||||
const DiscoveryQuery(geohashPrefix: 'sp3e9'),
|
||||
);
|
||||
sw.stop();
|
||||
expect(found, hasLength(1));
|
||||
// Not an assertion on wall-time (CI is noisy); just surface the number.
|
||||
printOnFailure('publish+discover round-trip: ${sw.elapsedMilliseconds} ms');
|
||||
// ignore: avoid_print
|
||||
print('[metric] publish+discover round-trip: ${sw.elapsedMilliseconds} ms');
|
||||
await t.close();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue