From 289713c639a9a9c65670dad83ee6d25f23e0a126 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 19 May 2026 15:07:46 +0930 Subject: [PATCH 1/4] common/test: don't use an empty note field in test vectors. It's now optional, so make it NULL here. Reported-by: @t-bast Signed-off-by: Rusty Russell --- common/test/run-bolt12_proof_vectors.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/common/test/run-bolt12_proof_vectors.c b/common/test/run-bolt12_proof_vectors.c index 4e79d760a030..ce535200d572 100644 --- a/common/test/run-bolt12_proof_vectors.c +++ b/common/test/run-bolt12_proof_vectors.c @@ -189,7 +189,8 @@ static void generate_valid_vector(const char *name, tal_hexstr(tmpctx, invoice_wire, tal_bytelen(invoice_wire))); printf("\"preimage\":\"%s\",\n", tal_hexstr(tmpctx, preimage->r, sizeof(preimage->r))); - printf("\"note\":\"%s\",\n", note); + if (note) + printf("\"note\":\"%s\",\n", note); printf("\"invoice_fields\":[\n"); print_fields_json(inv->fields, tal_count(inv->fields), include_field, NULL); printf("]\n"); @@ -540,7 +541,7 @@ int main(int argc, char *argv[]) assert(inv); printf("\"valid_vectors\":[\n"); - generate_valid_vector("full_disclosure", inv, &preimage, 'B', "", include_all, false); + generate_valid_vector("full_disclosure", inv, &preimage, 'B', NULL, include_all, false); printf(",\n"); /* For the rest, remove features and experimental field: keep it vanilla */ inv->invoice_features = tal_free(inv->invoice_features); @@ -553,7 +554,7 @@ int main(int argc, char *argv[]) inv = invoice_decode(tmpctx, invstr, strlen(invstr), NULL, NULL, &fail); assert(inv); - generate_valid_vector("minimal_disclosure", inv, &preimage, 'B', "", + generate_valid_vector("minimal_disclosure", inv, &preimage, 'B', NULL, include_minimal, false); printf(",\n"); generate_valid_vector("with_note", inv, &preimage, 'B', "test note", @@ -564,13 +565,13 @@ int main(int argc, char *argv[]) * is entirely omitted, so its subtree hash appears in proof_missing_hashes AFTER * the hash for the adjacent type82 leaf (which shares the same 4-leaf subtree * and is resolved first during DFS). */ - generate_valid_vector("left_subtree_omitted", inv, &preimage, 'B', "", + generate_valid_vector("left_subtree_omitted", inv, &preimage, 'B', NULL, include_amount, false); printf(",\n"); /* This vector demonstrates that proof_omitted_tlvs present with length 0 is * accepted identically to the field being absent. The spec says writers MAY * omit the field when empty, so readers must accept both forms. */ - generate_valid_vector("empty_proof_omitted_tlvs_explicit", inv, &preimage, 'B', "", + generate_valid_vector("empty_proof_omitted_tlvs_explicit", inv, &preimage, 'B', NULL, include_all, true); printf("\n],\n"); From 9fbc9588fb1acae8182001037fe317637b0fa501 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 19 May 2026 15:14:01 +0930 Subject: [PATCH 2/4] common: fix up test vectors' invoice_merkle_root. Calculate proof_merkle_root properly: we accidentally printed the `invoice_merkle_root` again here. Reported-by: @t-bast. Signed-off-by: Rusty Russell --- common/test/run-bolt12_proof_vectors.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/common/test/run-bolt12_proof_vectors.c b/common/test/run-bolt12_proof_vectors.c index ce535200d572..e8a1e12974e9 100644 --- a/common/test/run-bolt12_proof_vectors.c +++ b/common/test/run-bolt12_proof_vectors.c @@ -160,7 +160,7 @@ static void generate_valid_vector(const char *name, bool (*include_field)(const struct tlv_field *, void *), bool explicit_empty_omitted) { - struct sha256 mroot, shash; + struct sha256 mroot, pproot, shash; struct tlv_payer_proof *proof; secp256k1_keypair kp = keypair_for_letter(payer_letter); u8 *invoice_wire; @@ -179,6 +179,10 @@ static void generate_valid_vector(const char *name, } proof->proof_signature = payer_proof_signature(proof, proof, sign_payer, &kp); assert(proof->proof_signature); + /* Refresh fields to include proof_signature */ + tlv_update_fields(proof, tlv_payer_proof, &proof->fields); + + bolt12_payer_proof_merkle(proof, &pproot); printf("{\n"); printf("\"name\":\"%s\",\n", name); @@ -201,7 +205,7 @@ static void generate_valid_vector(const char *name, printf("\"invoice_sighash\":\"%s\",\n", fmt_sha256(tmpctx, &shash)); printf("\"invoice_signature\":\"%s\",\n", tal_hexstr(tmpctx, inv->signature->u8, sizeof(inv->signature->u8))); - printf("\"proof_merkle_root\":\"%s\",\n", fmt_sha256(tmpctx, &mroot)); + printf("\"proof_merkle_root\":\"%s\",\n", fmt_sha256(tmpctx, &pproot)); printf("\"proof_leaf_hashes\":[\n"); print_hashes_json(proof->proof_leaf_hashes, tal_count(proof->proof_leaf_hashes)); printf("],\n"); @@ -213,9 +217,6 @@ static void generate_valid_vector(const char *name, printf("]\n"); printf("},\n"); - /* Refresh fields to include proof_signature */ - tlv_update_fields(proof, tlv_payer_proof, &proof->fields); - printf("\"result\":{\n"); printf("\"payer_sig\":\"%s\",\n", fmt_bip340sig(tmpctx, proof->proof_signature)); From 5109d06be63b80fb97b89f250a92c6f4b3766e82 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 19 May 2026 15:14:51 +0930 Subject: [PATCH 3/4] common: add test that changing any field invalidates a payer proof. Signed-off-by: Rusty Russell --- common/test/run-bolt12_proof.c | 112 +++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/common/test/run-bolt12_proof.c b/common/test/run-bolt12_proof.c index 73f780093f97..f0484caaf84a 100644 --- a/common/test/run-bolt12_proof.c +++ b/common/test/run-bolt12_proof.c @@ -172,6 +172,118 @@ int main(int argc, char *argv[]) assert(check_payer_proof(tmpctx, proof) == NULL); } + /* Make sure that full proof notices if any field is altered! */ + for (size_t i = 0;; i++) { + u8 *tlvstream; + const u8 *p; + size_t len; + struct tlv_payer_proof *tlvpp; + struct sha256 merkle, shash; + + proof = make_unsigned_proof(tmpctx, inv, &preimage, "test", + exclude_this, int2ptr(0)); + proof->proof_signature = payer_proof_signature(proof, proof, sign, + &kp); + tlv_update_fields(proof, tlv_payer_proof, &proof->fields); + + if (i == tal_count(proof->fields)) + break; + + switch (proof->fields[i].numtype) { + /* Simple ones: we can increment the first byte to + * change the value (and have it still decode) */ + case TLV_PAYER_PROOF_OFFER_CHAINS: + case TLV_PAYER_PROOF_OFFER_METADATA: + case TLV_PAYER_PROOF_OFFER_CURRENCY: + case TLV_PAYER_PROOF_OFFER_AMOUNT: + case TLV_PAYER_PROOF_OFFER_DESCRIPTION: + case TLV_PAYER_PROOF_OFFER_ABSOLUTE_EXPIRY: + case TLV_PAYER_PROOF_OFFER_ISSUER: + case TLV_PAYER_PROOF_OFFER_QUANTITY_MAX: + case TLV_PAYER_PROOF_INVREQ_CHAIN: + case TLV_PAYER_PROOF_INVREQ_AMOUNT: + case TLV_PAYER_PROOF_INVREQ_QUANTITY: + case TLV_PAYER_PROOF_INVREQ_PAYER_NOTE: + case TLV_PAYER_PROOF_INVREQ_BIP_353_NAME: + case TLV_PAYER_PROOF_INVOICE_CREATED_AT: + case TLV_PAYER_PROOF_INVOICE_RELATIVE_EXPIRY: + case TLV_PAYER_PROOF_INVOICE_AMOUNT: + case TLV_PAYER_PROOF_PROOF_NOTE: + case TLV_PAYER_PROOF_INVOICE_PAYMENT_HASH: + case TLV_PAYER_PROOF_PROOF_PREIMAGE: + proof->fields[i].value[0]++; + break; + + /* Put in random signature */ + case TLV_PAYER_PROOF_SIGNATURE: + case TLV_PAYER_PROOF_PROOF_SIGNATURE: { + struct bip340sig sig; + struct sha256 msg; + memset(&msg, 0, sizeof(msg)); + sign("", "", &msg, &sig, &kp); + assert(proof->fields[i].length == sizeof(sig)); + memcpy(proof->fields[i].value, &sig, sizeof(sig)); + break; + } + /* Flip bit 1 here */ + case TLV_PAYER_PROOF_INVOICE_FEATURES: + case TLV_PAYER_PROOF_OFFER_FEATURES: + case TLV_PAYER_PROOF_INVREQ_FEATURES: + proof->fields[i].value[0] ^= 2; + break; + + /* Truncate these */ + case TLV_PAYER_PROOF_INVREQ_PATHS: + case TLV_PAYER_PROOF_INVOICE_PATHS: + case TLV_PAYER_PROOF_PROOF_OMITTED_TLVS: + case TLV_PAYER_PROOF_PROOF_MISSING_HASHES: + case TLV_PAYER_PROOF_PROOF_LEAF_HASHES: + case TLV_PAYER_PROOF_INVOICE_FALLBACKS: + case TLV_PAYER_PROOF_INVOICE_BLINDEDPAY: + case TLV_PAYER_PROOF_OFFER_PATHS: + proof->fields[i].length = 0; + break; + + /* Replace with random pubkey */ + case TLV_PAYER_PROOF_INVOICE_NODE_ID: + case TLV_PAYER_PROOF_INVREQ_PAYER_ID: + case TLV_PAYER_PROOF_OFFER_ISSUER_ID: { + struct secret secret; + struct pubkey pk; + + memset(&secret, 7, sizeof(secret)); + pubkey_from_secret(&secret, &pk); + assert(proof->fields[i].length == PUBKEY_CMPR_LEN); + pubkey_to_der(proof->fields[i].value, &pk); + break; + } + } + + tlvstream = tal_arr(tmpctx, u8, 0); + towire_tlvstream_raw(&tlvstream, proof->fields); + + /* Should demarshal OK */ + len = tal_bytelen(tlvstream); + p = tlvstream; + tlvpp = fromwire_tlv_payer_proof(tmpctx, &p, &len); + assert(tlvpp); + assert(p != NULL); + assert(len == 0); + + /* Proof sig should be invalid if we changed anything!*/ + bolt12_payer_proof_merkle(tlvpp, &merkle); + sighash_from_merkle("payer_proof", "proof_signature", + &merkle, &shash); + + /* Except the invoice signature (that's checked separately). */ + if (!check_schnorr_sig(&shash, &tlvpp->invreq_payer_id->pubkey, + tlvpp->proof_signature)) { + assert(proof->fields[i].numtype != TLV_PAYER_PROOF_SIGNATURE); + } else { + assert(proof->fields[i].numtype == TLV_PAYER_PROOF_SIGNATURE); + } + } + common_shutdown(); return 0; } From 2686d2ab92d78476a58c83617c8f61700d6537d4 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 19 May 2026 15:14:51 +0930 Subject: [PATCH 4/4] common: don't use a dummy zero field for payer proofs. Spec simplification suggested by @t-bast. Signed-off-by: Rusty Russell --- common/bolt12_proof.c | 40 +++------------------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/common/bolt12_proof.c b/common/bolt12_proof.c index 6276a233d673..aec5fef8102e 100644 --- a/common/bolt12_proof.c +++ b/common/bolt12_proof.c @@ -166,46 +166,13 @@ struct tlv_payer_proof *make_unsigned_proof_(const tal_t *ctx, return pptlv; } -struct tlv0_adding_leaf_iter { - const struct tlv_field *fields; - struct tlv_field tlv0; - int n; -}; - -static const struct tlv_field *next_field_prepend_tlv0(bool *is_omitted, - struct tlv0_adding_leaf_iter *iter) -{ - *is_omitted = false; - if (iter->n == -1) { - iter->n = 0; - return &iter->tlv0; - } - if (iter->n >= tal_count(iter->fields)) - return NULL; - return &iter->fields[iter->n++]; -} - /* BOLT-payer_proof #12: - * - MUST set `proof_signature` as detailed in [Signature Calculation](#signature-calculation) using the `invreq_payer_id` using the merkle-root as the `msg` and a `first_tlv` value of 0x0000 (i.e. type 0, length 0). + * - MUST set `proof_signature` as detailed in [Signature Calculation](#signature-calculation) using the `invreq_payer_id` using the merkle-root as the `msg`. */ void bolt12_payer_proof_merkle(const struct tlv_payer_proof *proof, struct sha256 *merkle) { - struct tlv0_adding_leaf_iter iter; - - /* We use a modified iterator to insert tlv0. */ - iter.fields = proof->fields; - iter.n = -1; - iter.tlv0.meta = NULL; - iter.tlv0.numtype = 0; - iter.tlv0.length = 0; - iter.tlv0.value = NULL; - - merkle_tlv_full(merkle, - next_field_prepend_tlv0, - bolt12_calc_nonce, - NULL, - &iter); + merkle_tlv(proof->fields, merkle); } struct bip340sig *payer_proof_signature_(const tal_t *ctx, @@ -479,8 +446,7 @@ const char *check_payer_proof(const tal_t *ctx, *... * - `proof_signature` is not a valid signature using * `invreq_payer_id` as described in [Signature - * Calculation](#signature-calculation), using `msg` merkle-root and - * a `first_tlv` value of 0x0000 (i.e. type 0, length 0). + * Calculation](#signature-calculation), using `msg` merkle-root. */ bolt12_payer_proof_merkle(pptlv, &merkle); sighash_from_merkle("payer_proof", "proof_signature", &merkle, &shash);