diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 03b676adc92..e05a3aeea88 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -109,7 +109,6 @@ impl BlindedPaymentPath { /// Errors if: /// * [`BlindedPayInfo`] calculation results in an integer overflow /// * any unknown features are required in the provided [`ForwardTlvs`] - // TODO: make all payloads the same size with padding + add dummy hops pub fn new( intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64, @@ -136,10 +135,7 @@ impl BlindedPaymentPath { /// /// This improves privacy by making path-length analysis based on fee and CLTV delta /// values less reliable. - /// - /// TODO: Add end-to-end tests validating fee aggregation, CLTV deltas, and - /// HTLC bounds when dummy hops are present, before exposing this API publicly. - pub(crate) fn new_with_dummy_hops< + pub fn new_with_dummy_hops< ES: EntropySource, T: secp256k1::Signing + secp256k1::Verification, >( @@ -392,15 +388,57 @@ pub struct DummyTlvs { pub payment_constraints: PaymentConstraints, } -impl Default for DummyTlvs { - fn default() -> Self { - let payment_relay = - PaymentRelay { cltv_expiry_delta: 0, fee_proportional_millionths: 0, fee_base_msat: 0 }; +// Default parameters used for dummy hops in blinded paths. +// +// These values are chosen to resemble typical forwarding hops while remaining +// stable and predictable for tests. + +/// Adds a realistic but stable CLTV cost per dummy hop. +/// +/// The router folds this into the blinded path's advertised CLTV delta, so it must +/// be non-trivial enough to model hidden relay latency while remaining predictable +/// for tests and callers reasoning about timeout budgets. +pub(crate) const DEFAULT_DUMMY_HOP_CLTV_EXPIRY_DELTA: u16 = 80; + +/// Keeps dummy-hop fee aggregation linear and deterministic. +/// +/// A non-zero proportional fee would compound across dummy hops and introduce rounding +/// effects into blinded payinfo. The base fee still makes dummy hops look like plausible relays. +pub(crate) const DEFAULT_DUMMY_HOP_FEE_PROPORTIONAL_MILLIONTHS: u32 = 0; + +/// Matches the default relay base fee used by the standard test channel configuration. +/// +/// This keeps dummy hops aligned with typical forwarding hops in tests rather than +/// making them appear unrealistically cheap or expensive. +pub(crate) const DEFAULT_DUMMY_HOP_FEE_BASE_MSAT: u32 = 1000; - let payment_constraints = - PaymentConstraints { max_cltv_expiry: u32::MAX, htlc_minimum_msat: 0 }; +/// Leaves the dummy hop's absolute CLTV ceiling effectively unbounded by default. +/// +/// `PaymentConstraints::max_cltv_expiry` is interpreted as an absolute block height, so using a +/// fixed low value here would cause dummy hops to reject otherwise-valid payments on live chains. +pub(crate) const DEFAULT_DUMMY_HOP_MAX_CLTV_EXPIRY: u32 = u32::MAX; - Self { payment_relay, payment_constraints } +/// Matches the default test channel HTLC minimum. +/// +/// The router takes the max of the introduction node's inbound HTLC minimum and this value, +/// so keeping them aligned prevents dummy hops from unexpectedly tightening or loosening +/// admission. +pub(crate) const DEFAULT_DUMMY_HOP_HTLC_MINIMUM_MSAT: u64 = 1000; + +impl Default for DummyTlvs { + /// Returns the documented default relay requirements and constraints for synthetic hops. + fn default() -> Self { + Self { + payment_relay: PaymentRelay { + cltv_expiry_delta: DEFAULT_DUMMY_HOP_CLTV_EXPIRY_DELTA, + fee_proportional_millionths: DEFAULT_DUMMY_HOP_FEE_PROPORTIONAL_MILLIONTHS, + fee_base_msat: DEFAULT_DUMMY_HOP_FEE_BASE_MSAT, + }, + payment_constraints: PaymentConstraints { + max_cltv_expiry: DEFAULT_DUMMY_HOP_MAX_CLTV_EXPIRY, + htlc_minimum_msat: DEFAULT_DUMMY_HOP_HTLC_MINIMUM_MSAT, + }, + } } } diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 011b7f595bc..db8f46ebaa0 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -902,6 +902,12 @@ pub enum Event { /// /// [`ChannelConfig::accept_underpaying_htlcs`]: crate::util::config::ChannelConfig::accept_underpaying_htlcs amount_msat: u64, + /// The additional skimmed fee, in thousandths of a satoshi, that the receiver earns from + /// dummy hops preceding the final receipt, in addition to the `amount_msat`. + /// + /// For backwards compatibility with older LDK versions where this TLV was not serialized, + /// this defaults to 0 when absent. + dummy_hops_skimmed_fee_msat: u64, /// The value, in thousands of a satoshi, that was skimmed off of this payment as an extra fee /// taken by our channel counterparty. /// @@ -965,6 +971,12 @@ pub enum Event { /// The value, in thousandths of a satoshi, that this payment is for. May be greater than the /// invoice amount. amount_msat: u64, + /// The additional skimmed fee, in thousandths of a satoshi, that the receiver earns from + /// dummy hops preceding the final receipt, in addition to the `amount_msat`. + /// + /// For backwards compatibility with older LDK versions where this TLV was not serialized, + /// this defaults to 0 when absent. + dummy_hops_skimmed_fee_msat: u64, /// The purpose of the claimed payment, i.e. whether the payment was for an invoice or a /// spontaneous payment. purpose: PaymentPurpose, @@ -1891,6 +1903,7 @@ impl Writeable for Event { &Event::PaymentClaimable { ref payment_hash, ref amount_msat, + dummy_hops_skimmed_fee_msat, counterparty_skimmed_fee_msat, ref purpose, ref receiver_node_id, @@ -1938,6 +1951,11 @@ impl Writeable for Event { } else { Some(counterparty_skimmed_fee_msat) }; + let dummy_skimmed_fee_opt = if dummy_hops_skimmed_fee_msat == 0 { + None + } else { + Some(dummy_hops_skimmed_fee_msat) + }; let (receiving_channel_id_legacy, receiving_user_channel_id_legacy) = match receiving_channel_ids.last() { @@ -1964,6 +1982,7 @@ impl Writeable for Event { (11, payment_context, option), (13, payment_id, option), (15, *receiving_channel_ids, optional_vec), + (17, dummy_skimmed_fee_opt, option), }); }, &Event::PaymentSent { @@ -2189,6 +2208,7 @@ impl Writeable for Event { &Event::PaymentClaimed { ref payment_hash, ref amount_msat, + dummy_hops_skimmed_fee_msat, ref purpose, ref receiver_node_id, ref htlcs, @@ -2197,6 +2217,11 @@ impl Writeable for Event { ref payment_id, } => { 19u8.write(writer)?; + let dummy_skimmed_fee_opt = if dummy_hops_skimmed_fee_msat == 0 { + None + } else { + Some(dummy_hops_skimmed_fee_msat) + }; write_tlv_fields!(writer, { (0, payment_hash, required), (1, receiver_node_id, option), @@ -2206,6 +2231,7 @@ impl Writeable for Event { (7, sender_intended_total_msat, option), (9, onion_fields, option), (11, payment_id, option), + (13, dummy_skimmed_fee_opt, option), }); }, &Event::ProbeSuccessful { ref payment_id, ref payment_hash, ref path } => { @@ -2407,6 +2433,7 @@ impl MaybeReadable for Event { let mut payment_preimage = None; let mut payment_secret = None; let mut amount_msat = 0; + let mut dummy_skimmed_fee_msat_opt = None; let mut counterparty_skimmed_fee_msat_opt = None; let mut receiver_node_id = None; let mut _user_payment_id = None::; // Used in 0.0.103 and earlier, no longer written in 0.0.116+. @@ -2432,6 +2459,7 @@ impl MaybeReadable for Event { (11, payment_context, option), (13, payment_id, option), (15, receiving_channel_ids_opt, optional_vec), + (17, dummy_skimmed_fee_msat_opt, option), }); let purpose = match payment_secret { Some(secret) => { @@ -2455,6 +2483,7 @@ impl MaybeReadable for Event { receiver_node_id, payment_hash, amount_msat, + dummy_hops_skimmed_fee_msat: dummy_skimmed_fee_msat_opt.unwrap_or(0), counterparty_skimmed_fee_msat: counterparty_skimmed_fee_msat_opt .unwrap_or(0), purpose, @@ -2760,6 +2789,7 @@ impl MaybeReadable for Event { let mut payment_hash = PaymentHash([0; 32]); let mut purpose = UpgradableRequired(None); let mut amount_msat = 0; + let mut dummy_hops_skimmed_fee_msat_opt = None; let mut receiver_node_id = None; let mut htlcs: Option> = Some(vec![]); let mut sender_intended_total_msat: Option = None; @@ -2775,12 +2805,14 @@ impl MaybeReadable for Event { (9, onion_fields, (option: ReadableArgs, sender_intended_total_msat.unwrap_or(amount_msat))), (11, payment_id, option), + (13, dummy_hops_skimmed_fee_msat_opt, option), }); Ok(Some(Event::PaymentClaimed { receiver_node_id, payment_hash, purpose: _init_tlv_based_struct_field!(purpose, upgradable_required), amount_msat, + dummy_hops_skimmed_fee_msat: dummy_hops_skimmed_fee_msat_opt.unwrap_or(0), htlcs: htlcs.unwrap_or_default(), sender_intended_total_msat, onion_fields, diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 25522346d9c..278201dcf33 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -11,7 +11,9 @@ use crate::blinded_path::message::{ BlindedMessagePath, MessageContext, NextMessageHop, OffersContext, }; use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentTlvs}; -use crate::blinded_path::payment::{DummyTlvs, PaymentContext}; +use crate::blinded_path::payment::{ + DummyTlvs, PaymentContext, DEFAULT_DUMMY_HOP_CLTV_EXPIRY_DELTA, +}; use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS}; use crate::events::{ Event, EventsProvider, HTLCHandlingFailureReason, HTLCHandlingFailureType, PaidBolt12Invoice, @@ -982,12 +984,14 @@ fn ignore_duplicate_invoice() { check_added_monitors(&sender, 1); let route: &[&[&Node]] = &[&[always_online_node, async_recipient]]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); - let (res, _) = - claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); + let (res, _) = claim_payment_along_route( + ClaimAlongRouteArgs::new(sender, route, keysend_preimage).with_dummy_tlvs(&dummy_tlvs), + ); assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice.clone()))); // After paying the static invoice, check that regular invoice received from async recipient is ignored. @@ -1063,7 +1067,7 @@ fn ignore_duplicate_invoice() { let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev) .without_clearing_recipient_events() - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); do_pass_along_path(args); let payment_preimage = match get_event!(async_recipient, Event::PaymentClaimable) { @@ -1072,7 +1076,11 @@ fn ignore_duplicate_invoice() { }; // After paying invoice, check that static invoice is ignored. - let res = claim_payment(sender, route[0], payment_preimage); + let res = claim_payment_along_route( + ClaimAlongRouteArgs::new(sender, &[route[0]], payment_preimage) + .with_dummy_tlvs(&dummy_tlvs), + ) + .0; assert_eq!(res, Some(PaidBolt12Invoice::Bolt12Invoice(invoice))); sender.onion_messenger.handle_onion_message(always_online_node_id, &static_invoice_om); @@ -1138,12 +1146,14 @@ fn async_receive_flow_success() { assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); - let (res, _) = - claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); + let (res, _) = claim_payment_along_route( + ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage).with_dummy_tlvs(&dummy_tlvs), + ); assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } @@ -1375,14 +1385,15 @@ fn async_receive_mpp() { _ => panic!(), }; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(&nodes[0], expected_route[0], amt_msat, payment_hash, ev) .without_claimable_event() - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); do_pass_along_path(args); let ev = remove_first_msg_event_to_node(&nodes[2].node.get_our_node_id(), &mut events); let args = PassAlongPathArgs::new(&nodes[0], expected_route[1], amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = match claimable_ev { Event::PaymentClaimable { @@ -1391,11 +1402,10 @@ fn async_receive_mpp() { } => payment_preimage.unwrap(), _ => panic!(), }; - claim_payment_along_route(ClaimAlongRouteArgs::new( - &nodes[0], - expected_route, - keysend_preimage, - )); + claim_payment_along_route( + ClaimAlongRouteArgs::new(&nodes[0], expected_route, keysend_preimage) + .with_dummy_tlvs(&dummy_tlvs), + ); } #[test] @@ -1498,10 +1508,11 @@ fn amount_doesnt_match_invreq() { let payment_hash = extract_payment_hash(&ev); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]]]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); do_pass_along_path(args); // Modify the invoice request stored in our outbounds to be the correct one, to make sure the @@ -1526,10 +1537,12 @@ fn amount_doesnt_match_invreq() { check_added_monitors(&nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[2], &nodes[3]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); - claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); + claim_payment_along_route( + ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage).with_dummy_tlvs(&dummy_tlvs), + ); } #[test] @@ -1717,8 +1730,9 @@ fn invalid_async_receive_with_retry( let payment_hash = extract_payment_hash(&ev); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); do_pass_along_path(args); // Fail the HTLC backwards to enable us to more easily modify the now-Retryable outbound to test @@ -1746,7 +1760,7 @@ fn invalid_async_receive_with_retry( let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); do_pass_along_path(args); fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2]], true); @@ -1759,10 +1773,12 @@ fn invalid_async_receive_with_retry( check_added_monitors(&nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); - claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); + claim_payment_along_route( + ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage).with_dummy_tlvs(&dummy_tlvs), + ); } #[cfg_attr(feature = "std", ignore)] @@ -1933,10 +1949,11 @@ fn expired_static_invoice_payment_path() { check_added_monitors(&nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); do_pass_along_path(args); fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2]], false); nodes[2].logger.assert_log_contains( @@ -2379,11 +2396,14 @@ fn refresh_static_invoices_for_used_offers() { check_added_monitors(&sender, 1); let route: &[&[&Node]] = &[&[server, recipient]]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); - let res = claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); + let res = claim_payment_along_route( + ClaimAlongRouteArgs::new(sender, route, keysend_preimage).with_dummy_tlvs(&dummy_tlvs), + ); assert_eq!(res.0, Some(PaidBolt12Invoice::StaticInvoice(updated_invoice))); } @@ -2714,11 +2734,14 @@ fn invoice_server_is_not_channel_peer() { check_added_monitors(&sender, 1); let route: &[&[&Node]] = &[&[forwarding_node, recipient]]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); - let res = claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); + let res = claim_payment_along_route( + ClaimAlongRouteArgs::new(sender, route, keysend_preimage).with_dummy_tlvs(&dummy_tlvs), + ); assert_eq!(res.0, Some(PaidBolt12Invoice::StaticInvoice(invoice))); } @@ -2954,14 +2977,16 @@ fn async_payment_e2e() { check_added_monitors(&sender_lsp, 1); let path: &[&Node] = &[invoice_server, recipient]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(sender_lsp, path, amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]]; let keysend_preimage = extract_payment_preimage(&claimable_ev); - let (res, _) = - claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); + let (res, _) = claim_payment_along_route( + ClaimAlongRouteArgs::new(sender, route, keysend_preimage).with_dummy_tlvs(&dummy_tlvs), + ); assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } @@ -3011,11 +3036,16 @@ fn held_htlc_timeout() { // Extract the release_htlc_om, but don't deliver it to the sender's LSP. let _ = extract_release_htlc_oms(recipient, &[sender, sender_lsp, invoice_server]); + // Dummy hops add to the blinded path's total advertised CLTV delta. + let additional_cltv_expiry = + DEFAULT_PAYMENT_DUMMY_HOPS as u32 * DEFAULT_DUMMY_HOP_CLTV_EXPIRY_DELTA as u32; + // Connect blocks to the sender's LSP until they timeout the HTLC. connect_blocks( sender_lsp, MIN_CLTV_EXPIRY_DELTA as u32 + TEST_FINAL_CLTV + + additional_cltv_expiry + HTLC_FAIL_BACK_BUFFER + LATENCY_GRACE_PERIOD_BLOCKS, ); @@ -3191,14 +3221,16 @@ fn intercepted_hold_htlc() { check_added_monitors(&lsp, 1); let path: &[&Node] = &[recipient]; - let args = PassAlongPathArgs::new(lsp, path, amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; + let args = + PassAlongPathArgs::new(lsp, path, amt_msat, payment_hash, ev).with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let route: &[&[&Node]] = &[&[lsp, recipient]]; let keysend_preimage = extract_payment_preimage(&claimable_ev); - let (res, _) = - claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); + let (res, _) = claim_payment_along_route( + ClaimAlongRouteArgs::new(sender, route, keysend_preimage).with_dummy_tlvs(&dummy_tlvs), + ); assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } @@ -3294,9 +3326,10 @@ fn async_payment_mpp() { let mut events = lsp_a.node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let ev = remove_first_msg_event_to_node(&recipient.node.get_our_node_id(), &mut events); + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(lsp_a, expected_path, amt_msat, payment_hash, ev) .without_claimable_event() - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); do_pass_along_path(args); lsp_b.node.process_pending_htlc_forwards(); @@ -3305,7 +3338,7 @@ fn async_payment_mpp() { assert_eq!(events.len(), 1); let ev = remove_first_msg_event_to_node(&recipient.node.get_our_node_id(), &mut events); let args = PassAlongPathArgs::new(lsp_b, expected_path, amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = match claimable_ev { @@ -3317,7 +3350,10 @@ fn async_payment_mpp() { }; let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; - claim_payment_along_route(ClaimAlongRouteArgs::new(sender, expected_route, keysend_preimage)); + claim_payment_along_route( + ClaimAlongRouteArgs::new(sender, expected_route, keysend_preimage) + .with_per_path_dummy_tlvs(&vec![dummy_tlvs.to_vec(); expected_route.len()]), + ); } #[test] @@ -3441,13 +3477,15 @@ fn release_htlc_races_htlc_onion_decode() { check_added_monitors(&sender_lsp, 1); let path: &[&Node] = &[invoice_server, recipient]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; let args = PassAlongPathArgs::new(sender_lsp, path, amt_msat, payment_hash, ev) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); let claimable_ev = do_pass_along_path(args).unwrap(); let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]]; let keysend_preimage = extract_payment_preimage(&claimable_ev); - let (res, _) = - claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); + let (res, _) = claim_payment_along_route( + ClaimAlongRouteArgs::new(sender, route, keysend_preimage).with_dummy_tlvs(&dummy_tlvs), + ); assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index e148ce2c474..5381b8bfe36 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -219,7 +219,30 @@ fn one_hop_blinded_path_with_dummy_hops() { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); - let dummy_tlvs = [DummyTlvs::default(); 2]; + let dummy_tlvs = [ + DummyTlvs { + payment_relay: PaymentRelay { + cltv_expiry_delta: 21, + fee_proportional_millionths: 0, + fee_base_msat: 750, + }, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: chan_upd.htlc_minimum_msat, + }, + }, + DummyTlvs { + payment_relay: PaymentRelay { + cltv_expiry_delta: 33, + fee_proportional_millionths: 0, + fee_base_msat: 1_250, + }, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: chan_upd.htlc_minimum_msat, + }, + }, + ]; let mut secp_ctx = Secp256k1::new(); let blinded_path = BlindedPaymentPath::new_with_dummy_hops( @@ -234,6 +257,9 @@ fn one_hop_blinded_path_with_dummy_hops() { &secp_ctx, ) .unwrap(); + assert_eq!(blinded_path.payinfo.fee_base_msat, 2_000); + assert_eq!(blinded_path.payinfo.fee_proportional_millionths, 0); + assert_eq!(blinded_path.payinfo.cltv_expiry_delta, TEST_FINAL_CLTV as u16 + 21 + 33); let route_params = RouteParameters::from_payment_params_and_value( PaymentParameters::blinded(vec![blinded_path]), @@ -254,14 +280,202 @@ fn one_hop_blinded_path_with_dummy_hops() { let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let payment_event = SendEvent::from_event(ev.clone()); + let expected_claimable_cltv = payment_event.msgs[0].cltv_expiry - (21 + 33) as u32; let path = &[&nodes[1]]; let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev) .with_dummy_tlvs(&dummy_tlvs) - .with_payment_secret(payment_secret); + .with_payment_secret(payment_secret) + .with_payment_claimable_cltv(expected_claimable_cltv); do_pass_along_path(args); - claim_payment(&nodes[0], &[&nodes[1]], payment_preimage); + let path: &[&[&Node<'_, '_, '_>]] = &[&[&nodes[1]]]; + let claim_args = + ClaimAlongRouteArgs::new(&nodes[0], path, payment_preimage).with_dummy_tlvs(&dummy_tlvs); + claim_payment_along_route(claim_args); +} + +#[test] +fn one_hop_blinded_path_with_dummy_hops_underpaid() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan_upd = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0).0.contents; + + let amt_msat = 5000; + let (_, payment_hash, payment_secret) = + get_payment_preimage_hash(&nodes[1], Some(amt_msat), None); + let payee_tlvs = ReceiveTlvs { + payment_secret, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: chan_upd.htlc_minimum_msat, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + }; + let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); + let dummy_tlvs = [DummyTlvs::default(); 2]; + + let mut secp_ctx = Secp256k1::new(); + // Advertise a receive path that includes dummy-hop relay requirements. + let blinded_path = BlindedPaymentPath::new_with_dummy_hops( + &[], + nodes[1].node.get_our_node_id(), + &dummy_tlvs, + receive_auth_key, + payee_tlvs, + u64::MAX, + TEST_FINAL_CLTV as u16, + &chanmon_cfgs[1].keys_manager, + &secp_ctx, + ) + .unwrap(); + assert!(blinded_path.payinfo.fee_base_msat > 0); + assert!(blinded_path.payinfo.cltv_expiry_delta > TEST_FINAL_CLTV as u16); + + let mut route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::blinded(vec![blinded_path]), + amt_msat, + ); + if let Payee::Blinded { ref mut route_hints, .. } = route_params.payment_params.payee { + // Simulate a payer that funds only the recipient amount, not the dummy-hop fees. + route_hints[0].payinfo.fee_base_msat = 0; + route_hints[0].payinfo.fee_proportional_millionths = 0; + } else { + panic!(); + } + + nodes[0] + .node + .send_payment( + payment_hash, + RecipientOnionFields::spontaneous_empty(amt_msat), + PaymentId(payment_hash.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + + let path = &[&nodes[1]]; + let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev) + // The receiver rejects the HTLC while processing the hidden dummy hops. + .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) + .with_payment_secret(payment_secret) + .with_dummy_tlvs(&dummy_tlvs); + do_pass_along_path(args); + + let mut updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); + // Blinded receive failures are surfaced to the sender as malformed onion blinding errors. + nodes[0].node.handle_update_fail_malformed_htlc( + nodes[1].node.get_our_node_id(), + &updates.update_fail_malformed_htlcs[0], + ); + do_commitment_signed_dance(&nodes[0], &nodes[1], &updates.commitment_signed, false, false); + expect_payment_failed_conditions( + &nodes[0], + payment_hash, + false, + PaymentFailedConditions::new() + .expected_htlc_error_data(LocalHTLCFailureReason::InvalidOnionBlinding, &[0; 32]), + ); +} + +#[test] +fn one_hop_blinded_path_with_dummy_hops_underadvertised_htlc_minimum_fails() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan_upd = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0).0.contents; + + let amt_msat = 5_000; + let (_, payment_hash, payment_secret) = + get_payment_preimage_hash(&nodes[1], Some(amt_msat), None); + let payee_tlvs = ReceiveTlvs { + payment_secret, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: chan_upd.htlc_minimum_msat, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + }; + let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); + let dummy_tlvs = [DummyTlvs { + payment_relay: PaymentRelay { + cltv_expiry_delta: 18, + fee_proportional_millionths: 0, + fee_base_msat: 500, + }, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: amt_msat + 2_000, + }, + }]; + + let mut secp_ctx = Secp256k1::new(); + let blinded_path = BlindedPaymentPath::new_with_dummy_hops( + &[], + nodes[1].node.get_our_node_id(), + &dummy_tlvs, + receive_auth_key, + payee_tlvs, + u64::MAX, + TEST_FINAL_CLTV as u16, + &chanmon_cfgs[1].keys_manager, + &secp_ctx, + ) + .unwrap(); + assert!(blinded_path.payinfo.htlc_minimum_msat > amt_msat); + + let mut route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::blinded(vec![blinded_path]), + amt_msat, + ); + if let Payee::Blinded { ref mut route_hints, .. } = route_params.payment_params.payee { + route_hints[0].payinfo.htlc_minimum_msat = amt_msat; + } else { + panic!(); + } + + nodes[0] + .node + .send_payment( + payment_hash, + RecipientOnionFields::spontaneous_empty(amt_msat), + PaymentId(payment_hash.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + + let payment_event = SendEvent::from_event(ev); + nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &payment_event.msgs[0]); + check_added_monitors(&nodes[1], 0); + do_commitment_signed_dance(&nodes[1], &nodes[0], &payment_event.commitment_msg, false, false); + while nodes[1].node.needs_pending_htlc_processing() { + nodes[1].node.process_pending_htlc_forwards(); + } + expect_htlc_handling_failed_destinations!( + nodes[1].node.get_and_clear_pending_events(), + &[HTLCHandlingFailureType::InvalidOnion] + ); + let mut updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); + assert_eq!(updates.update_fail_htlcs.len() + updates.update_fail_malformed_htlcs.len(), 1); + check_added_monitors(&nodes[1], 1); } #[test] @@ -1586,6 +1800,7 @@ fn update_add_msg( cltv_expiry, payment_hash: PaymentHash([0; 32]), onion_routing_packet, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point, hold_htlc: None, diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 9361cd3c749..81d1f7bb660 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -518,6 +518,7 @@ struct OutboundHTLCOutput { state: OutboundHTLCState, source: HTLCSource, blinding_point: Option, + dummy_hops_skimmed_fee_msat: Option, skimmed_fee_msat: Option, send_timestamp: Option, hold_htlc: Option<()>, @@ -536,6 +537,7 @@ enum HTLCUpdateAwaitingACK { payment_hash: PaymentHash, source: HTLCSource, onion_routing_packet: msgs::OnionPacket, + dummy_hops_skimmed_fee_msat: Option, // The extra fee we're skimming off the top of this HTLC. skimmed_fee_msat: Option, blinding_point: Option, @@ -8196,6 +8198,7 @@ where ref payment_hash, ref source, ref onion_routing_packet, + dummy_hops_skimmed_fee_msat, skimmed_fee_msat, blinding_point, hold_htlc, @@ -8208,6 +8211,7 @@ where source.clone(), onion_routing_packet.clone(), false, + dummy_hops_skimmed_fee_msat, skimmed_fee_msat, blinding_point, hold_htlc.is_some(), @@ -9521,6 +9525,7 @@ where payment_hash: htlc.payment_hash, cltv_expiry: htlc.cltv_expiry, onion_routing_packet: (**onion_packet).clone(), + dummy_hops_skimmed_fee_msat: htlc.dummy_hops_skimmed_fee_msat, skimmed_fee_msat: htlc.skimmed_fee_msat, blinding_point: htlc.blinding_point, hold_htlc: htlc.hold_htlc, @@ -12380,7 +12385,8 @@ where /// commitment update. pub fn queue_add_htlc( &mut self, amount_msat: u64, payment_hash: PaymentHash, cltv_expiry: u32, - source: HTLCSource, onion_routing_packet: msgs::OnionPacket, skimmed_fee_msat: Option, + source: HTLCSource, onion_routing_packet: msgs::OnionPacket, + dummy_hops_skimmed_fee_msat: Option, skimmed_fee_msat: Option, blinding_point: Option, accountable: bool, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> Result<(), (LocalHTLCFailureReason, String)> { @@ -12391,6 +12397,7 @@ where source, onion_routing_packet, true, + dummy_hops_skimmed_fee_msat, skimmed_fee_msat, blinding_point, // This method is only called for forwarded HTLCs, which are never held at the next hop @@ -12426,8 +12433,9 @@ where fn send_htlc( &mut self, amount_msat: u64, payment_hash: PaymentHash, cltv_expiry: u32, source: HTLCSource, onion_routing_packet: msgs::OnionPacket, mut force_holding_cell: bool, - skimmed_fee_msat: Option, blinding_point: Option, hold_htlc: bool, - accountable: bool, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, + dummy_hops_skimmed_fee_msat: Option, skimmed_fee_msat: Option, + blinding_point: Option, hold_htlc: bool, accountable: bool, + fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> Result { if !matches!(self.context.channel_state, ChannelState::ChannelReady(_)) || self.context.channel_state.is_local_shutdown_sent() @@ -12507,6 +12515,7 @@ where cltv_expiry, source, onion_routing_packet, + dummy_hops_skimmed_fee_msat, skimmed_fee_msat, blinding_point, hold_htlc: hold_htlc.then(|| ()), @@ -12530,6 +12539,7 @@ where state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())), source, blinding_point, + dummy_hops_skimmed_fee_msat, skimmed_fee_msat, send_timestamp, hold_htlc: hold_htlc.then(|| ()), @@ -12772,9 +12782,9 @@ where /// [`Self::send_htlc`] and [`Self::build_commitment_no_state_update`] for more info. pub fn send_htlc_and_commit( &mut self, amount_msat: u64, payment_hash: PaymentHash, cltv_expiry: u32, - source: HTLCSource, onion_routing_packet: msgs::OnionPacket, skimmed_fee_msat: Option, - hold_htlc: bool, accountable: bool, fee_estimator: &LowerBoundedFeeEstimator, - logger: &L, + source: HTLCSource, onion_routing_packet: msgs::OnionPacket, + dummy_hops_skimmed_fee_msat: Option, skimmed_fee_msat: Option, hold_htlc: bool, + accountable: bool, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, ) -> Result, ChannelError> { let send_res = self.send_htlc( amount_msat, @@ -12783,6 +12793,7 @@ where source, onion_routing_packet, false, + dummy_hops_skimmed_fee_msat, skimmed_fee_msat, None, hold_htlc, @@ -14409,6 +14420,7 @@ impl Writeable for FundedChannel { // but we still serialize the option to maintain backwards compatibility let mut preimages: Vec> = vec![]; let mut fulfill_attribution_data = vec![]; + let mut pending_outbound_dummy_hops_skimmed_fees: Vec> = Vec::new(); let mut pending_outbound_skimmed_fees: Vec> = Vec::new(); let mut pending_outbound_blinding_points: Vec> = Vec::new(); let mut pending_outbound_held_htlc_flags: Vec> = Vec::new(); @@ -14453,6 +14465,7 @@ impl Writeable for FundedChannel { reason.write(writer)?; }, } + pending_outbound_dummy_hops_skimmed_fees.push(htlc.dummy_hops_skimmed_fee_msat); pending_outbound_skimmed_fees.push(htlc.skimmed_fee_msat); pending_outbound_blinding_points.push(htlc.blinding_point); pending_outbound_held_htlc_flags.push(htlc.hold_htlc); @@ -14460,6 +14473,8 @@ impl Writeable for FundedChannel { } let holding_cell_htlc_update_count = self.context.holding_cell_htlc_updates.len(); + let mut holding_cell_dummy_hops_skimmed_fees: Vec> = + Vec::with_capacity(holding_cell_htlc_update_count); let mut holding_cell_skimmed_fees: Vec> = Vec::with_capacity(holding_cell_htlc_update_count); let mut holding_cell_blinding_points: Vec> = @@ -14481,6 +14496,7 @@ impl Writeable for FundedChannel { ref payment_hash, ref source, ref onion_routing_packet, + dummy_hops_skimmed_fee_msat, blinding_point, skimmed_fee_msat, hold_htlc, @@ -14493,6 +14509,7 @@ impl Writeable for FundedChannel { source.write(writer)?; onion_routing_packet.write(writer)?; + holding_cell_dummy_hops_skimmed_fees.push(dummy_hops_skimmed_fee_msat); holding_cell_skimmed_fees.push(skimmed_fee_msat); holding_cell_blinding_points.push(blinding_point); holding_cell_held_htlc_flags.push(hold_htlc); @@ -14737,6 +14754,7 @@ impl Writeable for FundedChannel { (28, holder_max_accepted_htlcs, option), (29, self.context.temporary_channel_id, option), (31, channel_pending_event_emitted, option), + (33, pending_outbound_dummy_hops_skimmed_fees, optional_vec), // Added in 0.3 (35, pending_outbound_skimmed_fees, optional_vec), (37, holding_cell_skimmed_fees, optional_vec), (38, self.context.is_batch_funding, option), @@ -14764,6 +14782,7 @@ impl Writeable for FundedChannel { (75, inbound_committed_update_adds, optional_vec), (77, holding_cell_accountable_flags, optional_vec), // Added in 0.3 (79, pending_outbound_accountable, optional_vec), // Added in 0.3 + (81, holding_cell_dummy_hops_skimmed_fees, optional_vec), // Added in 0.3 }); Ok(()) @@ -14924,6 +14943,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> }, _ => return Err(DecodeError::InvalidValue), }, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, send_timestamp: None, @@ -14945,6 +14965,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> payment_hash: Readable::read(reader)?, source: Readable::read(reader)?, onion_routing_packet: Readable::read(reader)?, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, @@ -15117,7 +15138,9 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> let mut blocked_monitor_updates = Some(Vec::new()); let mut pending_outbound_skimmed_fees_opt: Option>> = None; + let mut pending_outbound_dummy_hops_skimmed_fees_opt: Option>> = None; let mut holding_cell_skimmed_fees_opt: Option>> = None; + let mut holding_cell_dummy_hops_skimmed_fees_opt: Option>> = None; let mut is_batch_funding: Option<()> = None; @@ -15180,6 +15203,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> (28, holder_max_accepted_htlcs, option), (29, temporary_channel_id, option), (31, channel_pending_event_emitted, option), + (33, pending_outbound_dummy_hops_skimmed_fees_opt, optional_vec), (35, pending_outbound_skimmed_fees_opt, optional_vec), (37, holding_cell_skimmed_fees_opt, optional_vec), (38, is_batch_funding, option), @@ -15207,6 +15231,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> (75, inbound_committed_update_adds_opt, optional_vec), (77, holding_cell_accountable, optional_vec), // Added in 0.3 (79, pending_outbound_accountable, optional_vec), // Added in 0.3 + (81, holding_cell_dummy_hops_skimmed_fees_opt, optional_vec), }); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -15264,6 +15289,15 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> let holder_max_accepted_htlcs = holder_max_accepted_htlcs.unwrap_or(DEFAULT_MAX_HTLCS); + if let Some(dummy_hops_skimmed_fees) = pending_outbound_dummy_hops_skimmed_fees_opt { + let mut iter = dummy_hops_skimmed_fees.into_iter(); + for htlc in pending_outbound_htlcs.iter_mut() { + htlc.dummy_hops_skimmed_fee_msat = iter.next().ok_or(DecodeError::InvalidValue)?; + } + if iter.next().is_some() { + return Err(DecodeError::InvalidValue); + } + } if let Some(skimmed_fees) = pending_outbound_skimmed_fees_opt { let mut iter = skimmed_fees.into_iter(); for htlc in pending_outbound_htlcs.iter_mut() { @@ -15274,6 +15308,20 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> return Err(DecodeError::InvalidValue); } } + if let Some(dummy_hops_skimmed_fees) = holding_cell_dummy_hops_skimmed_fees_opt { + let mut iter = dummy_hops_skimmed_fees.into_iter(); + for htlc in holding_cell_htlc_updates.iter_mut() { + if let HTLCUpdateAwaitingACK::AddHTLC { + ref mut dummy_hops_skimmed_fee_msat, .. + } = htlc + { + *dummy_hops_skimmed_fee_msat = iter.next().ok_or(DecodeError::InvalidValue)?; + } + } + if iter.next().is_some() { + return Err(DecodeError::InvalidValue); + } + } if let Some(skimmed_fees) = holding_cell_skimmed_fees_opt { let mut iter = skimmed_fees.into_iter(); for htlc in holding_cell_htlc_updates.iter_mut() { @@ -15953,11 +16001,12 @@ mod tests { path: Path { hops: Vec::new(), blinded_tail: None }, session_priv: SecretKey::from_slice(&>::from_hex("0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap()[..]).unwrap(), first_hop_htlc_msat: 548, - payment_id: PaymentId([42; 32]), - bolt12_invoice: None, - }, - skimmed_fee_msat: None, - blinding_point: None, + payment_id: PaymentId([42; 32]), + bolt12_invoice: None, + }, + dummy_hops_skimmed_fee_msat: None, + skimmed_fee_msat: None, + blinding_point: None, send_timestamp: None, hold_htlc: None, accountable: false, @@ -16414,6 +16463,7 @@ mod tests { cltv_expiry: 0, state: OutboundHTLCState::Committed, source: dummy_htlc_source.clone(), + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, send_timestamp: None, @@ -16425,6 +16475,9 @@ mod tests { if idx % 2 == 0 { htlc.blinding_point = Some(test_utils::pubkey(42 + idx as u8)); } + if idx % 4 == 0 { + htlc.dummy_hops_skimmed_fee_msat = Some(2); + } if idx % 3 == 0 { htlc.skimmed_fee_msat = Some(1); } @@ -16442,6 +16495,7 @@ mod tests { hop_data: [0; 20 * 65], hmac: [0; 32], }, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, @@ -16480,11 +16534,13 @@ mod tests { let mut dummy_add = dummy_holding_cell_add_htlc.clone(); if let HTLCUpdateAwaitingACK::AddHTLC { ref mut blinding_point, + ref mut dummy_hops_skimmed_fee_msat, ref mut skimmed_fee_msat, .. } = &mut dummy_add { *blinding_point = Some(test_utils::pubkey(42 + i)); + *dummy_hops_skimmed_fee_msat = Some(41); *skimmed_fee_msat = Some(42); } else { panic!() diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 5951c6cdbe6..726e4ebdd1e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -427,6 +427,14 @@ pub struct PendingHTLCInfo { /// This is used to allow LSPs to take fees as a part of payments, without the sender having to /// shoulder them. pub skimmed_fee_msat: Option, + /// The fee skimmed by preceding [`DummyTlvs`] hops. + /// + /// Dummy hops are currently applied only to inbound payments. The skimmed fee + /// represents additional revenue for the receiver and is surfaced separately + /// in the corresponding [`Event::PaymentClaimable`]. + /// + /// [`DummyTlvs`]: crate::blinded_path::payment::DummyTlvs + pub dummy_hops_skimmed_fee_msat: Option, /// An experimental field indicating whether our node's reputation would be held accountable /// for the timely resolution of the received HTLC. pub incoming_accountable: bool, @@ -540,6 +548,10 @@ struct ClaimableHTLC { /// The total value received for a payment (sum of all MPP parts if the payment is a MPP). /// Gets set to the amount reported when pushing [`Event::PaymentClaimable`]. total_value_received: Option, + /// The amount (in msats) skimmed off by the dummy hops preceeding the HTLC. + /// This amount is the extra amount that the final receiver earns in addition + /// to the [`Self::value`]. And is set as such in [`Event::PaymentClaimable`]. + dummy_hops_skimmed_fee_msat: Option, /// The extra fee our counterparty skimmed off the top of this HTLC. counterparty_skimmed_fee_msat: Option, } @@ -1180,6 +1192,7 @@ pub(super) enum ChannelReadyOrder { #[derive(Clone, Debug, PartialEq, Eq)] struct ClaimingPayment { amount_msat: u64, + dummy_hops_skimmed_fee_msat: u64, payment_purpose: events::PaymentPurpose, receiver_node_id: PublicKey, htlcs: Vec, @@ -1206,6 +1219,7 @@ impl_writeable_tlv_based!(ClaimingPayment, { // onion_fields was added (and always set for new payments) in 0.0.124 (9, onion_fields, (required: ReadableArgs, amount_msat.0.unwrap())), (11, payment_id, option), + (13, dummy_hops_skimmed_fee_msat, (default_value, 0u64)), }); struct ClaimablePayment { @@ -1363,6 +1377,9 @@ impl ClaimablePayments { debug_assert!(durable_preimage_channel.is_some()); ClaimingPayment { amount_msat: payment.htlcs.iter().map(|source| source.value).sum(), + dummy_hops_skimmed_fee_msat: payment.htlcs.iter() + .map(|source| source.dummy_hops_skimmed_fee_msat.unwrap_or(0)) + .sum(), payment_purpose: payment.purpose, receiver_node_id, htlcs, @@ -5173,7 +5190,8 @@ impl< // delay) once they've send us a commitment_signed! let current_height: u32 = self.best_block.read().unwrap().height; create_recv_pending_htlc_info(decoded_hop, shared_secret, msg.payment_hash, - msg.amount_msat, msg.cltv_expiry, None, allow_underpay, msg.skimmed_fee_msat, + msg.amount_msat, msg.cltv_expiry, None, allow_underpay, + msg.dummy_hops_skimmed_fee_msat, msg.skimmed_fee_msat, msg.accountable.unwrap_or(false), current_height) }, onion_utils::Hop::Forward { .. } | onion_utils::Hop::BlindedForward { .. } => { @@ -5405,6 +5423,7 @@ impl< htlc_source, onion_packet, None, + None, hold_htlc_at_next_hop, false, // Not accountable by default for sender. &self.fee_estimator, @@ -7768,6 +7787,7 @@ impl< Some(phantom_shared_secret), false, None, + None, incoming_accountable, current_height, ); @@ -7883,6 +7903,7 @@ impl< outgoing_cltv_value, routing, skimmed_fee_msat, + dummy_hops_skimmed_fee_msat, incoming_accountable, .. }, @@ -7991,6 +8012,7 @@ impl< *outgoing_cltv_value, htlc_source.clone(), onion_packet.clone(), + *dummy_hops_skimmed_fee_msat, *skimmed_fee_msat, next_blinding_point, *incoming_accountable, @@ -8123,6 +8145,7 @@ impl< incoming_amt_msat, outgoing_amt_msat, skimmed_fee_msat, + dummy_hops_skimmed_fee_msat, .. }, .. @@ -8217,6 +8240,7 @@ impl< total_value_received: None, cltv_expiry, onion_payload, + dummy_hops_skimmed_fee_msat, counterparty_skimmed_fee_msat: skimmed_fee_msat, }; @@ -8322,6 +8346,8 @@ impl< claimable_payment.htlcs.iter().map(|htlc| htlc.value).sum(); claimable_payment.htlcs.iter_mut() .for_each(|htlc| htlc.total_value_received = Some(amount_msat)); + let dummy_hops_skimmed_fee_msat = claimable_payment.htlcs.iter() + .map(|htlc| htlc.dummy_hops_skimmed_fee_msat.unwrap_or(0)).sum(); let counterparty_skimmed_fee_msat = claimable_payment.htlcs.iter() .map(|htlc| htlc.counterparty_skimmed_fee_msat.unwrap_or(0)).sum(); debug_assert!(total_intended_recvd_value.saturating_sub(amount_msat) @@ -8334,6 +8360,7 @@ impl< payment_hash, purpose: $purpose, amount_msat, + dummy_hops_skimmed_fee_msat, counterparty_skimmed_fee_msat, receiving_channel_ids: claimable_payment.receiving_channel_ids(), claim_deadline: Some(earliest_expiry - HTLC_FAIL_BACK_BUFFER), @@ -10176,6 +10203,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ .remove(&payment_hash); if let Some(ClaimingPayment { amount_msat, + dummy_hops_skimmed_fee_msat, payment_purpose: purpose, receiver_node_id, htlcs, @@ -10189,6 +10217,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ payment_hash, purpose, amount_msat, + dummy_hops_skimmed_fee_msat, receiver_node_id: Some(receiver_node_id), htlcs, sender_intended_total_msat, @@ -17316,6 +17345,7 @@ impl_writeable_tlv_based!(PendingHTLCInfo, { (9, incoming_amt_msat, option), (10, skimmed_fee_msat, option), (11, incoming_accountable, (default_value, false)), + (12, dummy_hops_skimmed_fee_msat, option), }); impl Writeable for HTLCFailureMsg { @@ -17435,6 +17465,7 @@ fn write_claimable_htlc( (6, htlc.cltv_expiry, required), (8, keysend_preimage, option), (10, htlc.counterparty_skimmed_fee_msat, option), + (12, htlc.dummy_hops_skimmed_fee_msat, option), }); Ok(()) } @@ -17452,6 +17483,7 @@ impl Readable for (ClaimableHTLC, u64) { (6, cltv_expiry, required), (8, keysend_preimage, option), (10, counterparty_skimmed_fee_msat, option), + (12, dummy_hops_skimmed_fee_msat, option), }); let payment_data: Option = payment_data_opt; let value = value_ser.0.unwrap(); @@ -17473,6 +17505,7 @@ impl Readable for (ClaimableHTLC, u64) { onion_payload, cltv_expiry: cltv_expiry.0.unwrap(), counterparty_skimmed_fee_msat, + dummy_hops_skimmed_fee_msat, }, total_msat.0.expect("required field"))) } } @@ -20175,12 +20208,18 @@ impl< payment.inbound_payment_id(&inbound_payment_id_secret.unwrap()); let htlcs = payment.htlcs.iter().map(events::ClaimedHTLC::from).collect(); let sender_intended_total_msat = payment.onion_fields.total_mpp_amount_msat; + let dummy_hops_skimmed_fee_msat = payment + .htlcs + .iter() + .map(|htlc| htlc.dummy_hops_skimmed_fee_msat.unwrap_or(0)) + .sum(); pending_events.push_back(( events::Event::PaymentClaimed { receiver_node_id, payment_hash, purpose: payment.purpose, amount_msat: claimable_amt_msat, + dummy_hops_skimmed_fee_msat, htlcs, sender_intended_total_msat: Some(sender_intended_total_msat), onion_fields: Some(payment.onion_fields), @@ -21226,8 +21265,8 @@ mod tests { let current_height: u32 = node[0].node.best_block.read().unwrap().height; if let Err(crate::ln::channelmanager::InboundHTLCErr { reason, .. }) = create_recv_pending_htlc_info(hop_data, [0; 32], PaymentHash([0; 32]), - sender_intended_amt_msat - extra_fee_msat - 1, 42, None, true, Some(extra_fee_msat), - false, current_height) + sender_intended_amt_msat - extra_fee_msat - 1, 42, None, true, + None, Some(extra_fee_msat), false, current_height) { assert_eq!(reason, LocalHTLCFailureReason::FinalIncorrectHTLCAmount); } else { panic!(); } @@ -21249,7 +21288,7 @@ mod tests { }; let current_height: u32 = node[0].node.best_block.read().unwrap().height; assert!(create_recv_pending_htlc_info(hop_data, [0; 32], PaymentHash([0; 32]), - sender_intended_amt_msat - extra_fee_msat, 42, None, true, Some(extra_fee_msat), + sender_intended_amt_msat - extra_fee_msat, 42, None, true, None, Some(extra_fee_msat), false, current_height).is_ok()); } @@ -21275,7 +21314,7 @@ mod tests { custom_tlvs: Vec::new(), }, shared_secret: SharedSecret::from_bytes([0; 32]), - }, [0; 32], PaymentHash([0; 32]), 100, TEST_FINAL_CLTV + 1, None, true, None, false, current_height); + }, [0; 32], PaymentHash([0; 32]), 100, TEST_FINAL_CLTV + 1, None, true, None, None, false, current_height); // Should not return an error as this condition: // https://github.com/lightning/bolts/blob/4dcc377209509b13cf89a4b91fde7d478f5b46d8/04-onion-routing.md?plain=1#L334 diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 641842ddaff..51eb227a762 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -1312,6 +1312,24 @@ fn check_claimed_htlcs_match_route<'a, 'b, 'c>( } } +fn claimed_htlc_value_msats_for_paths<'a, 'b, 'c>( + origin_node: &Node<'a, 'b, 'c>, expected_paths: &[&[&Node<'a, 'b, 'c>]], htlcs: &[ClaimedHTLC], +) -> Vec { + let mut remaining_htlcs: Vec<&ClaimedHTLC> = htlcs.iter().collect(); + + expected_paths + .iter() + .map(|path| { + let idx = remaining_htlcs + .iter() + .position(|htlc| claimed_htlc_matches_path(origin_node, path, htlc)) + .expect("each path must have a unique matching claimed HTLC"); + + remaining_htlcs.remove(idx).value_msat + }) + .collect() +} + pub fn _reload_node<'a, 'b, 'c>( node: &'a Node<'a, 'b, 'c>, config: UserConfig, chanman_encoded: &[u8], monitors_encoded: &[&[u8]], _reconstruct_manager_from_monitors: Option, @@ -3673,6 +3691,7 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option ref payment_hash, ref purpose, amount_msat, + dummy_hops_skimmed_fee_msat, receiver_node_id, ref receiving_channel_ids, claim_deadline, @@ -3687,6 +3706,16 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option onion_fields.as_ref().unwrap().payment_metadata, payment_metadata ); + // Freshly generated `PaymentClaimable` events include one + // `receiving_channel_ids` entry per inbound HTLC, without + // deduplicating by channel, so `len() == 1` implies a + // single-part payment here. + if receiving_channel_ids.len() == 1 { + assert_eq!( + dummy_hops_total_fee_msat(recv_value, &dummy_tlvs), + *dummy_hops_skimmed_fee_msat + ); + } match &purpose { PaymentPurpose::Bolt11InvoicePayment { payment_preimage, @@ -3881,6 +3910,7 @@ pub fn do_claim_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { 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 dummy_tlvs: Vec>, pub expected_extra_fees: Vec, /// A one-off adjustment used only in tests to account for an existing /// fee-handling trade-off in LDK. @@ -3927,6 +3957,7 @@ impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> { Self { origin_node, expected_paths, + dummy_tlvs: vec![Vec::new(); expected_paths.len()], expected_extra_fees: vec![0; expected_paths.len()], expected_extra_total_fees_msat: 0, expected_min_htlc_overpay: vec![0; expected_paths.len()], @@ -3960,6 +3991,39 @@ impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> { self.custom_tlvs = custom_tlvs; self } + pub fn with_dummy_tlvs(mut self, dummy_tlvs: &[DummyTlvs]) -> Self { + self.dummy_tlvs = vec![dummy_tlvs.to_vec(); self.expected_paths.len()]; + self + } + pub fn with_per_path_dummy_tlvs(mut self, dummy_tlvs: &[Vec]) -> Self { + assert_eq!(dummy_tlvs.len(), self.expected_paths.len()); + self.dummy_tlvs = dummy_tlvs.to_vec(); + self + } +} + +/// Computes the total fees skimmed by all dummy hops for a single received HTLC. +/// +/// The provided `final_amount_msat` is the amount that reaches the recipient after all dummy hops +/// have been traversed. Because dummy hops each charge fees on the amount forwarded through them, +/// their fees must be accumulated in reverse order, with each hop's fee increasing the amount that +/// the previous dummy hop forwarded. +fn dummy_hops_total_fee_msat(final_amount_msat: u64, dummy_tlvs: &[DummyTlvs]) -> u64 { + let mut amount_msat = final_amount_msat; + let mut total_fee_msat = 0; + + // The last dummy hop forwards directly to the receiver, so work backwards from the final + // amount that reaches the recipient and accumulate the fees each earlier dummy hop must cover. + for tlvs in dummy_tlvs.iter().rev() { + let base_fee_msat = tlvs.payment_relay.fee_base_msat as u64; + let proportional_fee_millionths = tlvs.payment_relay.fee_proportional_millionths as u64; + let fee_msat = (amount_msat * proportional_fee_millionths / 1_000_000) + base_fee_msat; + + total_fee_msat += fee_msat; + amount_msat += fee_msat; + } + + total_fee_msat } macro_rules! single_fulfill_commit_from_ev { @@ -3994,9 +4058,7 @@ macro_rules! single_fulfill_commit_from_ev { pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { let claim_event = args.expected_paths[0].last().unwrap().node.get_and_clear_pending_events(); assert_eq!(claim_event.len(), 1, "{claim_event:?}"); - #[allow(unused)] - let mut fwd_amt_msat = 0; - match claim_event[0] { + let per_path_claim_amt_msats = match claim_event[0] { Event::PaymentClaimed { purpose: PaymentPurpose::SpontaneousPayment(preimage) @@ -4004,16 +4066,26 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { | PaymentPurpose::Bolt12OfferPayment { payment_preimage: Some(preimage), .. } | PaymentPurpose::Bolt12RefundPayment { payment_preimage: Some(preimage), .. }, amount_msat, + dummy_hops_skimmed_fee_msat, ref htlcs, ref onion_fields, .. } => { - assert_eq!(preimage, args.payment_preimage); assert_eq!(htlcs.len(), args.expected_paths.len()); // One per path. + assert_eq!(args.dummy_tlvs.len(), args.expected_paths.len()); + let expected_dummy_hops_skimmed_fee_msat = htlcs + .iter() + .zip(args.dummy_tlvs.iter()) + .map(|(htlc, path_dummy_tlvs)| { + dummy_hops_total_fee_msat(htlc.value_msat, path_dummy_tlvs) + }) + .sum::(); + assert_eq!(preimage, args.payment_preimage); assert_eq!(htlcs.iter().map(|h| h.value_msat).sum::(), amount_msat); + assert_eq!(dummy_hops_skimmed_fee_msat, expected_dummy_hops_skimmed_fee_msat); assert_eq!(onion_fields.as_ref().unwrap().custom_tlvs, args.custom_tlvs); check_claimed_htlcs_match_route(args.origin_node, args.expected_paths, htlcs); - fwd_amt_msat = amount_msat; + claimed_htlc_value_msats_for_paths(args.origin_node, args.expected_paths, htlcs) }, Event::PaymentClaimed { purpose: @@ -4022,19 +4094,29 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { | PaymentPurpose::Bolt12RefundPayment { .. }, payment_hash, amount_msat, + dummy_hops_skimmed_fee_msat, ref htlcs, ref onion_fields, .. } => { - assert_eq!(&payment_hash.0, &Sha256::hash(&args.payment_preimage.0)[..]); assert_eq!(htlcs.len(), args.expected_paths.len()); // One per path. + assert_eq!(args.dummy_tlvs.len(), args.expected_paths.len()); + let expected_dummy_hops_skimmed_fee_msat = htlcs + .iter() + .zip(args.dummy_tlvs.iter()) + .map(|(htlc, path_dummy_tlvs)| { + dummy_hops_total_fee_msat(htlc.value_msat, path_dummy_tlvs) + }) + .sum::(); + assert_eq!(&payment_hash.0, &Sha256::hash(&args.payment_preimage.0)[..]); assert_eq!(htlcs.iter().map(|h| h.value_msat).sum::(), amount_msat); + assert_eq!(dummy_hops_skimmed_fee_msat, expected_dummy_hops_skimmed_fee_msat); assert_eq!(onion_fields.as_ref().unwrap().custom_tlvs, args.custom_tlvs); check_claimed_htlcs_match_route(args.origin_node, args.expected_paths, htlcs); - fwd_amt_msat = amount_msat; + claimed_htlc_value_msats_for_paths(args.origin_node, args.expected_paths, htlcs) }, _ => panic!(), - } + }; check_added_monitors(args.expected_paths[0].last().unwrap(), args.expected_paths.len()); @@ -4063,17 +4145,35 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { } } - pass_claimed_payment_along_route_from_ev(fwd_amt_msat, per_path_msgs, args) + pass_claimed_payment_along_route_from_ev_with_path_amounts( + per_path_claim_amt_msats, + per_path_msgs, + args, + ) } pub fn pass_claimed_payment_along_route_from_ev( each_htlc_claim_amt_msat: u64, + per_path_msgs: Vec<((msgs::UpdateFulfillHTLC, Vec), PublicKey)>, + args: ClaimAlongRouteArgs, +) -> u64 { + let per_path_claim_amt_msats = vec![each_htlc_claim_amt_msat; args.expected_paths.len()]; + pass_claimed_payment_along_route_from_ev_with_path_amounts( + per_path_claim_amt_msats, + per_path_msgs, + args, + ) +} + +fn pass_claimed_payment_along_route_from_ev_with_path_amounts( + per_path_claim_amt_msats: Vec, mut per_path_msgs: Vec<((msgs::UpdateFulfillHTLC, Vec), PublicKey)>, args: ClaimAlongRouteArgs, ) -> u64 { let ClaimAlongRouteArgs { origin_node, expected_paths, + dummy_tlvs, expected_extra_fees, expected_min_htlc_overpay, skip_last, @@ -4081,13 +4181,23 @@ pub fn pass_claimed_payment_along_route_from_ev( allow_1_msat_fee_overpay, .. } = args; + assert_eq!(dummy_tlvs.len(), expected_paths.len()); - let mut fwd_amt_msat = each_htlc_claim_amt_msat; let mut expected_total_fee_msat = 0; - for (i, (expected_route, (path_msgs, next_hop))) in - expected_paths.iter().zip(per_path_msgs.drain(..)).enumerate() + for (i, (((expected_route, path_claim_amt_msat), path_dummy_tlvs), (path_msgs, next_hop))) in + expected_paths + .iter() + .zip(per_path_claim_amt_msats.into_iter()) + .zip(dummy_tlvs.into_iter()) + .zip(per_path_msgs.drain(..)) + .enumerate() { + let mut fwd_amt_msat = path_claim_amt_msat; + let dummy_hops_fee_msat = dummy_hops_total_fee_msat(fwd_amt_msat, &path_dummy_tlvs); + expected_total_fee_msat += dummy_hops_fee_msat; + fwd_amt_msat += dummy_hops_fee_msat; + let mut next_msgs = Some(path_msgs); let mut expected_next_node = next_hop; diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 17fbc1fce28..2c39915cef4 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -2307,6 +2307,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { payment_hash, cltv_expiry, onion_routing_packet, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index d88b9a2dc3f..14bb8bf4b7f 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -841,6 +841,7 @@ pub fn do_test_fee_spike_buffer(cfg: Option, htlc_fails: bool) { payment_hash, cltv_expiry: htlc_cltv, onion_routing_packet: onion_packet, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, @@ -1088,6 +1089,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { payment_hash, cltv_expiry: htlc_cltv, onion_routing_packet: onion_packet, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, @@ -1276,6 +1278,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { payment_hash: our_payment_hash_1, cltv_expiry: htlc_cltv, onion_routing_packet: onion_packet, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, @@ -1663,6 +1666,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { payment_hash: our_payment_hash, cltv_expiry: htlc_cltv, onion_routing_packet: onion_packet.clone(), + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, @@ -2265,6 +2269,7 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { payment_hash: payment_hash_0_1, cltv_expiry, onion_routing_packet, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index ac549ddd50c..f6241c7c9b3 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -758,6 +758,8 @@ pub struct UpdateAddHTLC { /// /// [`ChannelConfig::accept_underpaying_htlcs`]: crate::util::config::ChannelConfig::accept_underpaying_htlcs pub skimmed_fee_msat: Option, + /// The extra fees skimmed by the Dummy Hop + pub dummy_hops_skimmed_fee_msat: Option, /// The onion routing packet with encrypted data for the next hop. pub onion_routing_packet: OnionPacket, /// Provided if we are relaying or receiving a payment within a blinded path, to decrypt the onion @@ -3633,6 +3635,7 @@ impl_writeable_msg!(UpdateAddHTLC, { }, { (0, blinding_point, option), (65537, skimmed_fee_msat, option), + (65539, dummy_hops_skimmed_fee_msat, option), // TODO: currently we may fail to read the `ChannelManager` if we write a new even TLV in this message // and then downgrade. Once this is fixed, update the type here to match BOLTs PR 989. (75537, hold_htlc, option), @@ -6151,6 +6154,7 @@ mod tests { payment_hash: PaymentHash([1; 32]), cltv_expiry: 821716, onion_routing_packet, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, @@ -7050,6 +7054,7 @@ mod tests { amount_msat: 1000, payment_hash: PaymentHash([1; 32]), cltv_expiry: 500000, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, onion_routing_packet: msgs::OnionPacket { version: 0, diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index de08af5d276..c4e128eae49 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -244,7 +244,8 @@ fn claim_bolt12_payment_with_extra_fees<'a, 'b, 'c>( node, &expected_paths, payment_preimage, - ); + ) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); if let Some(extra) = expected_extra_fees_msat { args = args.with_expected_extra_total_fees_msat(extra); @@ -2454,7 +2455,11 @@ fn rejects_keysend_to_non_static_invoice_path() { _ => panic!() }; - claim_payment(&nodes[0], &[&nodes[1]], payment_preimage); + let path: &[&Node<'_, '_, '_>] = &[&nodes[1]]; + let dummy_tlvs = [DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]; + claim_payment_along_route( + ClaimAlongRouteArgs::new(&nodes[0], &[path], payment_preimage).with_dummy_tlvs(&dummy_tlvs), + ); expect_recent_payment!(&nodes[0], RecentPaymentDetails::Fulfilled, payment_id); // Time out the payment from recent payments so we can attempt to pay it again via keysend. @@ -2481,7 +2486,7 @@ fn rejects_keysend_to_non_static_invoice_path() { let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) .with_payment_preimage(payment_preimage) .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + .with_dummy_tlvs(&dummy_tlvs); do_pass_along_path(args); let mut updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); nodes[0].node.handle_update_fail_malformed_htlc(nodes[1].node.get_our_node_id(), &updates.update_fail_malformed_htlcs[0]); diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 5111f6982fe..c7ff7d1557e 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -272,6 +272,7 @@ pub(super) fn create_fwd_pending_htlc_info( incoming_amt_msat: Some(msg.amount_msat), outgoing_amt_msat: amt_to_forward, outgoing_cltv_value, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, incoming_accountable: msg.accountable.unwrap_or(false), }) @@ -281,7 +282,8 @@ pub(super) fn create_fwd_pending_htlc_info( pub(super) fn create_recv_pending_htlc_info( hop_data: onion_utils::Hop, shared_secret: [u8; 32], payment_hash: PaymentHash, amt_msat: u64, cltv_expiry: u32, phantom_shared_secret: Option<[u8; 32]>, allow_underpay: bool, - counterparty_skimmed_fee_msat: Option, incoming_accountable: bool, current_height: u32 + dummy_hops_skimmed_fee_msat: Option, counterparty_skimmed_fee_msat: Option, + incoming_accountable: bool, current_height: u32 ) -> Result { let ( payment_data, keysend_preimage, custom_tlvs, onion_amt_msat, onion_cltv_expiry, @@ -470,6 +472,7 @@ pub(super) fn create_recv_pending_htlc_info( incoming_amt_msat: Some(amt_msat), outgoing_amt_msat: onion_amt_msat, outgoing_cltv_value: onion_cltv_expiry, + dummy_hops_skimmed_fee_msat, skimmed_fee_msat: counterparty_skimmed_fee_msat, incoming_accountable, }) @@ -555,8 +558,8 @@ pub fn peel_payment_onion let shared_secret = hop.shared_secret().secret_bytes(); create_recv_pending_htlc_info( hop, shared_secret, msg.payment_hash, msg.amount_msat, msg.cltv_expiry, - None, allow_skimmed_fees, msg.skimmed_fee_msat, - msg.accountable.unwrap_or(false), cur_height, + None, allow_skimmed_fees, msg.dummy_hops_skimmed_fee_msat, + msg.skimmed_fee_msat, msg.accountable.unwrap_or(false), cur_height, )? } }) @@ -673,7 +676,7 @@ pub(super) fn decode_incoming_update_add_htlc_onion (amt, cltv), Err(()) => { - return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded forward", + return encode_relay_error("Underflow calculating outbound amount or cltv value for dummy hop", LocalHTLCFailureReason::InvalidOnionBlinding, shared_secret.secret_bytes(), None, &[0; 32]); } }; @@ -862,6 +865,7 @@ mod tests { cltv_expiry, payment_hash, onion_routing_packet, + dummy_hops_skimmed_fee_msat: None, skimmed_fee_msat: None, blinding_point: None, hold_htlc: None, diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 9b1b009e93a..759c8f8521e 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2574,6 +2574,10 @@ pub(super) fn peel_dummy_hop_update_add_htlc return None, }; - let cltv_expiry_delta = payment_relay.cltv_expiry_delta as u32; + let cltv_expiry_delta = payment_relay.cltv_expiry_delta as u32 + + dummy_tlvs.payment_relay.cltv_expiry_delta as u32 * DEFAULT_PAYMENT_DUMMY_HOPS as u32; + let htlc_minimum_msat = cmp::max( + details.inbound_htlc_minimum_msat.unwrap_or(0), + dummy_tlvs.payment_constraints.htlc_minimum_msat, + ); let payment_constraints = PaymentConstraints { max_cltv_expiry: tlvs.payment_constraints .max_cltv_expiry .saturating_add(cltv_expiry_delta), - htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0), + htlc_minimum_msat, }; Some(PaymentForwardNode { tlvs: ForwardTlvs { @@ -197,7 +203,7 @@ where }) .map(|forward_node| { BlindedPaymentPath::new_with_dummy_hops( - &[forward_node], recipient, &[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS], + &[forward_node], recipient, &[dummy_tlvs; DEFAULT_PAYMENT_DUMMY_HOPS], local_node_receive_key, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &self.entropy_source, secp_ctx ) }) @@ -209,7 +215,7 @@ where _ => { if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) { BlindedPaymentPath::new_with_dummy_hops( - &[], recipient, &[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS], + &[], recipient, &[dummy_tlvs; DEFAULT_PAYMENT_DUMMY_HOPS], local_node_receive_key, tlvs, u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &self.entropy_source, secp_ctx ).map(|path| vec![path]) } else {