Skip to content

Commit 45cf45c

Browse files
fixup! feat(offers): add BOLT 12 payer proof primitives
1 parent 6743424 commit 45cf45c

2 files changed

Lines changed: 80 additions & 27 deletions

File tree

lightning/src/offers/merkle.rs

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -396,13 +396,28 @@ pub(super) fn compute_selective_disclosure<'a>(
396396
Ok(SelectiveDisclosure { leaf_hashes, omitted_markers, missing_hashes, merkle_root })
397397
}
398398

399+
/// Returns the marker number that follows `prev` (an included TLV type or a
400+
/// previous marker) per BOLT 12 PR 1295.
401+
///
402+
/// A marker is one greater than the previous value, except that a value landing
403+
/// in the gap between the invoice TLV range and the experimental range (the
404+
/// signature/payer-proof range) jumps to the start of the experimental range.
405+
/// The producer and the readers all go through this so their marker sequences
406+
/// stay in agreement.
407+
pub(super) fn next_marker(prev: u64) -> u64 {
408+
let next = prev.saturating_add(1);
409+
if (INVOICE_TYPES.end..EXPERIMENTAL_OFFER_TYPES.start).contains(&next) {
410+
EXPERIMENTAL_OFFER_TYPES.start
411+
} else {
412+
next
413+
}
414+
}
415+
399416
/// Compute omitted markers per BOLT 12 payer proof spec.
400417
///
401-
/// Each omitted TLV gets a marker one greater than the previous included TLV
402-
/// type or the previous marker. If that value would land in the gap between the
403-
/// invoice TLV range and the experimental range, it jumps to the start of the
404-
/// experimental range instead. TLV type 0 is implicitly omitted (never assigned
405-
/// a marker).
418+
/// Each omitted TLV gets the marker number following the previous included TLV
419+
/// type or the previous marker (see [`next_marker`]). TLV type 0 is implicitly
420+
/// omitted (never assigned a marker).
406421
fn compute_omitted_markers<'a>(
407422
tlv_data: impl Iterator<Item = &'a TlvMerkleData> + 'a,
408423
) -> impl Iterator<Item = u64> + 'a {
@@ -413,17 +428,7 @@ fn compute_omitted_markers<'a>(
413428
*prev_value = data.tlv_type;
414429
Some(None)
415430
} else {
416-
// Per BOLT 12 PR 1295, omitted-TLV markers live in either the
417-
// invoice TLV range or the experimental range. A marker that would
418-
// land in the gap between them (the signature/payer-proof range)
419-
// jumps to the start of the experimental range.
420-
let next = prev_value.saturating_add(1);
421-
let marker = if (INVOICE_TYPES.end..EXPERIMENTAL_OFFER_TYPES.start).contains(&next)
422-
{
423-
EXPERIMENTAL_OFFER_TYPES.start
424-
} else {
425-
next
426-
};
431+
let marker = next_marker(*prev_value);
427432
*prev_value = marker;
428433
// Real BOLT 12 invoices have far fewer than 239 non-signature TLVs,
429434
// so the experimental range is never reached in practice; this
@@ -526,7 +531,7 @@ pub(super) fn reconstruct_merkle_root(
526531
} else {
527532
let marker = omitted_markers[mrk_idx];
528533
let inc_type = included_records[inc_idx].r#type;
529-
if marker == prev_marker + 1 {
534+
if marker == next_marker(prev_marker) {
530535
hashes.push(None);
531536
prev_marker = marker;
532537
mrk_idx += 1;
@@ -615,7 +620,7 @@ fn validate_omitted_markers(markers: &[u64]) -> Result<(), SelectiveDisclosureEr
615620
/// - After included type X, the next marker in that run equals X + 1
616621
///
617622
/// The algorithm tracks `prev_marker` to detect continuations vs jumps:
618-
/// - If `marker == prev_marker + 1`: continuation → omitted position
623+
/// - If `marker == next_marker(prev_marker)`: continuation → omitted position
619624
/// - Otherwise: jump → included position comes first, then process marker as continuation
620625
///
621626
/// Example: included=[10, 40], markers=[11, 12, 41, 42]
@@ -652,7 +657,7 @@ fn reconstruct_positions(included_types: &[u64], omitted_markers: &[u64]) -> Vec
652657
let marker = omitted_markers[mrk_idx];
653658
let inc_type = included_types[inc_idx];
654659

655-
if marker == prev_marker + 1 {
660+
if marker == next_marker(prev_marker) {
656661
// Continuation of current run → this position is omitted
657662
positions.push(false);
658663
prev_marker = marker;
@@ -1132,6 +1137,18 @@ mod tests {
11321137
assert_eq!(markers, vec![1_000_000_000, 1_000_000_001]);
11331138
}
11341139

1140+
/// [`next_marker`] increments by one within a range but jumps over the
1141+
/// signature/payer-proof gap, so producer and readers stay in agreement.
1142+
#[test]
1143+
fn next_marker_jumps_the_gap() {
1144+
assert_eq!(super::next_marker(0), 1);
1145+
assert_eq!(super::next_marker(5), 6);
1146+
assert_eq!(super::next_marker(238), 239);
1147+
// 240 would land in the signature range, so it jumps to the experimental range.
1148+
assert_eq!(super::next_marker(239), 1_000_000_000);
1149+
assert_eq!(super::next_marker(1_000_000_000), 1_000_000_001);
1150+
}
1151+
11351152
#[test]
11361153
fn test_tlv_record_read_value_rejects_trailing_bytes() {
11371154
use bitcoin::secp256k1::PublicKey;

lightning/src/offers/payer_proof.rs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ use crate::offers::invoice_request::{
3232
ExperimentalInvoiceRequestTlvStream, InvoiceRequestTlvStream, INVOICE_REQUEST_PAYER_ID_TYPE,
3333
};
3434
use crate::offers::merkle::{
35-
self, SelectiveDisclosure, SelectiveDisclosureError, SignError, TaggedHash, TlvRecord,
36-
TlvStream, SIGNATURE_TYPES,
35+
self, next_marker, SelectiveDisclosure, SelectiveDisclosureError, SignError, TaggedHash,
36+
TlvRecord, TlvStream, SIGNATURE_TYPES,
3737
};
3838
use crate::offers::nonce::Nonce;
3939
use crate::offers::offer::{
@@ -901,10 +901,11 @@ impl TryFrom<Vec<u8>> for PayerProof {
901901
/// above the experimental invoice range (`>= 4_000_000_000`) is rejected.
902902
/// - MUST be in strict ascending order
903903
/// - MUST NOT contain the number of an included TLV field
904-
/// - Markers MUST be minimized: each marker must be exactly prev_value + 1 within
905-
/// a run, and the first marker after an included type X must be X + 1. This
906-
/// naturally allows a trailing run of omitted TLVs after the final included
907-
/// type.
904+
/// - Markers MUST be minimized: each marker is the marker number following the
905+
/// previous marker (or the previous included type X) — one greater, except a
906+
/// value that would land in the signature/payer-proof gap jumps to the
907+
/// experimental range. This naturally allows a trailing run of omitted TLVs
908+
/// after the final included type.
908909
fn validate_omitted_markers_for_parsing(
909910
omitted_markers: &[u64], included_types: &BTreeSet<u64>,
910911
) -> Result<(), DecodeError> {
@@ -945,7 +946,7 @@ fn validate_omitted_markers_for_parsing(
945946
if marker != expected_next {
946947
let mut found = false;
947948
for inc_type in inc_iter.by_ref() {
948-
if inc_type + 1 == marker {
949+
if next_marker(inc_type) == marker {
949950
found = true;
950951
break;
951952
}
@@ -958,7 +959,7 @@ fn validate_omitted_markers_for_parsing(
958959
}
959960
}
960961

961-
expected_next = marker + 1;
962+
expected_next = next_marker(marker);
962963
prev = marker;
963964
}
964965

@@ -1445,6 +1446,41 @@ mod tests {
14451446
assert!(result.is_ok());
14461447
}
14471448

1449+
/// Reproduces the producer/consumer gap-jump mismatch.
1450+
///
1451+
/// `compute_omitted_markers` emits `[1, ..., 239, 1_000_000_000]` for 240
1452+
/// consecutive omitted TLVs (see the merkle.rs test
1453+
/// `compute_omitted_markers_jumps_to_high_range_after_239`): the marker after
1454+
/// 239 jumps over the signature/payer-proof gap into the experimental range.
1455+
/// `validate_omitted_markers_for_parsing` must accept that jump as a valid
1456+
/// minimized sequence, otherwise a proof the producer can legitimately build
1457+
/// is rejected on parse.
1458+
#[test]
1459+
fn test_validate_omitted_markers_accepts_gap_jump() {
1460+
let mut omitted: Vec<u64> = (1..=239).collect();
1461+
omitted.push(1_000_000_000);
1462+
let included: BTreeSet<u64> = BTreeSet::new();
1463+
1464+
let result = validate_omitted_markers_for_parsing(&omitted, &included);
1465+
assert!(result.is_ok(), "gap-jumped markers must be accepted, got {:?}", result);
1466+
}
1467+
1468+
/// An included TLV of type 239 followed by an omitted TLV: the producer emits
1469+
/// marker `next_marker(239)` = `1_000_000_000`. The reader's
1470+
/// jump-after-included-type path must accept it.
1471+
#[test]
1472+
fn test_validate_omitted_markers_accepts_gap_jump_after_included() {
1473+
let omitted = vec![1_000_000_000];
1474+
let included: BTreeSet<u64> = [239].iter().copied().collect();
1475+
1476+
let result = validate_omitted_markers_for_parsing(&omitted, &included);
1477+
assert!(
1478+
result.is_ok(),
1479+
"gap-jump after included type 239 must be accepted, got {:?}",
1480+
result
1481+
);
1482+
}
1483+
14481484
/// Test that non-minimized markers are rejected.
14491485
#[test]
14501486
fn test_validate_omitted_markers_rejects_non_minimized() {

0 commit comments

Comments
 (0)