common: routines to create and validate payer proofs.
What changed, and why it matters
This commit adds new code for creating and checking 'payer proofs' in Core Lightning, a feature used with BOLT 12 invoices. A payer proof lets someone who paid an invoice selectively reveal parts of it while keeping other parts hidden. The change is a new feature implementation, not a fix for a known bug or vulnerability. There is no evidence in the commit message or diff that this is a security patch or that it addresses any disclosed issue.
Review the new payer proof implementation for spec compliance and edge cases, especially around TLV omission rules, merkle reconstruction, and signature verification. Run the new unit tests and consider additional fuzzing or adversarial test vectors for proof_omitted_tlvs and proof_missing_hashes handling.
Security signals we found
New cryptographic validation code (Schnorr signature verification, SHA256 preimage check)
Merkle tree reconstruction with omitted branches and missing hash resolution
Strict TLV omission/inclusion rules enforced in proof creation and validation
No mention of CVE, security fix, vulnerability, bug, or incident in commit message or diff
No vendor or researcher attribution for a security issue
Evidence from the diff
The commit introduces common/bolt12_proof.c and common/bolt12_proof.h, implementing make_unsigned_proof(), payer_proof_signature(), check_payer_proof(), payer_proof_encode(), and payer_proof_decode() for BOLT 12 payer proofs. It also adds a unit test (common/test/run-bolt12_proof.c) and updates Makefiles. The code follows the BOLT-payer_proof specification, reconstructs merkle roots, validates Schnorr signatures, checks proof preimage against payment hash, and validates omitted TLV marker rules. No security bug fix, vulnerability disclosure, or incident reference is present in the materials.
Changed components
common/bolt12_proof.ccommon/bolt12_proof.hcommon/test/run-bolt12_proof.ccommon/Makefilecommon/test/Makefilewire/MakefileInspect captured patch +821 / −1
diff --git a/common/Makefile b/common/Makefile
index 7c762487..69befa16 100644
--- a/common/Makefile
+++ b/common/Makefile
@@ -18,6 +18,7 @@ COMMON_SRC_NOGEN := \
common/bolt12.c \
common/bolt12_id.c \
common/bolt12_merkle.c \
+ common/bolt12_proof.c \
common/channel_config.c \
common/channel_id.c \
common/channel_type.c \
diff --git a/common/bolt12_proof.c b/common/bolt12_proof.c
new file mode 100644
index 00000000..f8bbe5a3
--- /dev/null
+++ b/common/bolt12_proof.c
@@ -0,0 +1,530 @@
+#include "config.h"
+#include <assert.h>
+#include <bitcoin/preimage.h>
+#include <ccan/array_size/array_size.h>
+#include <ccan/cast/cast.h>
+#include <ccan/tal/str/str.h>
+#include <common/bech32_util.h>
+#include <common/bolt12_merkle.h>
+#include <common/bolt12_proof.h>
+#include <common/utils.h>
+#include <inttypes.h>
+
+struct creator {
+ size_t n_inv, n_included;
+ const struct tlv_invoice *inv;
+ /* A subset of inv->fields */
+ const struct tlv_field *included;
+ struct sha256 *missing_hashes;
+};
+
+static const struct tlv_field *next_field_create(bool *is_omitted,
+ struct creator *creator)
+{
+ if (creator->n_inv >= tal_count(creator->inv->fields))
+ return NULL;
+ /* If all included fields consumed, remaining must be omitted */
+ if (creator->n_included >= tal_count(creator->included))
+ *is_omitted = true;
+ else
+ *is_omitted = (creator->inv->fields[creator->n_inv].numtype
+ != creator->included[creator->n_included].numtype);
+
+ if (!*is_omitted)
+ creator->n_included++;
+ return &creator->inv->fields[creator->n_inv++];
+}
+
+/* BOLT-payer_proof #12:
+ * - MUST populate `proof_missing_hashes` with the merkle hash of the omitted branch
+ * of each internal node that has exactly one branch entirely omitted, in
+ * post-order depth-first smallest-to-largest TLV order.
+ */
+static void add_merkle(struct sha256 *h, struct creator *creator)
+{
+ tal_arr_expand(&creator->missing_hashes, *h);
+}
+
+/* If we've failed to parse but need to produce a hash */
+static struct sha256 dummy_hash(void)
+{
+ struct sha256 hash;
+ memset(hash.u.u8, 1, ARRAY_SIZE(hash.u.u8));
+ return hash;
+}
+
+struct tlv_payer_proof *make_unsigned_proof_(const tal_t *ctx,
+ const struct tlv_invoice *inv,
+ const struct preimage *preimage,
+ const char *note,
+ bool (*include_field)(const struct tlv_field *f, void *),
+ void *arg)
+{
+ size_t last_type = 0, len;
+ struct sha256 *leaf_hashes, merkle;
+ struct creator creator;
+ struct tlv_payer_proof *pptlv;
+ struct tlv_field *included;
+ bigsize_t *omitted;
+ u8 *tlvstream;
+ struct sha256_ctx lnnonce_ctx;
+
+ /* Calculates (H("LnNonce"||TLV0,...) ready for calc_nonce */
+ bolt12_lnnonce_ctx(&lnnonce_ctx, &inv->fields[0]);
+
+ included = tal_arr(tmpctx, struct tlv_field, 0);
+ leaf_hashes = tal_arr(tmpctx, struct sha256, 0);
+ omitted = tal_arr(tmpctx, bigsize_t, 0);
+
+ /* BOLT-payer_proof #12:
+ * - For each non-signature TLV in the invoice in ascending-type order:
+ */
+ for (size_t i = 0; i < tal_count(inv->fields); i++) {
+ const struct tlv_field *f = &inv->fields[i];
+
+ if (is_tlv_signature_field(f))
+ continue;
+
+ /* BOLT-payer_proof #12:
+ * - If the field is to be included in the payer_proof:
+ * - MUST copy it into the payer_proof.
+ * - MUST append the nonce (H("LnNonce"||TLV0,type)) to
+ * `proof_leaf_hashes`.
+ */
+ if (include_field(f, arg)) {
+ struct sha256 hash;
+ tal_arr_expand(&included, *f);
+ bolt12_calc_nonce(&lnnonce_ctx, f->numtype, &hash, NULL);
+ tal_arr_expand(&leaf_hashes, hash);
+ last_type = f->numtype;
+ continue;
+ }
+ /* BOLT-payer_proof #12:
+ * - otherwise, if the TLV type is not zero:
+ * - MUST append a *marker number* to `proof_omitted_tlvs`
+ * - If the previous TLV type was included:
+ * - The *marker number* is that previous tlv type,
+ * plus one.
+ * - Otherwise, if `proof_omitted_tlvs` is empty:
+ * - The *marker number* is 1.
+ * - Otherwise:
+ * - The *marker number* is one greater than the last
+ * `proof_omitted_tlvs` entry.
+ */
+ if (f->numtype != 0)
+ tal_arr_expand(&omitted, ++last_type);
+ }
+
+ /* Arg for next_field_create and add_merkle */
+ creator.n_inv = creator.n_included = 0;
+ creator.inv = inv;
+ creator.included = included;
+ creator.missing_hashes = tal_arr(tmpctx, struct sha256, 0);
+
+ merkle_tlv_full(&merkle,
+ next_field_create, bolt12_calc_nonce, add_merkle,
+ &creator);
+
+ /* Now we make the payer_proof, starting with the invoice fields. */
+ tlvstream = tal_arr(tmpctx, u8, 0);
+ towire_tlvstream_raw(&tlvstream, included);
+ len = tal_bytelen(tlvstream);
+ pptlv = fromwire_tlv_payer_proof(ctx,
+ cast_const2(const u8 **, &tlvstream), &len);
+ assert(pptlv);
+
+ /* BOLT-payer_proof #12:
+ * A writer of a payer_proof:
+ *...
+ * - MUST copy `signature` into the payer_proof.
+ */
+ pptlv->signature = tal_dup(pptlv, struct bip340sig, inv->signature);
+
+ /* BOLT-payer_proof #12:
+ * A writer of a payer_proof:
+ *...
+ * - MUST include `proof_preimage` containing the `payment_preimage` returned from successful payment of this invoice.
+ */
+ pptlv->proof_preimage = tal_dup(pptlv, struct preimage, preimage);
+ pptlv->proof_missing_hashes = tal_steal(pptlv, creator.missing_hashes);
+ /* BOLT-payer_proof #12:
+ * - If `proof_omitted_tlvs` is empty:
+ * - MAY omit `proof_omitted_tlvs` from the payer_proof.
+ */
+ pptlv->proof_omitted_tlvs = tal_count(omitted) ? tal_steal(pptlv, omitted) : NULL;
+ pptlv->proof_leaf_hashes = tal_steal(pptlv, leaf_hashes);
+
+ if (note) {
+ /* Not nul-terminated! */
+ pptlv->proof_note = tal_dup_arr(pptlv, utf8, note, strlen(note), 0);
+ assert(utf8_check(pptlv->proof_note, tal_bytelen(pptlv->proof_note)));
+ }
+
+ /* Make sure pptlv->fields correctly reflects values */
+ tlv_update_fields(pptlv, tlv_payer_proof, &pptlv->fields);
+
+ 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).
+ */
+static void merkle_payer_proof(const struct tlv_field *fields,
+ struct sha256 *merkle)
+{
+ struct tlv0_adding_leaf_iter iter;
+
+ /* We use a modified iterator to insert tlv0. */
+ iter.fields = 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);
+}
+
+struct bip340sig *payer_proof_signature_(const tal_t *ctx,
+ const struct tlv_payer_proof *unsignedproof,
+ bool (*sign)(const char *messagename,
+ const char *fieldname,
+ const struct sha256 *msg,
+ struct bip340sig *sig,
+ void *arg),
+ void *arg)
+{
+ struct sha256 merkle;
+ struct bip340sig *sig;
+
+ merkle_payer_proof(unsignedproof->fields, &merkle);
+
+ sig = tal(ctx, struct bip340sig);
+ if (!sign("payer_proof", "proof_signature", &merkle, sig, arg))
+ sig = tal_free(sig);
+
+ return sig;
+}
+
+struct checker {
+ const struct tlv_payer_proof *pptlv;
+
+ /* Where we're up to in pptlv->fields[] */
+ size_t included_n;
+
+ /* Where we're up to in omitted[] */
+ size_t omitted_n;
+ struct tlv_field *omitted;
+
+ /* Where we're up to in pptlv->proof_leaf_hashes */
+ size_t leaf_hashes_n;
+ bool leaf_hashes_exhausted;
+
+ /* Where we're up to in pptlv->proof_missing_hashes */
+ size_t missing_hashes_n;
+ bool missing_hashes_exhausted;
+};
+
+static const struct tlv_field *next_field_check(bool *is_omitted,
+ struct checker *checker)
+{
+ const struct tlv_field *included, *omitted;
+
+ /* BOLT-payer_proof #12:
+ * A reader of a payer_proof:
+ * - MUST reject the payer_proof if:
+ *...
+ * - `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).
+ */
+next:
+ if (checker->included_n < tal_count(checker->pptlv->fields)) {
+ included = &checker->pptlv->fields[checker->included_n];
+ if (included->numtype >= 1001 && included->numtype <= 999999999) {
+ checker->included_n++;
+ goto next;
+ }
+ } else
+ included = NULL;
+
+ if (checker->omitted_n < tal_count(checker->omitted))
+ omitted = &checker->omitted[checker->omitted_n];
+ else
+ omitted = NULL;
+
+ /* Both exhausted? We finish. */
+ if (!included && !omitted)
+ return NULL;
+
+ /* Only omitted left, or both and omitted comes first */
+ if ((omitted && !included)
+ || (omitted && included && omitted->numtype < included->numtype)) {
+ checker->omitted_n++;
+ *is_omitted = true;
+ return omitted;
+ }
+
+ *is_omitted = false;
+ checker->included_n++;
+ return included;
+}
+
+static void get_leaf_hash(const struct sha256_ctx *lnnonce_ctx,
+ bigsize_t fieldtype,
+ struct sha256 *hash,
+ struct checker *checker)
+{
+ if (checker->leaf_hashes_n >= tal_count(checker->pptlv->proof_leaf_hashes)) {
+ checker->leaf_hashes_exhausted = true;
+ *hash = dummy_hash();
+ } else {
+ *hash = checker->pptlv->proof_leaf_hashes[checker->leaf_hashes_n++];
+ }
+}
+
+static void resolve_omitted(struct sha256 *hash, struct checker *checker)
+{
+ if (checker->missing_hashes_n >= tal_count(checker->pptlv->proof_missing_hashes)) {
+ checker->missing_hashes_exhausted = true;
+ *hash = dummy_hash();
+ } else {
+ *hash = checker->pptlv->proof_missing_hashes[checker->missing_hashes_n++];
+ }
+}
+
+static bool find_tlv_num(const struct tlv_payer_proof *pptlv, bigsize_t num)
+{
+ for (size_t i = 0; i < tal_count(pptlv->fields); i++) {
+ if (pptlv->fields[i].numtype == num)
+ return true;
+ }
+ return false;
+}
+
+const char *check_payer_proof(const tal_t *ctx,
+ const struct tlv_payer_proof *pptlv)
+{
+ struct sha256 hash, merkle, shash;
+ struct checker checker;
+
+ /* BOLT-payer_proof #12:
+ * A reader of a payer_proof:
+ * - 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.
+ */
+ if (!pptlv->invreq_payer_id)
+ return tal_fmt(ctx, "Missing invreq_payer_id");
+ if (!pptlv->invoice_payment_hash)
+ return tal_fmt(ctx, "Missing invoice_payment_hash");
+ if (!pptlv->invoice_node_id)
+ return tal_fmt(ctx, "Missing invoice_node_id");
+ if (!pptlv->signature)
+ return tal_fmt(ctx, "Missing signature");
+ if (!pptlv->proof_preimage)
+ return tal_fmt(ctx, "Missing proof_preimage");
+ if (!pptlv->proof_missing_hashes)
+ return tal_fmt(ctx, "Missing proof_missing_hashes");
+ if (!pptlv->proof_leaf_hashes)
+ return tal_fmt(ctx, "Missing proof_leaf_hashes");
+ if (!pptlv->proof_signature)
+ return tal_fmt(ctx, "Missing proof_signature");
+
+ /* BOLT-payer_proof #12:
+ *...
+ * - SHA256(`proof_preimage`) does not equal `invoice_payment_hash`.
+ */
+ sha256(&hash, pptlv->proof_preimage, sizeof(*pptlv->proof_preimage));
+ if (!sha256_eq(&hash, pptlv->invoice_payment_hash))
+ return tal_fmt(ctx, "Incorrect preimage");
+
+ /* BOLT-payer_proof #12:
+ *...
+ * - `proof_omitted_tlvs` are not in strict ascending order (no duplicates).
+ */
+ for (size_t i = 0; i < tal_count(pptlv->proof_omitted_tlvs); i++) {
+ bigsize_t omitted = pptlv->proof_omitted_tlvs[i], prev_omitted;
+
+ /* BOLT-payer_proof #12:
+ *...
+ * - `proof_omitted_tlvs` contains 0.
+ */
+ if (omitted == 0)
+ return tal_fmt(ctx, "proof_omitted_tlvs[%zu] is 0", i);
+ /* BOLT-payer_proof #12:
+ *...
+ * - `proof_omitted_tlvs` contains number outside both ranges 1 to 239 and 1000000000 to 3999999999.
+ */
+ if (!(omitted >= 1 && omitted <= 239)
+ && !(omitted >= 1000000000 && omitted <= 3999999999)) {
+ return tal_fmt(ctx, "proof_omitted_tlvs[%zi] is"
+ " non-invoiced field %"PRIu64,
+ i, omitted);
+ }
+ /* BOLT-payer_proof #12:
+ *...
+ * - `proof_omitted_tlvs` contains the number of an included TLV
+ * field.
+ */
+ if (find_tlv_num(pptlv, omitted)) {
+ return tal_fmt(ctx, "proof_omitted_tlvs[%zi] is included field %"PRIu64,
+ i, omitted);
+ }
+ /* 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.
+ */
+ if (i > 0)
+ prev_omitted = pptlv->proof_omitted_tlvs[i-1];
+ else
+ prev_omitted = 0;
+
+ if (omitted != prev_omitted + 1) {
+ /* O(n^2) but doesn't matter */
+ if (!find_tlv_num(pptlv, omitted - 1)) {
+ return tal_fmt(ctx, "proof_omitted_tlvs[%zi] is"
+ " not one greater than the previous %"PRIu64" nor an included tlv entry",
+ i, prev_omitted);
+ }
+ }
+ }
+
+ checker.pptlv = pptlv;
+ checker.included_n = 0;
+ checker.omitted_n = 0;
+ checker.leaf_hashes_n = 0;
+ checker.missing_hashes_n = 0;
+ checker.leaf_hashes_exhausted = false;
+ checker.missing_hashes_exhausted = false;
+ /* Make empty "omitted" fields so we can return them. 0 is implied! */
+ checker.omitted = tal_arr(tmpctx, struct tlv_field,
+ 1 + tal_count(pptlv->proof_omitted_tlvs));
+ checker.omitted[0].numtype = 0;
+ checker.omitted[0].length = 0;
+ checker.omitted[0].value = NULL;
+ for (size_t i = 0; i < tal_count(pptlv->proof_omitted_tlvs); i++) {
+ checker.omitted[1 + i].numtype = pptlv->proof_omitted_tlvs[i];
+ checker.omitted[1 + i].length = 0;
+ checker.omitted[1 + i].value = NULL;
+ }
+ merkle_tlv_full(&merkle,
+ next_field_check, get_leaf_hash, resolve_omitted,
+ &checker);
+
+ /* BOLT-payer_proof #12:
+ *...
+ * - `proof_leaf_hashes` does not contain exactly one hash for each
+ * non-signature TLV field.
+ */
+ if (checker.leaf_hashes_exhausted)
+ return tal_fmt(ctx, "Not enough proof_leaf_hashes");
+ else if (checker.leaf_hashes_n != tal_count(pptlv->proof_leaf_hashes))
+ return tal_fmt(ctx, "Too many proof_leaf_hashes");
+
+ /* 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).
+ */
+ if (checker.missing_hashes_exhausted)
+ return tal_fmt(ctx, "Not enough proof_missing_hashes");
+ else if (checker.missing_hashes_n != tal_count(pptlv->proof_missing_hashes))
+ return tal_fmt(ctx, "Too many proof_missing_hashes");
+ /* 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).
+ */
+ sighash_from_merkle("invoice", "signature", &merkle, &shash);
+ if (!check_schnorr_sig(&shash, &pptlv->invoice_node_id->pubkey,
+ pptlv->signature)) {
+ return tal_fmt(ctx, "Invalid invoice signature");
+ }
+
+ /* 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).
+ */
+ merkle_payer_proof(pptlv->fields, &merkle);
+ sighash_from_merkle("payer_proof", "proof_signature", &merkle, &shash);
+ if (!check_schnorr_sig(&shash, &pptlv->invreq_payer_id->pubkey,
+ pptlv->proof_signature)) {
+ return tal_fmt(ctx, "Invalid invoice signature");
+ }
+
+ return NULL;
+}
+
+const char *payer_proof_encode(const tal_t *ctx, const struct tlv_payer_proof *pptlv)
+{
+ u8 *wire;
+
+ wire = tal_arr(tmpctx, u8, 0);
+ towire_tlv_payer_proof(&wire, pptlv);
+
+ return to_bech32_charset(ctx, "lnp", wire);
+}
+
+struct tlv_payer_proof *payer_proof_decode(const tal_t *ctx,
+ const char *b12, size_t b12len,
+ const char **fail)
+{
+ struct tlv_payer_proof *tlvpp;
+ const u8 *data;
+ size_t dlen;
+
+ data = b12_string_to_data(tmpctx, b12, b12len, "lnp", &dlen, fail);
+ if (!data) {
+ tal_steal(ctx, *fail);
+ return NULL;
+ }
+
+ tlvpp = fromwire_tlv_payer_proof(ctx, &data, &dlen);
+ if (!tlvpp) {
+ *fail = tal_fmt(ctx, "invalid payer_proof data");
+ return NULL;
+ }
+
+ *fail = check_payer_proof(ctx, tlvpp);
+ if (*fail)
+ return tal_free(tlvpp);
+
+ return tlvpp;
+}
diff --git a/common/bolt12_proof.h b/common/bolt12_proof.h
new file mode 100644
index 00000000..bfd8cdf6
--- /dev/null
+++ b/common/bolt12_proof.h
@@ -0,0 +1,99 @@
+#ifndef LIGHTNING_COMMON_BOLT12_PROOF_H
+#define LIGHTNING_COMMON_BOLT12_PROOF_H
+#include "config.h"
+#include <ccan/typesafe_cb/typesafe_cb.h>
+#include <wire/bolt12_wiregen.h>
+
+struct preimage;
+
+/**
+ * make_unsigned_proof - make an unsigned proof from this invoice
+ * @ctx: tal context for the returned tlv
+ * @inv: invoice we're creating the proof for
+ * @preimage: preimage proving payment.
+ * @note: optional string (can be NULL) to include in the proof.
+ * @includefn: the function which indicates whether an invoice field should be included.
+ * @arg: parameter for includefn.
+ *
+ * This is a generic helper to make a proof for an invoice. To create a valid proof,
+ * @includefn must return false for the following TLV type 0, and true for various other
+ * fields (see spec quote below).
+ */
+/* BOLT-payer_proof #12:
+ * A writer of a payer_proof:
+ * - MUST NOT include `invreq_metadata`.
+ * - MUST include `invreq_payer_id`, `invoice_payment_hash`,
+ * `invoice_node_id`, `signature` and (if present) `invoice_features` from
+ * the invoice.
+ */
+#define make_unsigned_proof(ctx, inv, preimage, note, includefn, arg) \
+ make_unsigned_proof_((ctx), (inv), (preimage), (note), \
+ typesafe_cb_preargs(bool, \
+ void *, \
+ (includefn), \
+ (arg), \
+ const struct tlv_field *), \
+ arg)
+
+struct tlv_payer_proof *make_unsigned_proof_(const tal_t *ctx,
+ const struct tlv_invoice *inv,
+ const struct preimage *preimage,
+ const char *note,
+ bool (*include_field)(const struct tlv_field *f, void *),
+ void *arg);
+
+/**
+ * payer_proof_signature - make a signature for a payer_proof
+ * @ctx: tal context for the returned proof signature
+ * @unsignedproof: merkle root hash, from make_unsigned_proof.
+ * @signfn: function to sign using the `invreq_payer_id`.
+ * @arg: parameter for @signfn.
+ *
+ * The signfn messagename will be "payer_proof", and fieldname will be
+ * "proof_signature". The msg is the concatentated hash of the note and
+ * the merkle root. It should sign using the invreq_payer_id key, and
+ * return true on success.
+ */
+#define payer_proof_signature(ctx, unsignedproof, signfn, arg) \
+ payer_proof_signature_((ctx), (unsignedproof), \
+ typesafe_cb_preargs(bool, \
+ void *, \
+ (signfn), \
+ (arg), \
+ const char *, \
+ const char *, \
+ const struct sha256 *, \
+ struct bip340sig *), \
+ (arg))
+
+struct bip340sig *payer_proof_signature_(const tal_t *ctx,
+ const struct tlv_payer_proof *unsignedproof,
+ bool (*sign)(const char *messagename,
+ const char *fieldname,
+ const struct sha256 *msg,
+ struct bip340sig *sig,
+ void *arg),
+ void *arg);
+
+/* Check the payer proof signatures are valid: returns NULL if so,
+ * otherwise error string */
+const char *check_payer_proof(const tal_t *ctx,
+ const struct tlv_payer_proof *pptlv);
+
+/**
+ * payer_proof_encode - encode this complete bolt12 payer_proof TLV into text.
+ */
+const char *payer_proof_encode(const tal_t *ctx, const struct tlv_payer_proof *pptlv);
+
+/**
+ * payer_proof_decode - decode this complete bolt12 text into a TLV.
+ * @ctx: the context to allocate return or *@fail off.
+ * @b12: the payer_proof string
+ * @b12len: the payer_proof string length
+ * @fail: pointer to descriptive error string, set if this returns NULL.
+ */
+struct tlv_payer_proof *payer_proof_decode(const tal_t *ctx,
+ const char *b12, size_t b12len,
+ const char **fail);
+
+#endif /* LIGHTNING_COMMON_BOLT12_PROOF_H */
diff --git a/common/test/Makefile b/common/test/Makefile
index a5ca36a9..355e6596 100644
--- a/common/test/Makefile
+++ b/common/test/Makefile
@@ -16,6 +16,9 @@ $(COMMON_TEST_OBJS): $(COMMON_HEADERS) $(WIRE_HEADERS) $(COMMON_SRC) common/test
ALL_C_SOURCES += $(COMMON_TEST_SRC)
ALL_TEST_PROGRAMS += $(COMMON_TEST_PROGRAMS)
+# Make them all depend on common/ files, for simplicity (they directly #include some)
+$(COMMON_TEST_OBJS): $(COMMON_SRC)
+
# Sphinx test wants to decode TLVs.
common/test/run-sphinx: wire/onion_wiregen.o wire/towire.o wire/fromwire.o
common/test/run-blindedpath_enctlv common/test/run-blindedpath_onion: common/base32.o common/wireaddr.o wire/onion_wiregen.o wire/peer_wiregen.o wire/towire.o wire/fromwire.o wire/tlvstream.o
@@ -114,6 +117,16 @@ common/test/run-bolt12_merkle-json: \
common/base32.o \
common/wireaddr.o
+common/test/run-bolt12_proof: \
+ common/bolt12.o \
+ common/bigsize.o \
+ common/amount.o \
+ common/sciddir_or_pubkey.o \
+ wire/bolt12_wiregen.o \
+ wire/onion_wiregen.o \
+ wire/tlvstream.o \
+ wire/fromwire.o \
+ wire/towire.o
common/test/run-version: \
common/amount.o \
diff --git a/common/test/run-bolt12_proof.c b/common/test/run-bolt12_proof.c
new file mode 100644
index 00000000..73f78009
--- /dev/null
+++ b/common/test/run-bolt12_proof.c
@@ -0,0 +1,177 @@
+#include "config.h"
+#include <stdio.h>
+#include "../bolt12_proof.c"
+#include "../bolt12_merkle.c"
+#include "../bech32_util.c"
+#include "../bech32.c"
+#include "../json_parse.c"
+#include "../json_parse_simple.c"
+#include <ccan/ptrint/ptrint.h>
+#include <common/features.h>
+#include <common/setup.h>
+#include <secp256k1_schnorrsig.h>
+
+/* AUTOGENERATED MOCKS START */
+/* Generated stub for features_unsupported */
+int features_unsupported(const struct feature_set *our_features UNNEEDED,
+ const u8 *their_features UNNEEDED,
+ enum feature_place p UNNEEDED)
+{ fprintf(stderr, "features_unsupported called!\n"); abort(); }
+/* Generated stub for mvt_tag_parse */
+bool mvt_tag_parse(const char *buf UNNEEDED, size_t len UNNEEDED, enum mvt_tag *tag UNNEEDED)
+{ fprintf(stderr, "mvt_tag_parse called!\n"); abort(); }
+/* Generated stub for node_id_from_hexstr */
+bool node_id_from_hexstr(const char *str UNNEEDED, size_t slen UNNEEDED, struct node_id *id UNNEEDED)
+{ fprintf(stderr, "node_id_from_hexstr called!\n"); abort(); }
+/* 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 */
+
+/* AAAA... BBBB... etc */
+static struct pubkey *pubkey_for_letter(const tal_t *ctx, char letter)
+{
+ struct secret secret;
+ struct pubkey *pk;
+
+ pk = tal(ctx, struct pubkey);
+ memset(&secret, letter, sizeof(secret));
+ assert(pubkey_from_secret(&secret, 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);
+
+ /* Update fields[] array from our settings */
+ 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;
+}
+
+/* Returns true to include the field */
+static bool exclude_this(const struct tlv_field *f, ptrint_t *p)
+{
+ /* We always exclude 0! */
+ if (f->numtype == 0)
+ return false;
+ if (f->numtype == ptr2int(p))
+ return false;
+ return true;
+}
+
+static bool sign(const char *messagename,
+ const char *fieldname,
+ const struct sha256 *msg,
+ struct bip340sig *sig,
+ secp256k1_keypair *kp)
+{
+ struct sha256 shash;
+
+ sighash_from_merkle(messagename, fieldname, msg, &shash);
+ return secp256k1_schnorrsig_sign32(secp256k1_ctx, sig->u8,
+ shash.u.u8,
+ kp,
+ NULL) == 1;
+}
+
+int main(int argc, char *argv[])
+{
+ struct tlv_invoice *inv;
+ struct preimage preimage;
+ struct tlv_payer_proof *proof;
+ const char *invstr, *fail;
+ secp256k1_keypair kp;
+
+ common_setup(argv[0]);
+
+ memset(&preimage, 0x1, sizeof(preimage));
+
+ /* Minimal invoice */
+ inv = tlv_invoice_new(tmpctx);
+ inv->invreq_metadata = tal_arrz(inv, u8, 16);
+ inv->offer_issuer_id = pubkey_for_letter(inv, 'A');
+
+ inv->invreq_amount = tal(inv, u64);
+ *inv->invreq_amount = 1;
+ inv->invreq_payer_id = pubkey_for_letter(inv, 'B');
+
+ 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'));
+ inv->invoice_paths[0]->first_path_key = *pubkey_for_letter(tmpctx, 'D');
+ 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');
+ 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 = 1;
+ inv->invoice_node_id = pubkey_for_letter(inv, 'F');
+
+ inv->signature = invoice_signature(inv, inv, 'F');
+
+ /* We did OK, right? Also, canonicalizes. */
+ invstr = invoice_encode(tmpctx, inv);
+ inv = invoice_decode(tmpctx, invstr, strlen(invstr), NULL, NULL, &fail);
+ assert(inv);
+
+ /* OK, make a proof (include everything) */
+ proof = make_unsigned_proof(tmpctx, inv, &preimage, "test",
+ exclude_this, int2ptr(0));
+ kp = keypair_for_letter('B');
+ proof->proof_signature = payer_proof_signature(proof, proof, sign, &kp);
+ assert(check_payer_proof(tmpctx, proof) == NULL);
+
+ /* For each field, try excluding it */
+ for (size_t i = 0; i < tal_count(inv->fields); i++) {
+ const char *note = tal_fmt(tmpctx, "test-exclude-%zu", i);
+ proof = make_unsigned_proof(tmpctx, inv, &preimage, note, exclude_this, int2ptr(i));
+ proof->proof_signature = payer_proof_signature(proof, proof, sign, &kp);
+ assert(check_payer_proof(tmpctx, proof) == NULL);
+ }
+
+ common_shutdown();
+ return 0;
+}
diff --git a/wire/Makefile b/wire/Makefile
index 30186441..656b64b4 100644
--- a/wire/Makefile
+++ b/wire/Makefile
@@ -88,7 +88,7 @@ wire/peer_exp_printgen.h_args := --include='wire/channel_type_printgen.h'
wire/onion_exp_wiregen.h_args := $(wire/onion_wiregen.h_args)
wire/onion_exp_wiregen.c_args := $(wire/onion_wiregen.c_args)
-wire/bolt12_wiregen.c_args := -s --expose-tlv-type=tlv_blinded_path --expose-tlv-type=tlv_invoice_request --expose-tlv-type=tlv_invoice --include='wire/onion_wiregen.h'
+wire/bolt12_wiregen.c_args := -s --expose-tlv-type=tlv_blinded_path --expose-tlv-type=tlv_invoice_request --expose-tlv-type=tlv_invoice --expose-tlv-type=tlv_payer_proof --include='wire/onion_wiregen.h'
wire/bolt12_wiregen.h_args := --include='bitcoin/short_channel_id.h' --include='bitcoin/signature.h' --include='bitcoin/privkey.h' --include='common/bigsize.h' --include='common/amount.h' --include='common/node_id.h' --include='bitcoin/block.h' $(wire/bolt12_wiregen.c_args) --include='bitcoin/preimage.h'
wire/bolt12_printgen.c_args := --expose-tlv-type=tlv_blinded_path --expose-tlv-type=tlv_invoice_request --include='wire/onion_wiregen.h' --include='wire/onion_printgen.h'
Why this scored 28/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.