Repository navigation
feat: Verify BOLT 12 Signatures With BIP-340 Schnorr - #28
Merged
Merged
Conversation
The merkle root from M7 becomes a verdict: bolt12sig.js tags the root, derives the sighash, and verifies a 64-byte signature against a 33-byte compressed point reduced to its x coordinate. The vendored secp256k1 ships hashes.sha256 unset and expects one supplied. The bolt11 paths pass prehash: false and supply their own digest, so nothing had reached the hook before now; Schnorr hashes internally and every verification returned false until it was wired. BIP-340's own suite is vendored from bitcoin/bips as a sixth source, since BOLT 12 defines its signatures against it. Nineteen vectors, nine valid and ten invalid, covering an off-curve public key, wrong R parity, negated message and s values, an infinite sG - eP, and field-size and curve-order boundaries. The csv is CRLF, which would otherwise leave a carriage return on every comment. An invoice_request is signed by invreq_payer_id. Both that key and offer_issuer_id are present in the stream, so the wrong-key case is asserted to fail rather than left implicit.
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M8 of the BOLT 12 signed-forms plan. Depends on #27.
Problem
M7 built the merkle tree but nothing turns a root into a verdict. Both remaining BOLT 12 forms need that before they can be decoded.
While wiring it up, a latent defect surfaced. The vendored library ships:
Nothing in the repo ever set
hashes.sha256. The BOLT 11 paths pass{ prehash: false }and supply their own digest, so they never reach it — which is why #23 shipped green. Schnorr hashes internally and does reach it, and noble catches the resulting error and returnsfalserather than throwing. So every Schnorr verification silently failed. That failure mode is worth calling out: a signature checker that answersfalseunconditionally looks like working code that rejects everything.Solution
js/bolt12sig.js:plus
xOnlyPoint(33-byte compressed → 32-byte x coordinate),verifyBolt12Signature,signedRecords, andverifySignedStream(messageName, records, signature, point)which rebuilds the root over every record except type 240, tags it, and verifies.Verification
BIP-340's own suite is now vendored from
bitcoin/bipsas a sixth source, since BOLT 12 defines its signatures against it. This is much stronger than the single BOLT 12 signature the plan called for — 19 vectors, 9 valid and 10 invalid:All 19 match, including messages of 0, 1, 17, 32 and 100 bytes. The csv is CRLF, so the extractor splits on
/\r?\n/— otherwise everycommentfield keeps a trailing carriage return and the column lookup fails.The invoice_request vector verifies against
invreq_payer_id, notoffer_issuer_id. My plan text said issuer id; that was wrong. Both keys are present in the stream, so the wrong-key case is asserted to fail rather than left implicit. Confirmed independently by deriving both keys from the privkeys the vector names in its comment:Also asserted: a tampered signature fails, a tampered record fails, the wrong
messagenamefails, the signature record is excluded from the tree it signs, a 63-byte signature is an error, and a stream containing only a signature is an error.No behaviour change: decoded output for all 16 valid invoices and 20 valid offers is byte-identical to
master, and the page still renders 16/16 and 20/20 with the error banner intact. Setting the hook cannot regress BOLT 11 because those paths bypass it.M9 is payer proofs, gated on 5 valid and 23 invalid vectors.