bolt12: generate payer_proof JSON test vectors.
What changed, and why it matters
This commit is a test-infrastructure change for Core Lightning's BOLT 12 payer proof implementation. It removes debug logging from the production merkle code and adds a new test program that prints canonical JSON test vectors. There is no change to security-sensitive runtime behavior, no bug fix, and no disclosed vulnerability.
No action required; this is a routine test/vector generation commit.
Security signals we found
No security-relevant code change
Test-only addition
Removal of debug logging only
Evidence from the diff
The diff removes SUPERVERBOSE tracing calls from common/bolt12_merkle.c and introduces common/test/run-bolt12_proof_vectors.c, a standalone generator for valid and invalid BOLT 12 payer_proof test vectors. It also updates common/test/Makefile to build the new test binary. The production cryptographic code paths are unchanged except for the removal of verbose debug output; no parsing, validation, or signature logic is modified.
Changed components
common/bolt12_merkle.ccommon/test/run-bolt12_proof_vectors.ccommon/test/MakefileInspect captured patch +587 / −29
diff --git a/common/bolt12_merkle.c b/common/bolt12_merkle.c
index 6fc2e5db..00ac05c3 100644
--- a/common/bolt12_merkle.c
+++ b/common/bolt12_merkle.c
@@ -27,7 +27,6 @@ static void sha256_update_bigsize(struct sha256_ctx *ctx, u64 bigsize)
size_t len;
len = bigsize_put(buf, bigsize);
- SUPERVERBOSE("%s", tal_hexstr(tmpctx, buf, len));
sha256_update(ctx, buf, len);
}
@@ -37,7 +36,6 @@ static void sha256_update_tlvfield(struct sha256_ctx *ctx,
/* We don't keep it raw, so reconstruct. */
sha256_update_bigsize(ctx, field->numtype);
sha256_update_bigsize(ctx, field->length);
- SUPERVERBOSE("%s", tal_hexstr(tmpctx, field->value, field->length));
sha256_update(ctx, field->value, field->length);
}
@@ -51,9 +49,6 @@ static void h_simpletag_ctx(struct sha256_ctx *sctx, const char *tag)
sha256_init(sctx);
sha256_update(sctx, &sha, sizeof(sha));
sha256_update(sctx, &sha, sizeof(sha));
- SUPERVERBOSE("tag=SHA256(%s) -> %s",
- tal_hexstr(tmpctx, tag, strlen(tag)),
- fmt_sha256(tmpctx, &sha));
}
@@ -71,11 +66,8 @@ void bolt12_lnnonce_ctx(struct sha256_ctx *sctx, const struct tlv_field *field)
sha256_init(&inner_sctx);
sha256_update(&inner_sctx, "LnNonce", 7);
- SUPERVERBOSE("tag=SHA256(%s", tal_hexstr(tmpctx, "LnNonce", 7));
sha256_update_tlvfield(&inner_sctx, field);
sha256_done(&inner_sctx, &sha);
- SUPERVERBOSE(") -> %s\n",
- fmt_sha256(tmpctx, &sha));
sha256_init(sctx);
sha256_update(sctx, &sha, sizeof(sha));
@@ -91,23 +83,17 @@ void bolt12_calc_nonce(const struct sha256_ctx *lnnonce_ctx,
/* Copy context, to add field */
struct sha256_ctx ctx = *lnnonce_ctx;
- SUPERVERBOSE("nonce: H(noncetag,");
sha256_update_bigsize(&ctx, fieldtype);
-
sha256_done(&ctx, hash);
- SUPERVERBOSE(") = %s\n", fmt_sha256(tmpctx, hash));
}
static void calc_lnleaf(const struct tlv_field *field, struct sha256 *hash)
{
struct sha256_ctx sctx;
- SUPERVERBOSE("leaf: H(");
h_simpletag_ctx(&sctx, "LnLeaf");
- SUPERVERBOSE(",");
sha256_update_tlvfield(&sctx, field);
sha256_done(&sctx, hash);
- SUPERVERBOSE(") -> %s\n", fmt_sha256(tmpctx, hash));
}
/* BOLT #12:
@@ -122,16 +108,11 @@ static struct sha256 merkle_pair(const struct sha256 *a, const struct sha256 *b)
if (memcmp(a->u.u8, b->u.u8, sizeof(a->u.u8)) > 0)
return merkle_pair(b, a);
- SUPERVERBOSE("branch: H(");
h_simpletag_ctx(&sctx, "LnBranch");
- SUPERVERBOSE(",%s %s",
- tal_hexstr(tmpctx, a->u.u8, sizeof(a->u.u8)),
- tal_hexstr(tmpctx, b->u.u8, sizeof(b->u.u8)));
sha256_update(&sctx, a->u.u8, sizeof(a->u.u8));
sha256_update(&sctx, b->u.u8, sizeof(b->u.u8));
sha256_done(&sctx, &res);
- SUPERVERBOSE(") -> %s\n", fmt_sha256(tmpctx, &res));
return res;
}
@@ -193,19 +174,13 @@ static struct sha256 *merkle_recurse(struct sha256 **base,
if (len == 1)
return arr[0];
- SUPERVERBOSE("Merkle recurse [%zu - %zu] and [%zu - %zu]\n",
- arr - base, arr + len / 2 - 1 - base,
- arr + len / 2 - base, arr + len - 1 - base);
left = merkle_recurse(base, arr, actual_arr, len / 2, resolve_omitted, arg);
right = merkle_recurse(base, arr + len / 2,
actual_arr ? actual_arr + len / 2 : NULL,
len / 2, resolve_omitted, arg);
/* left is never NULL if right is not NULL */
- if (!right) {
- SUPERVERBOSE("[%zu - %zu] is NULL!\n",
- arr + len / 2 - base, arr + len - base);
+ if (!right)
return left;
- }
ret = tal(base, struct sha256);
left_omitted = is_omitted(left);
right_omitted = is_omitted(right);
@@ -254,8 +229,6 @@ void merkle_tlv_full_(struct sha256 *merkle,
struct sha256_ctx lnnonce_ctx;
bool omitted;
- SUPERVERBOSE("nonce tag:");
-
leaves = tal_arr(NULL, struct sha256 *, 0);
actual_leaves = tal_arr(leaves, struct sha256 *, 0);
while ((f = next_field(&omitted, arg)) != NULL) {
diff --git a/common/test/Makefile b/common/test/Makefile
index 355e6596..2f6f2a5d 100644
--- a/common/test/Makefile
+++ b/common/test/Makefile
@@ -117,7 +117,7 @@ common/test/run-bolt12_merkle-json: \
common/base32.o \
common/wireaddr.o
-common/test/run-bolt12_proof: \
+common/test/run-bolt12_proof common/test/run-bolt12_proof_vectors: \
common/bolt12.o \
common/bigsize.o \
common/amount.o \
diff --git a/common/test/run-bolt12_proof_vectors.c b/common/test/run-bolt12_proof_vectors.c
new file mode 100644
index 00000000..4e79d760
--- /dev/null
+++ b/common/test/run-bolt12_proof_vectors.c
@@ -0,0 +1,585 @@
+/* Generate BOLT 12 payer proof test vectors with full intermediate steps.
+ * Output is JSON - pipe through jq for pretty-printing.
+ */
+#include "config.h"
+#include <ccan/err/err.h>
+#include <stdio.h>
+#include "../bech32.c"
+#include "../bech32_util.c"
+#include "../bolt12_merkle.c"
+#include "../bolt12_proof.c"
+#include "../features.c"
+#include <ccan/array_size/array_size.h>
+#include <ccan/str/str.h>
+#include <common/features.h>
+#include <common/setup.h>
+#include <secp256k1_schnorrsig.h>
+
+/* AUTOGENERATED MOCKS START */
+/* Generated stub for pubkey_from_node_id */
+bool pubkey_from_node_id(struct pubkey *key UNNEEDED, const struct node_id *id UNNEEDED)
+{ fprintf(stderr, "pubkey_from_node_id called!\n"); abort(); }
+/* Generated stub for siphash_seed */
+const struct siphash_seed *siphash_seed(void)
+{ fprintf(stderr, "siphash_seed called!\n"); abort(); }
+/* AUTOGENERATED MOCKS END */
+
+static struct pubkey *pubkey_for_letter(const tal_t *ctx, char letter, const char *name)
+{
+ struct secret secret;
+ struct pubkey *pk;
+
+ pk = tal(ctx, struct pubkey);
+ memset(&secret, letter, sizeof(secret));
+ assert(pubkey_from_secret(&secret, pk));
+
+ if (name) {
+ printf("\"%s\":{\"secret\":\"%s\",\"pubkey\":\"%s\"}",
+ name,
+ tal_hexstr(tmpctx, secret.data, sizeof(secret.data)),
+ fmt_pubkey(tmpctx, pk));
+ }
+ return pk;
+}
+
+static secp256k1_keypair keypair_for_letter(char letter)
+{
+ struct secret secret;
+ secp256k1_keypair kp;
+
+ memset(&secret, letter, sizeof(secret));
+ if (secp256k1_keypair_create(secp256k1_ctx, &kp, secret.data) != 1)
+ abort();
+ return kp;
+}
+
+static struct bip340sig *invoice_signature(const tal_t *ctx,
+ struct tlv_invoice *inv,
+ char letter)
+{
+ struct sha256 merkle, sha;
+ struct bip340sig *sig;
+ secp256k1_keypair kp = keypair_for_letter(letter);
+
+ tlv_update_fields(inv, tlv_invoice, &inv->fields);
+ merkle_tlv(inv->fields, &merkle);
+ inv->signature = tal(inv, struct bip340sig);
+ sighash_from_merkle("invoice", "signature", &merkle, &sha);
+
+ sig = tal(ctx, struct bip340sig);
+ assert(secp256k1_schnorrsig_sign32(secp256k1_ctx, sig->u8,
+ sha.u.u8, &kp, NULL) == 1);
+ return sig;
+}
+
+static bool sign_payer(const char *messagename UNNEEDED,
+ const char *fieldname UNNEEDED,
+ const struct sha256 *msg,
+ struct bip340sig *sig,
+ secp256k1_keypair *kp)
+{
+ struct sha256 shash;
+ sighash_from_merkle("payer_proof", "proof_signature", msg, &shash);
+ return secp256k1_schnorrsig_sign32(secp256k1_ctx, sig->u8,
+ shash.u.u8, kp, NULL) == 1;
+}
+
+/* include_all: include every non-signature field (type 0 cannot be in payer proof TLV) */
+static bool include_all(const struct tlv_field *f, void *arg UNNEEDED)
+{
+ return f->numtype != 0;
+}
+
+/* include_minimal: only invreq_payer_id, invoice_payment_hash, invoice_node_id */
+static bool include_minimal(const struct tlv_field *f, void *arg UNNEEDED)
+{
+ return f->numtype == TLV_PAYER_PROOF_INVREQ_PAYER_ID
+ || f->numtype == TLV_PAYER_PROOF_INVOICE_PAYMENT_HASH
+ || f->numtype == TLV_PAYER_PROOF_INVOICE_FEATURES
+ || f->numtype == TLV_PAYER_PROOF_INVOICE_NODE_ID;
+}
+
+/* include_amount: invreq_payer_id, invoice_payment_hash, invoice_node_id
+ * *and* invoice_amount. This demonstrates the case where
+ * proof_missing_hashes are NOT in ascending-type order: the left pair
+ * [type0,type22] is entirely omitted, so its subtree hash is appended *after*
+ * the hash for type82 (which shares the same 4-leaf subtree). The DFS
+ * traversal resolves the right child (type82) before bubbling up to resolve
+ * the left (type0+22).
+ */
+static bool include_amount(const struct tlv_field *f, void *arg UNNEEDED)
+{
+ return include_minimal(f, arg)
+ || f->numtype == TLV_PAYER_PROOF_INVOICE_AMOUNT;
+}
+
+static void print_fields_json(const struct tlv_field *fields, size_t n,
+ bool (*include_field)(const struct tlv_field *, void *),
+ void *arg)
+{
+ for (size_t i = 0; i < n; i++) {
+ const struct tlv_field *f = &fields[i];
+ if (include_field != NULL) {
+ bool included = !is_tlv_signature_field(f)
+ && include_field(f, arg);
+ printf("{\"type\":%"PRIu64",\"len\":%zu,\"hex\":\"%s\",\"included\":%s}%s\n",
+ f->numtype, f->length,
+ tal_hexstr(tmpctx, f->value, f->length),
+ included ? "true" : "false",
+ i + 1 < n ? "," : "");
+ } else {
+ printf("{\"type\":%"PRIu64",\"len\":%zu,\"hex\":\"%s\"}%s\n",
+ f->numtype, f->length,
+ tal_hexstr(tmpctx, f->value, f->length),
+ i + 1 < n ? "," : "");
+ }
+ }
+}
+
+static void print_hashes_json(const struct sha256 *hashes, size_t n)
+{
+ for (size_t i = 0; i < n; i++) {
+ printf("\"%s\"%s\n",
+ fmt_sha256(tmpctx, &hashes[i]),
+ i + 1 < n ? "," : "");
+ }
+}
+
+static void print_bigsize_array(const bigsize_t *arr, size_t n)
+{
+ for (size_t i = 0; i < n; i++) {
+ printf("%"PRIu64"%s\n", arr[i], i + 1 < n ? "," : "");
+ }
+}
+
+static void generate_valid_vector(const char *name,
+ struct tlv_invoice *inv,
+ const struct preimage *preimage,
+ char payer_letter,
+ const char *note,
+ bool (*include_field)(const struct tlv_field *, void *),
+ bool explicit_empty_omitted)
+{
+ struct sha256 mroot, shash;
+ struct tlv_payer_proof *proof;
+ secp256k1_keypair kp = keypair_for_letter(payer_letter);
+ u8 *invoice_wire;
+
+ invoice_wire = tal_arr(tmpctx, u8, 0);
+ towire_tlv_invoice(&invoice_wire, inv);
+
+ merkle_tlv(inv->fields, &mroot);
+ sighash_from_merkle("invoice", "signature", &mroot, &shash);
+ proof = make_unsigned_proof(tmpctx, inv, preimage, note, include_field, NULL);
+ /* Force proof_omitted_tlvs to be present-but-empty rather than absent,
+ * then refresh fields so payer_proof_signature signs the updated merkle. */
+ if (explicit_empty_omitted) {
+ proof->proof_omitted_tlvs = tal_arr(proof, bigsize_t, 0);
+ tlv_update_fields(proof, tlv_payer_proof, &proof->fields);
+ }
+ proof->proof_signature = payer_proof_signature(proof, proof, sign_payer, &kp);
+ assert(proof->proof_signature);
+
+ printf("{\n");
+ printf("\"name\":\"%s\",\n", name);
+
+ printf("\"input\":{\n");
+ printf("\"invoice\":\"%s\",\n", invoice_encode(tmpctx, inv));
+ printf("\"invoice_hex\":\"%s\",\n",
+ 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);
+ printf("\"invoice_fields\":[\n");
+ print_fields_json(inv->fields, tal_count(inv->fields), include_field, NULL);
+ printf("]\n");
+ printf("},\n");
+
+ printf("\"working\":{\n");
+ printf("\"invoice_merkle_root\":\"%s\",\n", fmt_sha256(tmpctx, &mroot));
+ 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_leaf_hashes\":[\n");
+ print_hashes_json(proof->proof_leaf_hashes, tal_count(proof->proof_leaf_hashes));
+ printf("],\n");
+ printf("\"proof_omitted_tlvs\":[\n");
+ print_bigsize_array(proof->proof_omitted_tlvs, tal_count(proof->proof_omitted_tlvs));
+ printf("],\n");
+ printf("\"proof_missing_hashes\":[\n");
+ print_hashes_json(proof->proof_missing_hashes, tal_count(proof->proof_missing_hashes));
+ 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));
+ printf("\"proof_fields\":[\n");
+ print_fields_json(proof->fields, tal_count(proof->fields), NULL, NULL);
+ printf("],\n");
+ const char *bech32 = payer_proof_encode(tmpctx, proof);
+ printf("\"bech32\":\"%s\"\n", bech32);
+ printf("}\n");
+
+ /* Verify round-trip: decode calls check_payer_proof internally */
+ {
+ const char *fail;
+ if (!payer_proof_decode(tmpctx, bech32, strlen(bech32), &fail))
+ abort();
+ }
+
+ printf("}");
+}
+
+/* Build a fresh minimal proof (used as base for invalid vectors). */
+static struct tlv_payer_proof *make_minimal_proof(struct tlv_invoice *inv,
+ const struct preimage *preimage)
+{
+ return make_unsigned_proof(tmpctx, inv, preimage, NULL,
+ include_minimal, NULL);
+}
+
+static void emit_invalid(bool *first, const char *reason,
+ secp256k1_keypair *kp,
+ struct tlv_payer_proof *proof)
+{
+ if (!*first)
+ printf(",\n");
+ *first = false;
+ /* Signature should be valid (we omit it for one test though) */
+ if (kp) {
+ proof->proof_signature = payer_proof_signature(proof, proof,
+ sign_payer, kp);
+ assert(proof->proof_signature);
+ }
+ printf("{\"reason\":\"%s\",\"bech32\":\"%s\"}",
+ reason, payer_proof_encode(tmpctx, proof));
+}
+
+static void generate_invalid_vectors(struct tlv_invoice *inv,
+ const struct preimage *preimage)
+{
+ struct tlv_payer_proof *proof;
+ secp256k1_keypair kp = keypair_for_letter('B');
+ bool first = true;
+ struct sha256 zero_hash;
+
+ memset(&zero_hash, 0, sizeof(zero_hash));
+
+/* Fresh minimal proof, then apply corruption before signing. */
+#define CORRUPT(reason, ...) do { \
+ proof = make_minimal_proof(inv, preimage); \
+ { __VA_ARGS__ } \
+ emit_invalid(&first, reason, &kp, proof); \
+} while(0)
+
+/* Fresh minimal proof, then apply corruption after signing. */
+#define CORRUPT_AFTER_SIG(reason, ...) do { \
+ proof = make_minimal_proof(inv, preimage); \
+ proof->proof_signature = payer_proof_signature(proof, proof, \
+ sign_payer, &kp); \
+ assert(proof->proof_signature); \
+ { __VA_ARGS__ } \
+ emit_invalid(&first, reason, NULL, proof); \
+} while(0)
+
+ /* BOLT-payer_proof #12: MUST reject the payer_proof if:
+ * - `invreq_payer_id`, `invoice_payment_hash`, `invoice_node_id`,
+ * `signature`, `proof_preimage`, `proof_missing_hashes`,
+ * `proof_leaf_hashes` or `proof_signature` are missing.
+ */
+ CORRUPT_AFTER_SIG("missing_invreq_payer_id",
+ proof->invreq_payer_id = NULL;);
+ CORRUPT("missing_invoice_payment_hash",
+ proof->invoice_payment_hash = NULL;);
+ CORRUPT("missing_invoice_node_id",
+ proof->invoice_node_id = NULL;);
+ CORRUPT("missing_signature",
+ proof->signature = NULL;);
+ CORRUPT("missing_proof_preimage",
+ proof->proof_preimage = NULL;);
+ CORRUPT("missing_proof_missing_hashes",
+ proof->proof_missing_hashes = NULL;);
+ CORRUPT("missing_proof_leaf_hashes",
+ proof->proof_leaf_hashes = NULL;);
+ CORRUPT_AFTER_SIG("missing_proof_signature",
+ proof->proof_signature = NULL;);
+
+ /* BOLT-payer_proof #12:
+ * ... - SHA256(`proof_preimage`) does not equal `invoice_payment_hash`. */
+ CORRUPT("wrong_proof_preimage", {
+ proof->proof_preimage = talz(proof, struct preimage);
+ });
+
+ /* BOLT-payer_proof #12:
+ * ... - `proof_omitted_tlvs` are not in strict ascending order (no duplicates). */
+ CORRUPT("proof_omitted_tlvs_not_ascending", {
+ /* Swap last two entries: [1,2,89,90,91,169] -> [1,2,89,90,169,91] */
+ size_t n = tal_count(proof->proof_omitted_tlvs);
+ assert(n >= 2);
+ bigsize_t tmp = proof->proof_omitted_tlvs[n-1];
+ proof->proof_omitted_tlvs[n-1] = proof->proof_omitted_tlvs[n-2];
+ proof->proof_omitted_tlvs[n-2] = tmp;
+ });
+
+ /* BOLT-payer_proof #12:
+ * ... - `proof_omitted_tlvs` contains 0.
+ */
+ CORRUPT("proof_omitted_tlvs_contains_zero", {
+ size_t n = tal_count(proof->proof_omitted_tlvs);
+ bigsize_t *new = tal_arr(proof, bigsize_t, n + 1);
+ new[0] = 0;
+ memcpy(new + 1, proof->proof_omitted_tlvs, n * sizeof(bigsize_t));
+ proof->proof_omitted_tlvs = new;
+ });
+
+ /* BOLT-payer_proof #12:
+ *... - `proof_omitted_tlvs` contains number outside both ranges 1 to
+ * 239 and 1000000000 to 3999999999. */
+ CORRUPT("proof_omitted_tlvs_contains_signature_field",
+ /* Append 241 (a signature field); sig-field check precedes sequential check */
+ tal_arr_expand(&proof->proof_omitted_tlvs, 241););
+ CORRUPT("proof_omitted_tlvs_contains_proof_field",
+ /* Append 1001 (a proof field); sig-field check precedes sequential check */
+ tal_arr_expand(&proof->proof_omitted_tlvs, 1001););
+ CORRUPT("proof_omitted_tlvs_contains_high_field",
+ /* Append 1001 (a proof field); sig-field check precedes sequential check */
+ tal_arr_expand(&proof->proof_omitted_tlvs, 4000000000ULL););
+
+ /* BOLT-payer_proof #12:
+ *... - `proof_omitted_tlvs` contains the number of an included TLV field.
+ */
+ CORRUPT("proof_omitted_tlvs_contains_included_tlv_field",
+ /* invreq_payer_id (88) is included; omitted_tlvs[2]=89 > 88 */
+ assert(proof->invreq_payer_id);
+ assert(proof->proof_omitted_tlvs[2] > TLV_PAYER_PROOF_INVREQ_PAYER_ID);
+ tal_arr_insert(&proof->proof_omitted_tlvs, 2, TLV_PAYER_PROOF_INVREQ_PAYER_ID););
+
+
+ /* BOLT-payer_proof #12:
+ * ...
+ * - `proof_omitted_tlvs` is not one greater than:
+ * - an included TLV number, or
+ * - the previous `proof_omitted_tlvs` or 0 if it is the first number.
+ */
+ CORRUPT("proof_omitted_tlvs_not_sequential", {
+ /* Replace 91 with 100: [1,2,89,90,100,169]; 100 != 90+1 and 99 not included */
+ size_t n = tal_count(proof->proof_omitted_tlvs);
+ assert(n >= 5);
+ proof->proof_omitted_tlvs[n-3] = 100;
+ /* Also fix up subsequent entry to maintain ascending order */
+ proof->proof_omitted_tlvs[n-2] = 101;
+ proof->proof_omitted_tlvs[n-1] = 102;
+ });
+
+ /* BOLT-payer_proof #12:
+ *...
+ * - `proof_leaf_hashes` does not contain exactly one hash for each
+ * non-signature TLV field.
+ */
+ CORRUPT("proof_leaf_hashes_too_few", {
+ size_t n = tal_count(proof->proof_leaf_hashes);
+ assert(n >= 1);
+ tal_resize(&proof->proof_leaf_hashes, n - 1);
+ });
+ CORRUPT("proof_leaf_hashes_too_many",
+ tal_arr_expand(&proof->proof_leaf_hashes, zero_hash););
+
+ /* BOLT-payer_proof #12:
+ * ...
+ * - There are not exactly enough `proof_missing_hashes` to
+ * reconstruct the merkle tree root using the `proof_omitted_tlvs`
+ * values (with `0` implied as the first omitted TLV).
+ */
+ CORRUPT("proof_missing_hashes_too_few", {
+ size_t n = tal_count(proof->proof_missing_hashes);
+ assert(n >= 1);
+ tal_resize(&proof->proof_missing_hashes, n - 1);
+ });
+ CORRUPT("proof_missing_hashes_too_many",
+ tal_arr_expand(&proof->proof_missing_hashes, zero_hash););
+
+ /* BOLT-payer_proof #12:
+ * ...
+ * - `signature` is not a valid signature using `invoice_node_id` as
+ * described in [Signature Calculation](#signature-calculation) (with
+ * `messagename` "invoice") of the reconstructed merkle-root of the
+ * invoice (i.e. without fields 1001 through 999999999 inclusive).
+ */
+ CORRUPT("wrong_invoice_signature",
+ proof->signature->u8[0] ^= 1;);
+
+ /* BOLT-payer_proof #12:
+ * ...
+ * - `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).
+ */
+ CORRUPT_AFTER_SIG("wrong_proof_signature",
+ proof->proof_signature->u8[0] ^= 1;);
+
+ /* BOLT-payer_proof #12:
+ * A writer of a payer_proof:
+ * - MUST NOT include `invreq_metadata`.
+ */
+ /* This is not an explicit reader requirement: it comes under the
+ * "unknown even" rule */
+ {
+ const char *fail;
+ const u8 *data;
+ size_t dlen;
+ u8 *wire;
+ struct tlv_payer_proof *p = make_minimal_proof(inv, preimage);
+ p->proof_signature = payer_proof_signature(p, p, sign_payer, &kp);
+ assert(p->proof_signature);
+ /* Prepend a type-0 TLV (type=0, len=1, value=0x42) to the wire. */
+ wire = tal_arr(tmpctx, u8, 0);
+ towire_bigsize(&wire, 0);
+ towire_bigsize(&wire, 1);
+ towire(&wire, (u8[]){0x42}, 1);
+ data = b12_string_to_data(tmpctx, payer_proof_encode(tmpctx, p),
+ strlen(payer_proof_encode(tmpctx, p)),
+ "lnp", &dlen, &fail);
+ assert(data);
+ towire(&wire, data, dlen);
+ if (!first)
+ printf(",\n");
+ first = false;
+ printf("{\"reason\":\"contains_invreq_metadata\",\"bech32\":\"%s\"}",
+ to_bech32_charset(tmpctx, "lnp", wire));
+ }
+
+#undef CORRUPT
+}
+
+int main(int argc, char *argv[])
+{
+ struct tlv_invoice *inv;
+ struct preimage preimage;
+ const char *invstr, *fail;
+
+ common_setup(argv[0]);
+
+ memset(&preimage, 0x1, sizeof(preimage));
+
+ printf("{\n");
+ printf("\"payer_secret\":\"%s\",\n",
+ tal_hexstr(tmpctx, (u8[32]){[0 ... 31] = 'B'}, 32));
+
+ /* Build a minimal invoice with known keys (letters A-F).
+ * offer_issuer_id must equal invoice_node_id (both use key 'F'). */
+ inv = tlv_invoice_new(tmpctx);
+ inv->invreq_metadata = tal_arrz(inv, u8, 16);
+ printf("\"keys\":{");
+ inv->offer_issuer_id = pubkey_for_letter(inv, 'F', "offer_issuer_id");
+ printf(",");
+ inv->invreq_amount = tal(inv, u64);
+ *inv->invreq_amount = 1000;
+ inv->invreq_payer_id = pubkey_for_letter(inv, 'B', "invreq_payer_id");
+ printf(",");
+
+ inv->invoice_paths = tal_arr(inv, struct blinded_path *, 1);
+ inv->invoice_paths[0] = tal(inv->invoice_paths, struct blinded_path);
+ sciddir_or_pubkey_from_pubkey(&inv->invoice_paths[0]->first_node_id,
+ pubkey_for_letter(tmpctx, 'C', "first_node_id"));
+ printf(",");
+ inv->invoice_paths[0]->first_path_key = *pubkey_for_letter(tmpctx, 'D', "first_path_key");
+ printf(",");
+ inv->invoice_paths[0]->path = tal_arr(inv->invoice_paths[0],
+ struct blinded_path_hop *, 1);
+ inv->invoice_paths[0]->path[0] = tal(inv->invoice_paths[0]->path,
+ struct blinded_path_hop);
+ inv->invoice_paths[0]->path[0]->blinded_node_id = *pubkey_for_letter(tmpctx, 'E', "blinded_node_id");
+ printf(",");
+ inv->invoice_paths[0]->path[0]->encrypted_recipient_data =
+ tal_arrz(inv->invoice_paths[0]->path[0], u8, 16);
+ inv->invoice_blindedpay = tal_arr(inv, struct blinded_payinfo *, 1);
+ inv->invoice_blindedpay[0] = tal(inv->invoice_blindedpay, struct blinded_payinfo);
+ inv->invoice_blindedpay[0]->fee_base_msat = 1;
+ inv->invoice_blindedpay[0]->fee_proportional_millionths = 2;
+ inv->invoice_blindedpay[0]->cltv_expiry_delta = 3;
+ inv->invoice_blindedpay[0]->htlc_minimum_msat = AMOUNT_MSAT(4);
+ inv->invoice_blindedpay[0]->htlc_maximum_msat = AMOUNT_MSAT(5);
+ inv->invoice_blindedpay[0]->features = NULL;
+
+ inv->invoice_created_at = tal(inv, u64);
+ *inv->invoice_created_at = 1733458312;
+ inv->invoice_payment_hash = tal(inv, struct sha256);
+ sha256(inv->invoice_payment_hash, &preimage, sizeof(preimage));
+ inv->invoice_amount = tal(inv, u64);
+ *inv->invoice_amount = 1000;
+ /* invoice_features present (undefined bit 99) to exercise the include_all
+ * path and the "must include if present" writer requirement. */
+ inv->invoice_features = tal_arr(inv, u8, 0);
+ set_feature_bit(&inv->invoice_features, 99);
+ inv->invoice_node_id = pubkey_for_letter(inv, 'F', "invoice_node_id");
+ printf("},\n");
+
+ /* Add experimental field type 3000000001 (non-signature, ignorable). */
+ {
+ struct tlv_field f;
+ f.meta = NULL;
+ f.numtype = 3000000001ULL;
+ f.value = tal_arr(inv, u8, 1);
+ f.value[0] = 0x42;
+ f.length = 1;
+ tlv_update_fields(inv, tlv_invoice, &inv->fields);
+ tal_arr_expand(&inv->fields, f);
+ }
+
+ inv->signature = invoice_signature(inv, inv, 'F');
+
+ /* Canonicalize via encode/decode */
+ invstr = invoice_encode(tmpctx, inv);
+ inv = invoice_decode(tmpctx, invstr, strlen(invstr), NULL, NULL, &fail);
+ assert(inv);
+
+ printf("\"valid_vectors\":[\n");
+ generate_valid_vector("full_disclosure", inv, &preimage, 'B', "", include_all, false);
+ printf(",\n");
+ /* For the rest, remove features and experimental field: keep it vanilla */
+ inv->invoice_features = tal_free(inv->invoice_features);
+ tal_arr_remove(&inv->fields, tal_count(inv->fields)-1);
+
+ /* Re-sign (invoice_signature calls tlv_update_fields, dropping the
+ * now-absent features and experimental entries) then canonicalize. */
+ inv->signature = invoice_signature(inv, inv, 'F');
+ invstr = invoice_encode(tmpctx, inv);
+ inv = invoice_decode(tmpctx, invstr, strlen(invstr), NULL, NULL, &fail);
+ assert(inv);
+
+ generate_valid_vector("minimal_disclosure", inv, &preimage, 'B', "",
+ include_minimal, false);
+ printf(",\n");
+ generate_valid_vector("with_note", inv, &preimage, 'B', "test note",
+ include_minimal, false);
+ printf(",\n");
+ /* This vector demonstrates that proof_missing_hashes are in DFS (left-to-right
+ * tree traversal) order, NOT ascending by TLV type. The pair [type0,type22]
+ * 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', "",
+ 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', "",
+ include_all, true);
+ printf("\n],\n");
+
+ printf("\"invalid_vectors\":[\n");
+ generate_invalid_vectors(inv, &preimage);
+ printf("\n]\n");
+
+ printf("}\n");
+
+ common_shutdown();
+ return 0;
+}
Why this scored 13/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.