-
Notifications
You must be signed in to change notification settings - Fork 1k
common: fix BOLT 12 payer_proof marker-jump via next_marker helper #9429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vincenzopalazzo
wants to merge
3
commits into
ElementsProject:master
Choose a base branch
from
vincenzopalazzo:bolt12-payer-proof-marker-jump
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+423
−22
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
117764b
common/test: reproduce BOLT 12 PR #1295 marker-jump spec contradiction
vincenzopalazzo b94075b
common: fix BOLT 12 payer_proof marker-jump via next_marker helper
vincenzopalazzo 8a2c9c8
common/test: generate marker-jump vector in run-bolt12_proof_vectors
vincenzopalazzo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| /* End-to-end test of the 239 -> 1000000000 marker-jump scenario from | ||
| * BOLT 12 PR #1295 discussion r3286972971. | ||
| * | ||
| * Spec discussion: | ||
| * https://github.com/lightning/bolts/pull/1295#discussion_r3286972971 | ||
| * | ||
| * The writer rule (lines 1041-1049, head db3eff3a54) and the reader rule | ||
| * (lines 1065-1067) disagree on what happens after a `proof_omitted_tlvs` | ||
| * entry of 239: the writer skips to 1000000000, the reader as originally | ||
| * written rejected anything that was not `prev + 1`. | ||
| * | ||
| * This test exercises the fix on the CLN side, where both the writer-side | ||
| * marker emission and the reader-side validation share a single | ||
| * `next_marker(prev)` helper (see common/bolt12_proof.c). The test: | ||
| * | ||
| * 1. Builds an invoice with the usual required fields PLUS 64 unknown | ||
| * TLVs at types 1000000000..1000000063 (legal payload range). | ||
| * 2. Builds a `payer_proof` that includes only the required fields, | ||
| * omitting offer_issuer_id, the other optional invoice fields, and | ||
| * all 64 dummies. The marker sequence ends with [..., 239, 1000000000] | ||
| * because next_marker(239) == 1000000000. | ||
| * 3. Calls `check_payer_proof` and asserts it returns NULL, then sanity- | ||
| * checks that the produced marker sequence really did contain the | ||
| * 239 -> 1000000000 jump. | ||
| * | ||
| * Parallel LDK reproduction: lightningdevkit/rust-lightning#4297, test | ||
| * `spec_writer_reader_rules_contradict_on_gap_jump` in | ||
| * lightning/src/offers/merkle.rs (commit 0034c79ae). | ||
| */ | ||
| #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 */ | ||
|
|
||
| #define N_DUMMIES 64 | ||
|
|
||
| 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); | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| /* Include only the three required fields; omit everything else. Combined | ||
| * with the 64 dummy TLVs we inject in main(), the resulting marker sequence | ||
| * has the 239 -> 1000000000 jump near the end. */ | ||
| static bool include_required_only(const struct tlv_field *f, void *arg UNNEEDED) | ||
| { | ||
| if (f->numtype == 0) | ||
| return false; | ||
| return f->numtype == TLV_INVOICE_INVREQ_PAYER_ID | ||
| || f->numtype == TLV_INVOICE_INVOICE_PAYMENT_HASH | ||
| || f->numtype == TLV_INVOICE_INVOICE_NODE_ID; | ||
| } | ||
|
|
||
| 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 *err; | ||
| secp256k1_keypair kp; | ||
| bool saw_239 = false, saw_1e9_after_239 = false; | ||
| size_t n_omitted; | ||
|
|
||
| common_setup(argv[0]); | ||
|
|
||
| memset(&preimage, 0x1, sizeof(preimage)); | ||
|
|
||
| /* Standard minimal invoice (same skeleton as run-bolt12_proof.c). */ | ||
| 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'); | ||
|
|
||
| /* Inject 64 dummy unknown TLVs at *odd* types in the extension range | ||
| * (1000000001, 1000000003, ...). Unknown even TLVs are rejected by | ||
| * BOLT-12's general TLV rule, so we keep them odd to be ignored on | ||
| * decode while still appearing in inv->fields and counting toward the | ||
| * marker emission. These get omitted in the proof; combined with the | ||
| * included required fields, the writer-side marker emission walks | ||
| * through the 1..239 range and then jumps to 1000000000 on the 64th | ||
| * dummy. */ | ||
| for (size_t i = 0; i < N_DUMMIES; i++) { | ||
| struct tlv_field f; | ||
| f.meta = NULL; | ||
| f.numtype = 1000000001 + 2 * i; | ||
| f.length = 1; | ||
| f.value = tal_arrz(inv, u8, 1); | ||
| f.value[0] = (u8)(i + 1); | ||
| tal_arr_expand(&inv->fields, f); | ||
| } | ||
|
|
||
| inv->signature = invoice_signature(inv, inv, 'F'); | ||
|
|
||
| /* We skip the usual encode/decode round-trip because invoice_decode | ||
| * enforces invoice-level invariants (signature check, required-field | ||
| * check) that are not what this test is exercising. inv->fields is | ||
| * already canonicalised by invoice_signature's call to | ||
| * tlv_update_fields. */ | ||
|
|
||
| /* Build proof: include the three required fields, omit everything | ||
| * else. The 64 trailing omitted dummies push the marker counter past | ||
| * 239, triggering the gap-jump emission. */ | ||
| proof = make_unsigned_proof(tmpctx, inv, &preimage, "marker-jump", | ||
| include_required_only, NULL); | ||
| kp = keypair_for_letter('B'); | ||
| proof->proof_signature = payer_proof_signature(proof, proof, sign, &kp); | ||
|
|
||
| /* The fix under test: with the next_marker helper used on both writer | ||
| * and reader sides, the produced sequence ends with [..., 239, | ||
| * 1000000000] and check_payer_proof accepts it. */ | ||
| err = check_payer_proof(tmpctx, proof); | ||
| if (err) | ||
| fprintf(stderr, "check_payer_proof returned: %s\n", err); | ||
| assert(err == NULL); | ||
|
|
||
| /* Sanity-check that the marker sequence actually did contain the | ||
| * 239 -> 1000000000 jump we were trying to exercise. Without this | ||
| * check the test could go green for the wrong reason. */ | ||
| n_omitted = tal_count(proof->proof_omitted_tlvs); | ||
| for (size_t i = 0; i < n_omitted; i++) { | ||
| bigsize_t m = proof->proof_omitted_tlvs[i]; | ||
| if (m == 239) | ||
| saw_239 = true; | ||
| if (m == 1000000000 && i > 0 | ||
| && proof->proof_omitted_tlvs[i-1] == 239) | ||
| saw_1e9_after_239 = true; | ||
| } | ||
| fprintf(stderr, "marker sequence (%zu entries):", n_omitted); | ||
| for (size_t i = 0; i < n_omitted; i++) | ||
| fprintf(stderr, " %"PRIu64, proof->proof_omitted_tlvs[i]); | ||
| fprintf(stderr, "\n"); | ||
| fprintf(stderr, "saw 239: %s, saw 1000000000 immediately after: %s\n", | ||
| saw_239 ? "yes" : "no", | ||
| saw_1e9_after_239 ? "yes" : "no"); | ||
| assert(saw_239); | ||
| assert(saw_1e9_after_239); | ||
|
|
||
| common_shutdown(); | ||
| return 0; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
find_included_marker_predecessor()does not exclude the proof's own fields, so the 1000000000 check almost never fails. as i understood a proof withproof_omitted_tlvs = [1000000000]as its only entry - with no field of type 239 anywhere still passes the check.next_marker(0) != 1000000000, butfind_included_marker_predecessorsays yes anyway. Maybe we should also excludenumtypein [1001, 999999999] infind_included_marker_predecessor? As i seenext_field_check()correctly skips this range