Update PaymentPath, and ClaimAlongRoute arguments
What changed, and why it matters
This commit only changes internal test helper code in the Lightning Dev Kit repository. It adds new optional arguments to testing utilities so that future tests can simulate payments that include fake 'dummy hops' in blinded payment paths. There is no change to production code, no user-facing behavior change, and no security fix or vulnerability.
No action needed. This is a test-only refactoring commit. Continue normal review and testing.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff modifies two test-only files: functional_test_utils.rs and offers_tests.rs. It adds a dummy_tlvs: Vec<DummyTlvs> field to PassAlongPathArgs and an expected_extra_total_fees_msat: u64 field to ClaimAlongRouteArgs, along with builder methods and default values. The code also loops over dummy_tlvs at the final hop to call process_pending_htlc_forwards() once per dummy layer, and adds an extra fee offset when computing expected_total_fee_msat in claim_payment_along_route. All changes are inside test helpers and are explicitly described in the commit message as preparation for upcoming tests.
Changed components
lightning/src/ln/functional_test_utils.rs (test utilities only)lightning/src/ln/offers_tests.rs (test file only)Inspect captured patch +93 / −17
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index ff33d75..6d607eb 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -10,6 +10,7 @@
//! A bunch of useful utilities for building networks of nodes and exchanging messages between
//! nodes for functional tests.
+use crate::blinded_path::payment::DummyTlvs;
use crate::chain::channelmonitor::ChannelMonitor;
use crate::chain::transaction::OutPoint;
use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch};
@@ -3435,6 +3436,7 @@ fn fail_payment_along_path<'a, 'b, 'c>(expected_path: &[&Node<'a, 'b, 'c>]) {
pub struct PassAlongPathArgs<'a, 'b, 'c, 'd> {
pub origin_node: &'a Node<'b, 'c, 'd>,
pub expected_path: &'a [&'a Node<'b, 'c, 'd>],
+ pub dummy_tlvs: Vec<DummyTlvs>,
pub recv_value: u64,
pub payment_hash: PaymentHash,
pub payment_secret: Option<PaymentSecret>,
@@ -3456,6 +3458,7 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> {
Self {
origin_node,
expected_path,
+ dummy_tlvs: vec![],
recv_value,
payment_hash,
payment_secret: None,
@@ -3503,12 +3506,17 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> {
self.expected_failure = Some(failure);
self
}
+ pub fn with_dummy_tlvs(mut self, dummy_tlvs: &[DummyTlvs]) -> Self {
+ self.dummy_tlvs = dummy_tlvs.to_vec();
+ self
+ }
}
pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event> {
let PassAlongPathArgs {
origin_node,
expected_path,
+ dummy_tlvs,
recv_value,
payment_hash: our_payment_hash,
payment_secret: our_payment_secret,
@@ -3543,6 +3551,16 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event>
node.node.process_pending_htlc_forwards();
}
+ if is_last_hop {
+ // At the final hop, the incoming packet contains N dummy-hop layers
+ // before the real HTLC. Each call to `process_pending_htlc_forwards`
+ // strips exactly one dummy layer, so we call it N times.
+ for _ in 0..dummy_tlvs.len() {
+ assert!(node.node.needs_pending_htlc_processing());
+ node.node.process_pending_htlc_forwards();
+ }
+ }
+
if is_last_hop && clear_recipient_events {
let events_2 = node.node.get_and_clear_pending_events();
if payment_claimable_expected {
@@ -3755,6 +3773,29 @@ pub struct ClaimAlongRouteArgs<'a, 'b, 'c, 'd> {
pub origin_node: &'a Node<'b, 'c, 'd>,
pub expected_paths: &'a [&'a [&'a Node<'b, 'c, 'd>]],
pub expected_extra_fees: Vec<u32>,
+ /// A one-off adjustment used only in tests to account for an existing
+ /// fee-handling trade-off in LDK.
+ ///
+ /// When the payer is the introduction node of a blinded path, LDK does not
+ /// subtract the forward fee for the `payer -> next_hop` channel
+ /// (see [`BlindedPaymentPath::advance_path_by_one`]). This keeps the fee
+ /// logic simpler at the cost of a small, intentional overpayment.
+ ///
+ /// In the simple two-hop case (payer as introduction node → payee),
+ /// this overpayment has historically been avoided by simply not charging
+ /// the payer the forward fee, since the payer knows there is only
+ /// a single hop after them.
+ ///
+ /// However, with the introduction of dummy hops in LDK v0.3, even a
+ /// two-node real path (payer as introduction node → payee) may appear as a
+ /// multi-hop blinded path. This makes the existing overpayment surface in
+ /// tests.
+ ///
+ /// Until the fee-handling trade-off is revisited, this field allows tests
+ /// to compensate for that expected difference.
+ ///
+ /// [`BlindedPaymentPath::advance_path_by_one`]: crate::blinded_path::payment::BlindedPaymentPath::advance_path_by_one
+ pub expected_extra_total_fees_msat: u64,
pub expected_min_htlc_overpay: Vec<u32>,
pub skip_last: bool,
pub payment_preimage: PaymentPreimage,
@@ -3778,6 +3819,7 @@ impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> {
origin_node,
expected_paths,
expected_extra_fees: vec![0; expected_paths.len()],
+ expected_extra_total_fees_msat: 0,
expected_min_htlc_overpay: vec![0; expected_paths.len()],
skip_last: false,
payment_preimage,
@@ -3793,6 +3835,10 @@ impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> {
self.expected_extra_fees = extra_fees;
self
}
+ pub fn with_expected_extra_total_fees_msat(mut self, extra_total_fees: u64) -> Self {
+ self.expected_extra_total_fees_msat = extra_total_fees;
+ self
+ }
pub fn with_expected_min_htlc_overpay(mut self, extra_fees: Vec<u32>) -> Self {
self.expected_min_htlc_overpay = extra_fees;
self
@@ -4060,13 +4106,21 @@ pub fn pass_claimed_payment_along_route_from_ev(
expected_total_fee_msat
}
+
pub fn claim_payment_along_route(
args: ClaimAlongRouteArgs,
) -> (Option<PaidBolt12Invoice>, Vec<Event>) {
- let origin_node = args.origin_node;
- let payment_preimage = args.payment_preimage;
- let skip_last = args.skip_last;
- let expected_total_fee_msat = do_claim_payment_along_route(args);
+ let ClaimAlongRouteArgs {
+ origin_node,
+ payment_preimage,
+ skip_last,
+ expected_extra_total_fees_msat,
+ ..
+ } = args;
+
+ let expected_total_fee_msat =
+ do_claim_payment_along_route(args) + expected_extra_total_fees_msat;
+
if !skip_last {
expect_payment_sent!(origin_node, payment_preimage, Some(expected_total_fee_msat))
} else {
diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs
index 4c53aef..0b2d5b8 100644
--- a/lightning/src/ln/offers_tests.rs
+++ b/lightning/src/ln/offers_tests.rs
@@ -185,7 +185,20 @@ fn route_bolt12_payment<'a, 'b, 'c>(
fn claim_bolt12_payment<'a, 'b, 'c>(
node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext, invoice: &Bolt12Invoice
) {
- let recipient = &path[path.len() - 1];
+ claim_bolt12_payment_with_extra_fees(
+ node,
+ path,
+ expected_payment_context,
+ invoice,
+ None,
+ )
+}
+
+fn claim_bolt12_payment_with_extra_fees<'a, 'b, 'c>(
+ node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext, invoice: &Bolt12Invoice,
+ expected_extra_fees_msat: Option<u64>,
+) {
+ let recipient = path.last().expect("Empty path?");
let payment_purpose = match get_event!(recipient, Event::PaymentClaimable) {
Event::PaymentClaimable { purpose, .. } => purpose,
_ => panic!("No Event::PaymentClaimable"),
@@ -194,20 +207,29 @@ fn claim_bolt12_payment<'a, 'b, 'c>(
Some(preimage) => preimage,
None => panic!("No preimage in Event::PaymentClaimable"),
};
- match payment_purpose {
- PaymentPurpose::Bolt12OfferPayment { payment_context, .. } => {
- assert_eq!(PaymentContext::Bolt12Offer(payment_context), expected_payment_context);
- },
- PaymentPurpose::Bolt12RefundPayment { payment_context, .. } => {
- assert_eq!(PaymentContext::Bolt12Refund(payment_context), expected_payment_context);
- },
+ let context = match payment_purpose {
+ PaymentPurpose::Bolt12OfferPayment { payment_context, .. } =>
+ PaymentContext::Bolt12Offer(payment_context),
+ PaymentPurpose::Bolt12RefundPayment { payment_context, .. } =>
+ PaymentContext::Bolt12Refund(payment_context),
_ => panic!("Unexpected payment purpose: {:?}", payment_purpose),
- }
- if let Some(inv) = claim_payment(node, path, payment_preimage) {
- assert_eq!(inv, PaidBolt12Invoice::Bolt12Invoice(invoice.to_owned()));
- } else {
- panic!("Expected PaidInvoice::Bolt12Invoice");
};
+
+ assert_eq!(context, expected_payment_context);
+
+ let expected_paths = [path];
+ let mut args = ClaimAlongRouteArgs::new(
+ node,
+ &expected_paths,
+ payment_preimage,
+ );
+
+ if let Some(extra) = expected_extra_fees_msat {
+ args = args.with_expected_extra_total_fees_msat(extra);
+ }
+
+ let (inv, _) = claim_payment_along_route(args);
+ assert_eq!(inv, Some(PaidBolt12Invoice::Bolt12Invoice(invoice.clone())));
}
fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> Nonce {
Why this scored 15/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.