From 4741892c9c93c1fadc086c6b0e12079d416466b6 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Wed, 26 Mar 2025 13:25:49 -0700 Subject: [PATCH 1/9] Remove Trampoline config flag In this PR, we add end-to-end tests for Trampoline receive and forwarding behavior (specifically rejection of payments for the latter), ensuring sanity, no unexpected errors, and obviating the cfg-gating. --- Cargo.toml | 1 - ci/ci-tests.sh | 2 - lightning/src/blinded_path/payment.rs | 3 -- lightning/src/ln/blinded_payment_tests.rs | 1 - lightning/src/ln/channelmanager.rs | 54 +---------------------- lightning/src/ln/msgs.rs | 33 +------------- lightning/src/ln/onion_payment.rs | 10 ----- lightning/src/ln/onion_utils.rs | 17 +++---- 8 files changed, 7 insertions(+), 114 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8c088524416..41a91996f7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,6 @@ check-cfg = [ "cfg(ldk_bench)", "cfg(ldk_test_vectors)", "cfg(taproot)", - "cfg(trampoline)", "cfg(require_route_graph_test)", "cfg(splicing)", "cfg(async_payments)", diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index 9bc23d042ec..7eacd9c4744 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -134,8 +134,6 @@ RUSTFLAGS="--cfg=taproot" cargo test --verbose --color always -p lightning [ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean RUSTFLAGS="--cfg=splicing" cargo test --verbose --color always -p lightning [ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean -RUSTFLAGS="--cfg=trampoline" cargo test --verbose --color always -p lightning -[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean RUSTFLAGS="--cfg=async_payments" cargo test --verbose --color always -p lightning [ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean RUSTFLAGS="--cfg=lsps1_service" cargo test --verbose --color always -p lightning-liquidity diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index e4678a2322e..963c7de1f0a 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -298,7 +298,6 @@ pub struct ForwardTlvs { } /// Data to construct a [`BlindedHop`] for forwarding a Trampoline payment. -#[cfg(trampoline)] #[derive(Clone, Debug)] pub struct TrampolineForwardTlvs { /// The node id to which the trampoline node must find a route. @@ -371,7 +370,6 @@ pub(crate) enum BlindedPaymentTlvs { /// Data to construct a [`BlindedHop`] for sending a Trampoline payment over. /// /// [`BlindedHop`]: crate::blinded_path::BlindedHop -#[cfg(trampoline)] pub(crate) enum BlindedTrampolineTlvs { /// This blinded payment data is for a forwarding node. Forward(TrampolineForwardTlvs), @@ -591,7 +589,6 @@ impl Readable for BlindedPaymentTlvs { } } -#[cfg(trampoline)] impl Readable for BlindedTrampolineTlvs { fn read(r: &mut R) -> Result { _init_and_read_tlv_stream!(r, { diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 89b83ede3eb..8808d7bb03a 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1816,7 +1816,6 @@ fn test_combined_trampoline_onion_creation_vectors() { } #[test] -#[cfg(trampoline)] fn test_trampoline_inbound_payment_decoding() { let secp_ctx = Secp256k1::new(); let session_priv = secret_from_hex("0303030303030303030303030303030303030303030303030303030303030303"); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fab15bfea28..26be25198b4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -170,7 +170,6 @@ pub enum PendingHTLCRouting { incoming_cltv_expiry: Option, }, /// An HTLC which should be forwarded on to another Trampoline node. - #[cfg(trampoline)] TrampolineForward { /// The onion shared secret we build with the sender (or the preceding Trampoline node) used /// to decrypt the onion. @@ -288,7 +287,6 @@ impl PendingHTLCRouting { fn blinded_failure(&self) -> Option { match self { Self::Forward { blinded: Some(BlindedForward { failure, .. }), .. } => Some(*failure), - #[cfg(trampoline)] Self::TrampolineForward { blinded: Some(BlindedForward { failure, .. }), .. } => Some(*failure), Self::Receive { requires_blinded_error: true, .. } => Some(BlindedFailure::FromBlindedNode), Self::ReceiveKeysend { requires_blinded_error: true, .. } => Some(BlindedFailure::FromBlindedNode), @@ -299,7 +297,6 @@ impl PendingHTLCRouting { fn incoming_cltv_expiry(&self) -> Option { match self { Self::Forward { incoming_cltv_expiry, .. } => *incoming_cltv_expiry, - #[cfg(trampoline)] Self::TrampolineForward { incoming_cltv_expiry, .. } => Some(*incoming_cltv_expiry), Self::Receive { incoming_cltv_expiry, .. } => Some(*incoming_cltv_expiry), Self::ReceiveKeysend { incoming_cltv_expiry, .. } => Some(*incoming_cltv_expiry), @@ -4510,24 +4507,7 @@ where } } match decoded_hop { - onion_utils::Hop::Receive { .. } | onion_utils::Hop::BlindedReceive { .. } => { - // OUR PAYMENT! - let current_height: u32 = self.best_block.read().unwrap().height; - match 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, - current_height) - { - Ok(info) => { - // Note that we could obviously respond immediately with an update_fulfill_htlc - // message, however that would leak that we are the recipient of this payment, so - // instead we stay symmetric with the forwarding case, only responding (after a - // delay) once they've sent us a commitment_signed! - PendingHTLCStatus::Forward(info) - }, - Err(InboundHTLCErr { err_code, err_data, msg }) => return_err!(msg, err_code, &err_data) - } - }, - #[cfg(trampoline)] + onion_utils::Hop::Receive { .. } | onion_utils::Hop::BlindedReceive { .. } | onion_utils::Hop::TrampolineReceive { .. } | onion_utils::Hop::TrampolineBlindedReceive { .. } => { // OUR PAYMENT! let current_height: u32 = self.best_block.read().unwrap().height; @@ -4551,7 +4531,6 @@ where Err(InboundHTLCErr { err_code, err_data, msg }) => return_err!(msg, err_code, &err_data) } }, - #[cfg(trampoline)] onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => { match create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt) { Ok(info) => PendingHTLCStatus::Forward(info), @@ -9067,7 +9046,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ for (forward_info, prev_htlc_id) in pending_forwards.drain(..) { let scid = match forward_info.routing { PendingHTLCRouting::Forward { short_channel_id, .. } => short_channel_id, - #[cfg(trampoline)] PendingHTLCRouting::TrampolineForward { .. } => 0, PendingHTLCRouting::Receive { .. } => 0, PendingHTLCRouting::ReceiveKeysend { .. } => 0, @@ -12888,36 +12866,6 @@ impl_writeable_tlv_based!(BlindedForward, { (3, next_blinding_override, option), }); -#[cfg(not(trampoline))] -impl_writeable_tlv_based_enum!(PendingHTLCRouting, - (0, Forward) => { - (0, onion_packet, required), - (1, blinded, option), - (2, short_channel_id, required), - (3, incoming_cltv_expiry, option), - }, - (1, Receive) => { - (0, payment_data, required), - (1, phantom_shared_secret, option), - (2, incoming_cltv_expiry, required), - (3, payment_metadata, option), - (5, custom_tlvs, optional_vec), - (7, requires_blinded_error, (default_value, false)), - (9, payment_context, option), - }, - (2, ReceiveKeysend) => { - (0, payment_preimage, required), - (1, requires_blinded_error, (default_value, false)), - (2, incoming_cltv_expiry, required), - (3, payment_metadata, option), - (4, payment_data, option), // Added in 0.0.116 - (5, custom_tlvs, optional_vec), - (7, has_recipient_created_payment_secret, (default_value, false)), - (9, payment_context, option), - (11, invoice_request, option), - }, -); -#[cfg(trampoline)] impl_writeable_tlv_based_enum!(PendingHTLCRouting, (0, Forward) => { (0, onion_packet, required), diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 1887cb18a88..6d643d53209 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -32,7 +32,6 @@ use bitcoin::script::ScriptBuf; use bitcoin::hash_types::Txid; use crate::blinded_path::payment::{BlindedPaymentTlvs, ForwardTlvs, ReceiveTlvs, UnauthenticatedReceiveTlvs}; -#[cfg(trampoline)] use crate::blinded_path::payment::{BlindedTrampolineTlvs, TrampolineForwardTlvs}; use crate::ln::channelmanager::Verification; use crate::ln::types::ChannelId; @@ -2075,8 +2074,7 @@ mod fuzzy_internal_msgs { pub outgoing_cltv_value: u32, } - #[cfg(trampoline)] - #[cfg_attr(trampoline, allow(unused))] + #[allow(unused)] pub struct InboundTrampolineEntrypointPayload { pub amt_to_forward: u64, pub outgoing_cltv_value: u32, @@ -2118,15 +2116,12 @@ mod fuzzy_internal_msgs { pub enum InboundOnionPayload { Forward(InboundOnionForwardPayload), - #[cfg(trampoline)] - #[cfg_attr(trampoline, allow(unused))] TrampolineEntrypoint(InboundTrampolineEntrypointPayload), Receive(InboundOnionReceivePayload), BlindedForward(InboundOnionBlindedForwardPayload), BlindedReceive(InboundOnionBlindedReceivePayload), } - #[cfg(trampoline)] pub struct InboundTrampolineForwardPayload { pub next_trampoline: PublicKey, /// The value, in msat, of the payment after this hop's fee is deducted. @@ -2134,7 +2129,6 @@ mod fuzzy_internal_msgs { pub outgoing_cltv_value: u32, } - #[cfg(trampoline)] pub struct InboundTrampolineBlindedForwardPayload { pub next_trampoline: PublicKey, pub payment_relay: PaymentRelay, @@ -2144,7 +2138,6 @@ mod fuzzy_internal_msgs { pub next_blinding_override: Option, } - #[cfg(trampoline)] pub enum InboundTrampolinePayload { Forward(InboundTrampolineForwardPayload), BlindedForward(InboundTrampolineBlindedForwardPayload), @@ -3239,7 +3232,6 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh let mut payment_metadata: Option>> = None; let mut total_msat = None; let mut keysend_preimage: Option = None; - #[cfg(trampoline)] let mut trampoline_onion_packet: Option = None; let mut invoice_request: Option = None; let mut custom_tlvs = Vec::new(); @@ -3247,7 +3239,6 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh let tlv_len = BigSize::read(r)?; let mut rd = FixedLengthReader::new(r, tlv_len.0); - #[cfg(trampoline)] decode_tlv_stream_with_custom_tlv_decode!(&mut rd, { (2, amt, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), (4, cltv_value, (option, encoding: (u32, HighZeroBytesDroppedBigSize))), @@ -3268,33 +3259,12 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh custom_tlvs.push((msg_type, value)); Ok(true) }); - #[cfg(not(trampoline))] - decode_tlv_stream_with_custom_tlv_decode!(&mut rd, { - (2, amt, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), - (4, cltv_value, (option, encoding: (u32, HighZeroBytesDroppedBigSize))), - (6, short_id, option), - (8, payment_data, option), - (10, encrypted_tlvs_opt, option), - (12, intro_node_blinding_point, option), - (16, payment_metadata, option), - (18, total_msat, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), - (77_777, invoice_request, option), - // See https://github.com/lightning/blips/blob/master/blip-0003.md - (5482373484, keysend_preimage, option) - }, |msg_type: u64, msg_reader: &mut FixedLengthReader<_>| -> Result { - if msg_type < 1 << 16 { return Ok(false) } - let mut value = Vec::new(); - msg_reader.read_to_limit(&mut value, u64::MAX)?; - custom_tlvs.push((msg_type, value)); - Ok(true) - }); if amt.unwrap_or(0) > MAX_VALUE_MSAT { return Err(DecodeError::InvalidValue) } if intro_node_blinding_point.is_some() && update_add_blinding_point.is_some() { return Err(DecodeError::InvalidValue) } - #[cfg(trampoline)] if let Some(trampoline_onion_packet) = trampoline_onion_packet { if payment_metadata.is_some() || encrypted_tlvs_opt.is_some() || total_msat.is_some() @@ -3391,7 +3361,6 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh } } -#[cfg(trampoline)] impl ReadableArgs<(Option, NS)> for InboundTrampolinePayload where NS::Target: NodeSigner { fn read(r: &mut R, args: (Option, NS)) -> Result { let (update_add_blinding_point, node_signer) = args; diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 46661df6807..4c690caa04e 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -6,8 +6,6 @@ use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; - -#[cfg(trampoline)] use bitcoin::secp256k1::ecdh::SharedSecret; use crate::blinded_path; @@ -69,7 +67,6 @@ enum RoutingInfo { new_packet_bytes: [u8; ONION_DATA_LEN], next_hop_hmac: [u8; 32] }, - #[cfg(trampoline)] Trampoline { next_trampoline: PublicKey, // Trampoline onions are currently variable length @@ -118,14 +115,12 @@ pub(super) fn create_fwd_pending_htlc_info( err_code: 0x4000 | 22, err_data: Vec::new(), }), - #[cfg(trampoline)] onion_utils::Hop::TrampolineReceive { .. } | onion_utils::Hop::TrampolineBlindedReceive { .. } => return Err(InboundHTLCErr { msg: "Final Node OnionHopData provided for us as an intermediary node", err_code: 0x4000 | 22, err_data: Vec::new(), }), - #[cfg(trampoline)] onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { ( RoutingInfo::Trampoline { @@ -141,7 +136,6 @@ pub(super) fn create_fwd_pending_htlc_info( None ) }, - #[cfg(trampoline)] onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward( msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features @@ -192,7 +186,6 @@ pub(super) fn create_fwd_pending_htlc_info( }), } } - #[cfg(trampoline)] RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key } => { let next_trampoline_packet_pubkey = match next_packet_pubkey_opt { Some(Ok(pubkey)) => pubkey, @@ -272,7 +265,6 @@ pub(super) fn create_recv_pending_htlc_info( sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context), intro_node_blinding_point.is_none(), true, invoice_request) } - #[cfg(trampoline)] onion_utils::Hop::TrampolineReceive { .. } | onion_utils::Hop::TrampolineBlindedReceive { .. } => todo!(), onion_utils::Hop::Forward { .. } => { return Err(InboundHTLCErr { @@ -288,7 +280,6 @@ pub(super) fn create_recv_pending_htlc_info( msg: "Got blinded non final data with an HMAC of 0", }) }, - #[cfg(trampoline)] onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => { return Err(InboundHTLCErr { err_code: 0x4000|22, @@ -560,7 +551,6 @@ where outgoing_cltv_value }) } - #[cfg(trampoline)] onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 68d7e4d7d9d..b68edd081e8 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1564,7 +1564,6 @@ pub(crate) enum Hop { }, /// This onion was received via Trampoline, and needs to be forwarded to a subsequent Trampoline /// node. - #[cfg(trampoline)] TrampolineForward { #[allow(unused)] outer_hop_data: msgs::InboundTrampolineEntrypointPayload, @@ -1577,11 +1576,10 @@ pub(crate) enum Hop { }, /// This onion was received via Trampoline, and needs to be forwarded to a subsequent Trampoline /// node. - #[allow(unused)] - #[cfg(trampoline)] TrampolineBlindedForward { outer_hop_data: msgs::InboundTrampolineEntrypointPayload, outer_shared_secret: SharedSecret, + #[allow(unused)] incoming_trampoline_public_key: PublicKey, trampoline_shared_secret: SharedSecret, next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload, @@ -1617,22 +1615,22 @@ pub(crate) enum Hop { }, /// This onion payload was for us, not for forwarding to a next-hop, and it was sent to us via /// Trampoline. Contains information for verifying the incoming payment. - #[allow(unused)] - #[cfg(trampoline)] TrampolineReceive { + #[allow(unused)] outer_hop_data: msgs::InboundTrampolineEntrypointPayload, outer_shared_secret: SharedSecret, trampoline_hop_data: msgs::InboundOnionReceivePayload, + #[allow(unused)] trampoline_shared_secret: SharedSecret, }, /// This onion payload was for us, not for forwarding to a next-hop, and it was sent to us via /// Trampoline. Contains information for verifying the incoming payment. - #[allow(unused)] - #[cfg(trampoline)] TrampolineBlindedReceive { + #[allow(unused)] outer_hop_data: msgs::InboundTrampolineEntrypointPayload, outer_shared_secret: SharedSecret, trampoline_hop_data: msgs::InboundOnionBlindedReceivePayload, + #[allow(unused)] trampoline_shared_secret: SharedSecret, }, } @@ -1655,15 +1653,11 @@ impl Hop { match self { Hop::Forward { shared_secret, .. } => shared_secret, Hop::BlindedForward { shared_secret, .. } => shared_secret, - #[cfg(trampoline)] Hop::TrampolineForward { outer_shared_secret, .. } => outer_shared_secret, - #[cfg(trampoline)] Hop::TrampolineBlindedForward { outer_shared_secret, .. } => outer_shared_secret, Hop::Receive { shared_secret, .. } => shared_secret, Hop::BlindedReceive { shared_secret, .. } => shared_secret, - #[cfg(trampoline)] Hop::TrampolineReceive { outer_shared_secret, .. } => outer_shared_secret, - #[cfg(trampoline)] Hop::TrampolineBlindedReceive { outer_shared_secret, .. } => outer_shared_secret, } } @@ -1750,7 +1744,6 @@ where msgs::InboundOnionPayload::BlindedReceive(hop_data) => { Ok(Hop::BlindedReceive { shared_secret, hop_data }) }, - #[cfg(trampoline)] msgs::InboundOnionPayload::TrampolineEntrypoint(hop_data) => { let incoming_trampoline_public_key = hop_data.trampoline_packet.public_key; let trampoline_blinded_node_id_tweak = hop_data.current_path_key.map(|bp| { From 0c82cc29633a351ce734cdc2a428562abbb52a71 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Mon, 17 Mar 2025 01:36:52 -0700 Subject: [PATCH 2/9] Make Trampoline inbound MPP data optional Per the resolution of the spec discussion regarding the requirement of this field outside MPP scenarios, we are now marking this field as optional. --- lightning/src/ln/msgs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 6d643d53209..06e09c7c917 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -2078,7 +2078,7 @@ mod fuzzy_internal_msgs { pub struct InboundTrampolineEntrypointPayload { pub amt_to_forward: u64, pub outgoing_cltv_value: u32, - pub multipath_trampoline_data: FinalOnionHopData, + pub multipath_trampoline_data: Option, pub trampoline_packet: TrampolineOnionPacket, /// The blinding point this hop needs to decrypt its Trampoline onion. /// This is used for Trampoline hops that are not the blinded path intro hop. @@ -3272,7 +3272,7 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh return Ok(Self::TrampolineEntrypoint(InboundTrampolineEntrypointPayload { amt_to_forward: amt.ok_or(DecodeError::InvalidValue)?, outgoing_cltv_value: cltv_value.ok_or(DecodeError::InvalidValue)?, - multipath_trampoline_data: payment_data.ok_or(DecodeError::InvalidValue)?, + multipath_trampoline_data: payment_data, trampoline_packet: trampoline_onion_packet, current_path_key: intro_node_blinding_point, })) From a293d214870e8215054c586ecae4a37c5915e59e Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Mon, 17 Mar 2025 04:13:24 -0700 Subject: [PATCH 3/9] Fix blinded Trampoline TLV IDs Trivial fix where the Trampoline hop TLV inside blinded contexts differs from un-blinded ones. --- lightning/src/blinded_path/payment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 963c7de1f0a..403a9aac15c 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -592,10 +592,10 @@ impl Readable for BlindedPaymentTlvs { impl Readable for BlindedTrampolineTlvs { fn read(r: &mut R) -> Result { _init_and_read_tlv_stream!(r, { + (4, next_trampoline, option), (8, next_blinding_override, option), (10, payment_relay, option), (12, payment_constraints, required), - (14, next_trampoline, option), (14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))), (65536, payment_secret, option), (65537, payment_context, option), From 313f3440e48795725f44310879b545849cea7840 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Wed, 26 Mar 2025 11:19:25 -0700 Subject: [PATCH 4/9] De-macro `return_malformed_err` It does not need to be a macro, so we convert it into a simple lambda. --- lightning/src/ln/onion_payment.rs | 40 ++++++++++++++----------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 4c690caa04e..44a5c4c2461 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -464,27 +464,23 @@ where NS::Target: NodeSigner, L::Target: Logger, { - macro_rules! return_malformed_err { - ($msg: expr, $err_code: expr) => { - { - log_info!(logger, "Failed to accept/forward incoming HTLC: {}", $msg); - let (sha256_of_onion, failure_code) = if msg.blinding_point.is_some() { - ([0; 32], INVALID_ONION_BLINDING) - } else { - (Sha256::hash(&msg.onion_routing_packet.hop_data).to_byte_array(), $err_code) - }; - return Err(HTLCFailureMsg::Malformed(msgs::UpdateFailMalformedHTLC { - channel_id: msg.channel_id, - htlc_id: msg.htlc_id, - sha256_of_onion, - failure_code, - })); - } - } - } + let encode_malformed_error = |message: &str, err_code: u16| { + log_info!(logger, "Failed to accept/forward incoming HTLC: {}", message); + let (sha256_of_onion, failure_code) = if msg.blinding_point.is_some() { + ([0; 32], INVALID_ONION_BLINDING) + } else { + (Sha256::hash(&msg.onion_routing_packet.hop_data).to_byte_array(), err_code) + }; + return Err(HTLCFailureMsg::Malformed(msgs::UpdateFailMalformedHTLC { + channel_id: msg.channel_id, + htlc_id: msg.htlc_id, + sha256_of_onion, + failure_code, + })); + }; if let Err(_) = msg.onion_routing_packet.public_key { - return_malformed_err!("invalid ephemeral pubkey", 0x8000 | 0x4000 | 6); + return encode_malformed_error("invalid ephemeral pubkey", 0x8000 | 0x4000 | 6); } if msg.onion_routing_packet.version != 0 { @@ -494,12 +490,12 @@ where //receiving node would have to brute force to figure out which version was put in the //packet by the node that send us the message, in the case of hashing the hop_data, the //node knows the HMAC matched, so they already know what is there... - return_malformed_err!("Unknown onion packet version", 0x8000 | 0x4000 | 4); + return encode_malformed_error("Unknown onion packet version", 0x8000 | 0x4000 | 4); } let encode_relay_error = |message: &str, err_code: u16, shared_secret: [u8; 32], trampoline_shared_secret: Option<[u8; 32]>, data: &[u8]| { if msg.blinding_point.is_some() { - return_malformed_err!(message, INVALID_ONION_BLINDING) + return encode_malformed_error(message, INVALID_ONION_BLINDING) } log_info!(logger, "Failed to accept/forward incoming HTLC: {}", message); @@ -518,7 +514,7 @@ where ) { Ok(res) => res, Err(onion_utils::OnionDecodeErr::Malformed { err_msg, err_code }) => { - return_malformed_err!(err_msg, err_code); + return encode_malformed_error(err_msg, err_code); }, Err(onion_utils::OnionDecodeErr::Relay { err_msg, err_code, shared_secret, trampoline_shared_secret }) => { return encode_relay_error(err_msg, err_code, shared_secret.secret_bytes(), trampoline_shared_secret.map(|tss| tss.secret_bytes()), &[0; 0]); From 23b4c8029f0a0e801389546ce5c203086f72dae0 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Wed, 26 Mar 2025 11:19:57 -0700 Subject: [PATCH 5/9] Propagate Trampoline blinding errors When a blinded hop fails to decode, we send a special malformed error. However, we previously simply checked the presence of a blinding point within the `UpdateAddHTLC` message, which is not necessarily applicable to Trampoline, so now we additionally return a flag if the error stemmed from an inner onion's blinded hop decoding failure. --- lightning/src/ln/onion_payment.rs | 2 +- lightning/src/ln/onion_utils.rs | 64 ++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 44a5c4c2461..c17c7ad01b1 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -466,7 +466,7 @@ where { let encode_malformed_error = |message: &str, err_code: u16| { log_info!(logger, "Failed to accept/forward incoming HTLC: {}", message); - let (sha256_of_onion, failure_code) = if msg.blinding_point.is_some() { + let (sha256_of_onion, failure_code) = if msg.blinding_point.is_some() || err_code == INVALID_ONION_BLINDING { ([0; 32], INVALID_ONION_BLINDING) } else { (Sha256::hash(&msg.onion_routing_packet.hop_data).to_byte_array(), err_code) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index b68edd081e8..8d198e10b21 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1821,16 +1821,60 @@ where trampoline_shared_secret, ), }), - Ok((_, None)) => Err(OnionDecodeErr::Malformed { - err_msg: "Non-final Trampoline onion data provided to us as last hop", - // todo: find more suitable error code - err_code: 0x4000 | 22, - }), - Ok((_, Some(_))) => Err(OnionDecodeErr::Malformed { - err_msg: "Final Trampoline onion data provided to us as intermediate hop", - // todo: find more suitable error code - err_code: 0x4000 | 22, - }), + Ok((msgs::InboundTrampolinePayload::BlindedForward(hop_data), None)) => { + if hop_data.intro_node_blinding_point.is_some() { + return Err(OnionDecodeErr::Relay { + err_msg: "Non-final intro node Trampoline onion data provided to us as last hop", + err_code: INVALID_ONION_BLINDING, + shared_secret, + trampoline_shared_secret: Some(SharedSecret::from_bytes( + trampoline_shared_secret, + )), + }); + } + Err(OnionDecodeErr::Malformed { + err_msg: "Non-final Trampoline onion data provided to us as last hop", + err_code: INVALID_ONION_BLINDING, + }) + }, + Ok((msgs::InboundTrampolinePayload::BlindedReceive(hop_data), Some(_))) => { + if hop_data.intro_node_blinding_point.is_some() { + return Err(OnionDecodeErr::Relay { + err_msg: "Final Trampoline intro node onion data provided to us as intermediate hop", + err_code: 0x4000 | 22, + shared_secret, + trampoline_shared_secret: Some(SharedSecret::from_bytes( + trampoline_shared_secret, + )), + }); + } + Err(OnionDecodeErr::Malformed { + err_msg: + "Final Trampoline onion data provided to us as intermediate hop", + err_code: INVALID_ONION_BLINDING, + }) + }, + Ok((msgs::InboundTrampolinePayload::Forward(_), None)) => { + Err(OnionDecodeErr::Relay { + err_msg: "Non-final Trampoline onion data provided to us as last hop", + err_code: 0x4000 | 22, + shared_secret, + trampoline_shared_secret: Some(SharedSecret::from_bytes( + trampoline_shared_secret, + )), + }) + }, + Ok((msgs::InboundTrampolinePayload::Receive(_), Some(_))) => { + Err(OnionDecodeErr::Relay { + err_msg: + "Final Trampoline onion data provided to us as intermediate hop", + err_code: 0x4000 | 22, + shared_secret, + trampoline_shared_secret: Some(SharedSecret::from_bytes( + trampoline_shared_secret, + )), + }) + }, Err(e) => Err(e), } }, From dc14f26bfd10eaf9bc3e0bde040cc1beec012bce Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Mon, 17 Mar 2025 04:13:58 -0700 Subject: [PATCH 6/9] Implement `Writeable` for `TrampolineForwardTlvs` We will need the ability to serialize blinded Trampoline forwards to construct their encrypted TLV data from its unencrypted state using `construct_blinded_hops`, which initially we only do in unit tests. --- lightning/src/blinded_path/payment.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 403a9aac15c..b0639dd57e5 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -512,6 +512,23 @@ impl Writeable for ForwardTlvs { } } +impl Writeable for TrampolineForwardTlvs { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + let features_opt = if self.features == BlindedHopFeatures::empty() { + None + } else { + Some(WithoutLength(&self.features)) + }; + encode_tlv_stream!(w, { + (4, self.next_trampoline, required), + (10, self.payment_relay, required), + (12, self.payment_constraints, required), + (14, features_opt, option) + }); + Ok(()) + } +} + impl Writeable for ReceiveTlvs { fn write(&self, w: &mut W) -> Result<(), io::Error> { encode_tlv_stream!(w, { From 35ff1421e184029288234691c50d06872ecb6ee9 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Mon, 17 Mar 2025 01:43:47 -0700 Subject: [PATCH 7/9] Handle Trampoline receives Handle payment receives with payment details located inside an inner onion. --- lightning/src/ln/blinded_payment_tests.rs | 217 ++++++++++++++++++++++ lightning/src/ln/onion_payment.rs | 31 +++- 2 files changed, 247 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 8808d7bb03a..81535bcb9ee 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -40,6 +40,7 @@ use crate::util::test_utils; use lightning_invoice::RawBolt11Invoice; use types::features::Features; use crate::blinded_path::BlindedHop; +use crate::routing::router::Route; pub fn blinded_payment_path( payment_secret: PaymentSecret, intro_node_min_htlc: u64, intro_node_max_htlc: u64, @@ -1957,3 +1958,219 @@ fn test_trampoline_inbound_payment_decoding() { panic!(); }; } + +fn do_test_trampoline_single_hop_receive(success: bool) { + const TOTAL_NODE_COUNT: usize = 3; + let secp_ctx = Secp256k1::new(); + + let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); + let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]); + let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs); + + let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks + connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1); + } + + let alice_node_id = nodes[0].node().get_our_node_id(); + let bob_node_id = nodes[1].node().get_our_node_id(); + let carol_node_id = nodes[2].node().get_our_node_id(); + + let alice_bob_scid = nodes[0].node().list_channels().iter().find(|c| c.channel_id == chan_id_alice_bob).unwrap().short_channel_id.unwrap(); + let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap(); + + let amt_msat = 1000; + let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); + + let carol_alice_trampoline_session_priv = secret_from_hex("a0f4b8d7b6c2d0ffdfaf718f76e9decaef4d9fb38a8c4addb95c4007cc3eee03"); + let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &carol_alice_trampoline_session_priv); + let carol_blinded_hops = if success { + let payee_tlvs = UnauthenticatedReceiveTlvs { + payment_secret, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: amt_msat, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + }; + + let nonce = Nonce([42u8; 16]); + let expanded_key = nodes[2].keys_manager.get_inbound_payment_key(); + let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); + let carol_unblinded_tlvs = payee_tlvs.encode(); + + let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))]; + blinded_path::utils::construct_blinded_hops( + &secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv + ).unwrap() + } else { + let payee_tlvs = blinded_path::payment::TrampolineForwardTlvs { + next_trampoline: alice_node_id, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: amt_msat, + }, + features: BlindedHopFeatures::empty(), + payment_relay: PaymentRelay { + cltv_expiry_delta: 0, + fee_proportional_millionths: 0, + fee_base_msat: 0, + }, + next_blinding_override: None, + }; + + let carol_unblinded_tlvs = payee_tlvs.encode(); + let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))]; + blinded_path::utils::construct_blinded_hops( + &secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv + ).unwrap() + }; + + let route = Route { + paths: vec![Path { + hops: vec![ + // Bob + RouteHop { + pubkey: bob_node_id, + node_features: NodeFeatures::empty(), + short_channel_id: alice_bob_scid, + channel_features: ChannelFeatures::empty(), + fee_msat: 1000, + cltv_expiry_delta: 48, + maybe_announced_channel: false, + }, + + // Carol + RouteHop { + pubkey: carol_node_id, + node_features: NodeFeatures::empty(), + short_channel_id: bob_carol_scid, + channel_features: ChannelFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: 48, + maybe_announced_channel: false, + } + ], + blinded_tail: Some(BlindedTail { + trampoline_hops: vec![ + // Carol + TrampolineHop { + pubkey: carol_node_id, + node_features: Features::empty(), + fee_msat: amt_msat, + cltv_expiry_delta: 24, + }, + ], + hops: carol_blinded_hops, + blinding_point: carol_blinding_point, + excess_final_cltv_expiry_delta: 39, + final_value_msat: amt_msat, + }) + }], + route_params: None, + }; + + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + + check_added_monitors!(&nodes[0], 1); + + if success { + pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], amt_msat, payment_hash, payment_secret); + claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); + } else { + let replacement_onion = { + // create a substitute onion where the last Trampoline hop is a forward + let trampoline_secret_key = secret_from_hex("0134928f7b7ca6769080d70f16be84c812c741f545b49a34db47ce338a205799"); + let prng_seed = secret_from_hex("fe02b4b9054302a3ddf4e1e9f7c411d644aebbd295218ab009dca94435f775a9"); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); + + let mut blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); + + // append some dummy blinded hop so the intro hop looks like a forward + blinded_tail.hops.push(BlindedHop { + blinded_node_id: alice_node_id, + encrypted_payload: vec![], + }); + + let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, amt_msat, &recipient_onion_fields, 32, &None).unwrap(); + + // pop the last dummy hop + trampoline_payloads.pop(); + + let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys(&secp_ctx, &route.paths[0].blinded_tail.as_ref().unwrap(), &trampoline_secret_key).unwrap(); + let trampoline_packet = onion_utils::construct_trampoline_onion_packet( + trampoline_payloads, + trampoline_onion_keys, + prng_seed.secret_bytes(), + &payment_hash, + None, + ).unwrap(); + + let outer_session_priv = secret_from_hex("e52c20461ed7acd46c4e7b591a37610519179482887bd73bf3b94617f8f03677"); + + let (outer_payloads, _, _) = onion_utils::build_onion_payloads(&route.paths[0], outer_total_msat, &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap(); + let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv).unwrap(); + let outer_packet = onion_utils::construct_onion_packet( + outer_payloads, + outer_onion_keys, + prng_seed.secret_bytes(), + &payment_hash, + ).unwrap(); + + outer_packet + }; + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let mut first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let mut update_message = match first_message_event { + MessageSendEvent::UpdateHTLCs { ref mut updates, .. } => { + assert_eq!(updates.update_add_htlcs.len(), 1); + updates.update_add_htlcs.get_mut(0) + }, + _ => panic!() + }; + update_message.map(|msg| { + msg.onion_routing_packet = replacement_onion.clone(); + }); + + let route: &[&Node] = &[&nodes[1], &nodes[2]]; + let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event) + .with_payment_preimage(payment_preimage) + .without_claimable_event() + .expect_failure(HTLCDestination::InvalidOnion); + do_pass_along_path(args); + + { + let unblinded_node_updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id()); + nodes[1].node.handle_update_fail_htlc( + nodes[2].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[1], &nodes[2], &unblinded_node_updates.commitment_signed, true, false); + } + { + let unblinded_node_updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); + nodes[0].node.handle_update_fail_htlc( + nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false); + } + { + let payment_failed_conditions = PaymentFailedConditions::new() + .expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 0]); + expect_payment_failed_conditions(&nodes[0], payment_hash, true, payment_failed_conditions); + } + } +} + +#[test] +fn test_trampoline_single_hop_receive() { + // Simulate a payment of A (0) -> B (1) -> C(Trampoline (blinded intro)) (2) + do_test_trampoline_single_hop_receive(true); + + // Simulate a payment failure of A (0) -> B (1) -> C(Trampoline (blinded forward)) (2) + do_test_trampoline_single_hop_receive(false); +} diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index c17c7ad01b1..27d9bd4d6e7 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -265,7 +265,36 @@ pub(super) fn create_recv_pending_htlc_info( sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context), intro_node_blinding_point.is_none(), true, invoice_request) } - onion_utils::Hop::TrampolineReceive { .. } | onion_utils::Hop::TrampolineBlindedReceive { .. } => todo!(), + onion_utils::Hop::TrampolineReceive { + trampoline_hop_data: msgs::InboundOnionReceivePayload { + payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, + cltv_expiry_height, payment_metadata, .. + }, .. + } => + (payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, + cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None), + onion_utils::Hop::TrampolineBlindedReceive { + trampoline_hop_data: msgs::InboundOnionBlindedReceivePayload { + sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, payment_secret, + intro_node_blinding_point, payment_constraints, payment_context, keysend_preimage, + custom_tlvs, invoice_request + }, .. + } => { + check_blinded_payment_constraints( + sender_intended_htlc_amt_msat, cltv_expiry, &payment_constraints, + ) + .map_err(|()| { + InboundHTLCErr { + err_code: INVALID_ONION_BLINDING, + err_data: vec![0; 32], + msg: "Amount or cltv_expiry violated blinded payment constraints within Trampoline onion", + } + })?; + let payment_data = msgs::FinalOnionHopData { payment_secret, total_msat }; + (Some(payment_data), keysend_preimage, custom_tlvs, + sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context), + intro_node_blinding_point.is_none(), true, invoice_request) + }, onion_utils::Hop::Forward { .. } => { return Err(InboundHTLCErr { err_code: 0x4000|22, From a6f4c7fc727312fcbf4a6dc5872060b546907a95 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Tue, 25 Mar 2025 12:23:08 -0700 Subject: [PATCH 8/9] Test unblinded Trampoline receives --- lightning/src/ln/blinded_payment_tests.rs | 167 ++++++++++++++++++++++ lightning/src/ln/msgs.rs | 19 +++ 2 files changed, 186 insertions(+) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 81535bcb9ee..3f42be59e3a 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2174,3 +2174,170 @@ fn test_trampoline_single_hop_receive() { // Simulate a payment failure of A (0) -> B (1) -> C(Trampoline (blinded forward)) (2) do_test_trampoline_single_hop_receive(false); } + +#[test] +fn test_trampoline_unblinded_receive() { + // Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2) + + const TOTAL_NODE_COUNT: usize = 3; + let secp_ctx = Secp256k1::new(); + + let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); + let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]); + let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs); + + let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks + connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1); + } + + let alice_node_id = nodes[0].node().get_our_node_id(); + let bob_node_id = nodes[1].node().get_our_node_id(); + let carol_node_id = nodes[2].node().get_our_node_id(); + + let alice_bob_scid = nodes[0].node().list_channels().iter().find(|c| c.channel_id == chan_id_alice_bob).unwrap().short_channel_id.unwrap(); + let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap(); + + let amt_msat = 1000; + let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); + let payee_tlvs = blinded_path::payment::TrampolineForwardTlvs { + next_trampoline: alice_node_id, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: amt_msat, + }, + features: BlindedHopFeatures::empty(), + payment_relay: PaymentRelay { + cltv_expiry_delta: 0, + fee_proportional_millionths: 0, + fee_base_msat: 0, + }, + next_blinding_override: None, + }; + + let carol_unblinded_tlvs = payee_tlvs.encode(); + let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))]; + let carol_alice_trampoline_session_priv = secret_from_hex("a0f4b8d7b6c2d0ffdfaf718f76e9decaef4d9fb38a8c4addb95c4007cc3eee03"); + let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &carol_alice_trampoline_session_priv); + let carol_blinded_hops = blinded_path::utils::construct_blinded_hops( + &secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv + ).unwrap(); + + let route = Route { + paths: vec![Path { + hops: vec![ + // Bob + RouteHop { + pubkey: bob_node_id, + node_features: NodeFeatures::empty(), + short_channel_id: alice_bob_scid, + channel_features: ChannelFeatures::empty(), + fee_msat: 1000, + cltv_expiry_delta: 48, + maybe_announced_channel: false, + }, + + // Carol + RouteHop { + pubkey: carol_node_id, + node_features: NodeFeatures::empty(), + short_channel_id: bob_carol_scid, + channel_features: ChannelFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: 48, + maybe_announced_channel: false, + } + ], + blinded_tail: Some(BlindedTail { + trampoline_hops: vec![ + // Carol + TrampolineHop { + pubkey: carol_node_id, + node_features: Features::empty(), + fee_msat: amt_msat, + cltv_expiry_delta: 24, + }, + ], + hops: carol_blinded_hops, + blinding_point: carol_blinding_point, + excess_final_cltv_expiry_delta: 39, + final_value_msat: amt_msat, + }) + }], + route_params: None, + }; + + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + + let replacement_onion = { + // create a substitute onion where the last Trampoline hop is an unblinded receive, which we + // (deliberately) do not support out of the box, therefore necessitating this workaround + let trampoline_secret_key = secret_from_hex("0134928f7b7ca6769080d70f16be84c812c741f545b49a34db47ce338a205799"); + let prng_seed = secret_from_hex("fe02b4b9054302a3ddf4e1e9f7c411d644aebbd295218ab009dca94435f775a9"); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); + + let blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); + let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, amt_msat, &recipient_onion_fields, 32, &None).unwrap(); + + // pop the last dummy hop + trampoline_payloads.pop(); + + trampoline_payloads.push(msgs::OutboundTrampolinePayload::Receive { + payment_data: Some(msgs::FinalOnionHopData { + payment_secret, + total_msat: amt_msat, + }), + sender_intended_htlc_amt_msat: amt_msat, + cltv_expiry_height: 104, + }); + + let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys(&secp_ctx, &route.paths[0].blinded_tail.as_ref().unwrap(), &trampoline_secret_key).unwrap(); + let trampoline_packet = onion_utils::construct_trampoline_onion_packet( + trampoline_payloads, + trampoline_onion_keys, + prng_seed.secret_bytes(), + &payment_hash, + None, + ).unwrap(); + + let outer_session_priv = secret_from_hex("e52c20461ed7acd46c4e7b591a37610519179482887bd73bf3b94617f8f03677"); + + let (outer_payloads, _, _) = onion_utils::build_onion_payloads(&route.paths[0], outer_total_msat, &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap(); + let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv).unwrap(); + let outer_packet = onion_utils::construct_onion_packet( + outer_payloads, + outer_onion_keys, + prng_seed.secret_bytes(), + &payment_hash, + ).unwrap(); + + outer_packet + }; + + check_added_monitors!(&nodes[0], 1); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let mut first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let mut update_message = match first_message_event { + MessageSendEvent::UpdateHTLCs { ref mut updates, .. } => { + assert_eq!(updates.update_add_htlcs.len(), 1); + updates.update_add_htlcs.get_mut(0) + }, + _ => panic!() + }; + update_message.map(|msg| { + msg.onion_routing_packet = replacement_onion.clone(); + }); + + let route: &[&Node] = &[&nodes[1], &nodes[2]]; + let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event) + .with_payment_secret(payment_secret); + do_pass_along_path(args); + + claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); +} + diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 06e09c7c917..810477afbfe 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -2202,6 +2202,15 @@ mod fuzzy_internal_msgs { /// The node id to which the trampoline node must find a route. outgoing_node_id: PublicKey, }, + #[cfg(test)] + /// LDK does not support making Trampoline payments to unblinded recipients. However, for + /// the purpose of testing our ability to receive them, we make this variant available in a + /// testing environment. + Receive { + payment_data: Option, + sender_intended_htlc_amt_msat: u64, + cltv_expiry_height: u32, + }, #[allow(unused)] /// This is the last Trampoline hop, whereupon the Trampoline forward mechanism is exited, /// and payment data is relayed using non-Trampoline blinded hops @@ -3178,6 +3187,16 @@ impl<'a> Writeable for OutboundTrampolinePayload<'a> { (14, outgoing_node_id, required) }); }, + #[cfg(test)] + Self::Receive { + ref payment_data, sender_intended_htlc_amt_msat, cltv_expiry_height, + } => { + _encode_varint_length_prefixed_tlv!(w, { + (2, HighZeroBytesDroppedBigSize(*sender_intended_htlc_amt_msat), required), + (4, HighZeroBytesDroppedBigSize(*cltv_expiry_height), required), + (8, payment_data, option) + }); + }, Self::LegacyBlindedPathEntry { amt_to_forward, outgoing_cltv_value, payment_paths, invoice_features } => { let mut blinded_path_serialization = [0u8; 2048]; // Fixed-length buffer on the stack let serialization_length = { From 3970f1d96b52c9e11626864589352db9492938ac Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Wed, 26 Mar 2025 13:13:50 -0700 Subject: [PATCH 9/9] Test rejection of Trampoline forwards In the next PR, we will be adding support for forwarding payments between Trampoline hops. Until we do that, forwarding payments need to be rejected, which we test in this commit. --- lightning/src/ln/blinded_payment_tests.rs | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 3f42be59e3a..17494b06098 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2341,3 +2341,122 @@ fn test_trampoline_unblinded_receive() { claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); } +#[test] +fn test_trampoline_forward_rejection() { + const TOTAL_NODE_COUNT: usize = 3; + + let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); + let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]); + let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs); + + let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks + connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1); + } + + let alice_node_id = nodes[0].node().get_our_node_id(); + let bob_node_id = nodes[1].node().get_our_node_id(); + let carol_node_id = nodes[2].node().get_our_node_id(); + + let alice_bob_scid = nodes[0].node().list_channels().iter().find(|c| c.channel_id == chan_id_alice_bob).unwrap().short_channel_id.unwrap(); + let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap(); + + let amt_msat = 1000; + let (payment_preimage, payment_hash, _) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); + + let route = Route { + paths: vec![Path { + hops: vec![ + // Bob + RouteHop { + pubkey: bob_node_id, + node_features: NodeFeatures::empty(), + short_channel_id: alice_bob_scid, + channel_features: ChannelFeatures::empty(), + fee_msat: 1000, + cltv_expiry_delta: 48, + maybe_announced_channel: false, + }, + + // Carol + RouteHop { + pubkey: carol_node_id, + node_features: NodeFeatures::empty(), + short_channel_id: bob_carol_scid, + channel_features: ChannelFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: 48, + maybe_announced_channel: false, + } + ], + blinded_tail: Some(BlindedTail { + trampoline_hops: vec![ + // Carol + TrampolineHop { + pubkey: carol_node_id, + node_features: Features::empty(), + fee_msat: amt_msat, + cltv_expiry_delta: 24, + }, + + // Alice (unreachable) + TrampolineHop { + pubkey: alice_node_id, + node_features: Features::empty(), + fee_msat: amt_msat, + cltv_expiry_delta: 24, + }, + ], + hops: vec![BlindedHop{ + // Fake public key + blinded_node_id: alice_node_id, + encrypted_payload: vec![], + }], + blinding_point: alice_node_id, + excess_final_cltv_expiry_delta: 39, + final_value_msat: amt_msat, + }) + }], + route_params: None, + }; + + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.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 first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + + let route: &[&Node] = &[&nodes[1], &nodes[2]]; + let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event) + .with_payment_preimage(payment_preimage) + .without_claimable_event() + .expect_failure(HTLCDestination::FailedPayment { payment_hash }); + do_pass_along_path(args); + + { + let unblinded_node_updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id()); + nodes[1].node.handle_update_fail_htlc( + nodes[2].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[1], &nodes[2], &unblinded_node_updates.commitment_signed, true, false); + } + { + let unblinded_node_updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); + nodes[0].node.handle_update_fail_htlc( + nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false); + } + { + // Expect a PERM|10 (unknown_next_peer) error while we are unable to route forwarding + // Trampoline payments. + let payment_failed_conditions = PaymentFailedConditions::new() + .expected_htlc_error_data(0x4000 | 10, &[0; 0]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + } +}