aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-29 00:50:09 -0700
committerAdam Soltys <[email protected]>2026-08-29 00:50:09 -0700
commit2023fe06d5cc4867f09d5916680b5fe92394dacb (patch)
treeaf8e230bd283776273537da60fe9c0d6782cba28 /tools
parent8ea0416c08535750839088c060d3252415510cc5 (diff)
downloadcoinosv3-2023fe06d5cc4867f09d5916680b5fe92394dacb.tar.gz
coinosv3-2023fe06d5cc4867f09d5916680b5fe92394dacb.zip
The wallet learns to read BOLT 12 for itself: offers and invoices decode client-side, and every invoice the ASP fetches over onion messages is verified here — payment hash read from the raw TLVs, offer terms mirrored byte-for-byte, the issuer's schnorr signature checked against LDK's exact merkle construction (proven against a live CLN-signed fixture) — so no server can hand back an invoice whose preimage it already holds. Paste an lno1 or resolve a name whose record carries one and the existing HTLC pay flow carries it (captaind's Invoice enum takes lni strings natively); a name's owner may now publish their OWN node's offer in their DNS record, retiring the registrar's stand-in — senders fetch invoices straight from their node and no coinos service ever touches the money. The /lnquote fallback prices blinded paths by entry node + advertised payinfo
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
Diffstat (limited to 'tools')
-rw-r--r--tools/bolt12-test.js63
1 files changed, 63 insertions, 0 deletions
diff --git a/tools/bolt12-test.js b/tools/bolt12-test.js
new file mode 100644
index 0000000..28909d3
--- /dev/null
+++ b/tools/bolt12-test.js
@@ -0,0 +1,63 @@
+// BOLT 12 decode + signature verification against a REAL offer/invoice pair
+// (fetched from a live CLN via fetchinvoice) plus tamper checks.
+// Run: bun tools/bolt12-test.js [offer-file invoice-file]
+import { readFileSync } from 'node:fs';
+import {
+ decodeOffer, decodeBolt12Invoice, verifyOfferInvoice, encodeBech32Raw, decodeBech32Raw,
+} from '../src/bolt12.js';
+
+let fails = 0;
+const check = (name, ok, detail = '') => {
+ console.log(` ${ok ? '✓' : '✗'} ${name}${detail ? ' — ' + String(detail).slice(0, 140) : ''}`);
+ if (!ok) fails++;
+};
+const throws = (fn) => { try { fn(); return null; } catch (e) { return e.message; } };
+
+const [offerFile, invoiceFile] = process.argv.slice(2);
+const offerStr = readFileSync(offerFile || '/tmp/claude-1000/-home-adam-coinosv3/556b3f05-c728-4e11-a426-50382ad8bd8d/scratchpad/fixture-offer.txt', 'utf8').trim();
+const invStr = readFileSync(invoiceFile || '/tmp/claude-1000/-home-adam-coinosv3/556b3f05-c728-4e11-a426-50382ad8bd8d/scratchpad/fixture-invoice.txt', 'utf8').trim();
+
+console.log('[codec]');
+const rt = decodeBech32Raw(invStr);
+check('lni hrp', rt.hrp === 'lni');
+check('bech32 roundtrips', encodeBech32Raw('lni', rt.bytes) === invStr.toLowerCase());
+
+console.log('\n[offer]');
+const offer = decodeOffer(offerStr);
+check('decodes', !!offer.records.length, `${offer.records.length} records`);
+check('mainnet', offer.network === 'mainnet');
+check('describes itself', typeof offer.description === 'string', offer.description);
+
+console.log('\n[invoice]');
+const inv = decodeBolt12Invoice(invStr);
+check('payment hash present', /^[0-9a-f]{64}$/.test(inv.paymentHash || ''), inv.paymentHash);
+check('amount = 12345 msat', inv.amountMsat === 12345n, String(inv.amountMsat));
+check('amount rounds up to 13 sat', inv.amountSat === 13);
+check('signer named', /^[0-9a-f]{66}$/.test(inv.nodeId || ''), inv.nodeId);
+check('expiry sane', inv.expiresAt > Date.now() - 86400_000, new Date(inv.expiresAt).toISOString());
+
+console.log('\n[verification]');
+check('real invoice verifies', throws(() => verifyOfferInvoice(offer, inv)) === null,
+ throws(() => verifyOfferInvoice(offer, inv)) || 'ok');
+
+// tamper with the amount TLV: signature must fail
+const tampered = decodeBolt12Invoice(invStr);
+const amtRec = tampered.records.find((r) => r.type === 170);
+amtRec.raw[amtRec.raw.length - 1] ^= 1;
+check('tampered amount rejected', /signature/.test(throws(() => verifyOfferInvoice(offer, tampered)) || ''));
+
+// swap in a different offer description: mirror check must fail
+const wrongOffer = decodeOffer(offerStr);
+const descRec = wrongOffer.records.find((r) => r.type === 10);
+if (descRec) {
+ descRec.raw[descRec.raw.length - 1] ^= 1;
+ check('mismatched offer rejected', /match/.test(throws(() => verifyOfferInvoice(wrongOffer, inv)) || ''));
+}
+
+// forged signer: issuer pin must fail
+const forged = decodeBolt12Invoice(invStr);
+forged.nodeId = '02' + '11'.repeat(32);
+check('wrong signer rejected', /wrong node|signature/.test(throws(() => verifyOfferInvoice(offer, forged)) || ''));
+
+console.log(fails ? `\n❌ ${fails} failure(s)` : '\n✅ bolt12 verifies');
+process.exit(fails ? 1 : 0);