tane/spike/block2_spike/lib/src/bech32.dart
vjrj 49b872b405 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.
2026-07-10 01:57:25 +02:00

54 lines
1.6 KiB
Dart

/// Self-contained bech32 encode (BIP-173 checksum), enough for NIP-19
/// `npub`/`nsec`. Written inline to avoid a dependency's version quirks; NIP-19
/// is plain bech32 (checksum constant 1), not bech32m.
const _charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
int _polymod(List<int> values) {
const gen = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
var chk = 1;
for (final v in values) {
final top = chk >> 25;
chk = ((chk & 0x1ffffff) << 5) ^ v;
for (var i = 0; i < 5; i++) {
if ((top >> i) & 1 == 1) chk ^= gen[i];
}
}
return chk;
}
List<int> _hrpExpand(String hrp) => [
...hrp.codeUnits.map((c) => c >> 5),
0,
...hrp.codeUnits.map((c) => c & 31),
];
List<int> _createChecksum(String hrp, List<int> data) {
final values = [..._hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0];
final mod = _polymod(values) ^ 1;
return List.generate(6, (i) => (mod >> (5 * (5 - i))) & 31);
}
/// Converts 8-bit bytes to 5-bit groups (bech32 payload), padding with zeros.
List<int> _convertBits8to5(List<int> data) {
var acc = 0;
var bits = 0;
final out = <int>[];
for (final value in data) {
acc = (acc << 8) | value;
bits += 8;
while (bits >= 5) {
bits -= 5;
out.add((acc >> bits) & 31);
}
}
if (bits > 0) out.add((acc << (5 - bits)) & 31);
return out;
}
/// bech32-encodes [data] (raw bytes) under [hrp] (e.g. `npub`).
String bech32Encode(String hrp, List<int> data) {
final five = _convertBits8to5(data);
final checksum = _createChecksum(hrp, five);
final combined = [...five, ...checksum];
return '${hrp}1${combined.map((v) => _charset[v]).join()}';
}