From 50f826ff5a9ae44ed0d93768191bf47af31ec81a Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 17 Apr 2025 11:04:31 -0700 Subject: [PATCH 1/9] TODO: rephrase: blinded hop error handling --- lightning/src/ln/onion_utils.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index f4cd412cc1d..cfdd2c459da 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2232,6 +2232,12 @@ where Ok(Hop::BlindedReceive { shared_secret, hop_data }) }, msgs::InboundOnionPayload::TrampolineEntrypoint(hop_data) => { + if blinding_point.is_some() { + return Err(OnionDecodeErr::Malformed { + err_msg: "UpdateAddHTLC messages cannot contain blinding points for TrampolineEntryPoint payloads.", + reason: LocalHTLCFailureReason::InvalidOnionBlinding, + }); + } let incoming_trampoline_public_key = hop_data.trampoline_packet.public_key; let trampoline_blinded_node_id_tweak = hop_data.current_path_key.map(|bp| { let blinded_tlvs_ss = @@ -2256,7 +2262,7 @@ where &hop_data.trampoline_packet.hop_data, hop_data.trampoline_packet.hmac, Some(payment_hash), - (blinding_point, node_signer), + (hop_data.current_path_key, node_signer), ); match decoded_trampoline_hop { Ok(( From df22b62df5074ff6987555ed4aae4fa592330587 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 17 Apr 2025 11:04:20 -0700 Subject: [PATCH 2/9] Enforce Trampoline constraints Ensure that the Trampoline onion's amount and CLTV values do not exceed the limitations imposed by the outer onion. --- lightning/src/ln/blinded_payment_tests.rs | 65 ++++++++--- lightning/src/ln/onion_payment.rs | 125 +++++++++++++++++++++- lightning/src/ln/onion_utils.rs | 22 ++++ lightning/src/util/errors.rs | 2 + 4 files changed, 195 insertions(+), 19 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 368b9cd199a..1115c879ea7 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2061,7 +2061,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { pubkey: carol_node_id, node_features: Features::empty(), fee_msat: amt_msat, - cltv_expiry_delta: 24, + cltv_expiry_delta: 39, }, ], hops: carol_blinded_hops, @@ -2175,8 +2175,7 @@ fn test_trampoline_single_hop_receive() { do_test_trampoline_single_hop_receive(false); } -#[test] -fn test_trampoline_unblinded_receive() { +fn do_test_trampoline_unblinded_receive(underpay: bool) { // Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2) const TOTAL_NODE_COUNT: usize = 3; @@ -2246,7 +2245,7 @@ fn test_trampoline_unblinded_receive() { node_features: NodeFeatures::empty(), short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), - fee_msat: 0, + fee_msat: 0, // no routing fees because it's the final hop cltv_expiry_delta: 48, maybe_announced_channel: false, } @@ -2257,8 +2256,8 @@ fn test_trampoline_unblinded_receive() { TrampolineHop { pubkey: carol_node_id, node_features: Features::empty(), - fee_msat: amt_msat, - cltv_expiry_delta: 24, + fee_msat: 0, + cltv_expiry_delta: 72, }, ], hops: carol_blinded_hops, @@ -2270,6 +2269,8 @@ fn test_trampoline_unblinded_receive() { route_params: None, }; + // outer 56 + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); let replacement_onion = { @@ -2285,12 +2286,13 @@ fn test_trampoline_unblinded_receive() { // pop the last dummy hop trampoline_payloads.pop(); + let replacement_payload_amount = if underpay { amt_msat * 2 } else { amt_msat }; trampoline_payloads.push(msgs::OutboundTrampolinePayload::Receive { payment_data: Some(msgs::FinalOnionHopData { payment_secret, - total_msat: amt_msat, + total_msat: replacement_payload_amount, }), - sender_intended_htlc_amt_msat: amt_msat, + sender_intended_htlc_amt_msat: replacement_payload_amount, cltv_expiry_height: 104, }); @@ -2334,15 +2336,50 @@ fn test_trampoline_unblinded_receive() { }); 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); + let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event); + let args = if underpay { + args.with_payment_preimage(payment_preimage) + .without_claimable_event() + .expect_failure(HTLCDestination::FailedPayment { payment_hash }) + } else { + args.with_payment_secret(payment_secret) + }; + do_pass_along_path(args); - claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); + if underpay { + { + 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(LocalHTLCFailureReason::FinalIncorrectHTLCAmount, &[0, 0, 0, 0, 0, 0, 3, 232]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + } + } else { + claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); + } +} + +#[test] +fn test_trampoline_unblinded_receive() { + do_test_trampoline_unblinded_receive(true); + do_test_trampoline_unblinded_receive(false); } #[test] -fn test_trampoline_forward_rejection() { +fn test_trampoline_constraint_enforcement() { const TOTAL_NODE_COUNT: usize = 3; let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); @@ -2435,7 +2472,7 @@ fn test_trampoline_forward_rejection() { 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 }); + .expect_failure(HTLCDestination::InvalidOnion); do_pass_along_path(args); { @@ -2455,7 +2492,7 @@ fn test_trampoline_forward_rejection() { { // Expect UnknownNextPeer error while we are unable to route forwarding Trampoline payments. let payment_failed_conditions = PaymentFailedConditions::new() - .expected_htlc_error_data(LocalHTLCFailureReason::UnknownNextPeer, &[0; 0]); + .expected_htlc_error_data(LocalHTLCFailureReason::FinalIncorrectHTLCAmount, &[0, 0, 0, 0, 0, 0, 3, 232]); expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); } } diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 09d453d0020..edc8b031730 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -19,6 +19,7 @@ use crate::ln::onion_utils; use crate::ln::onion_utils::{HTLCFailReason, ONION_DATA_LEN, LocalHTLCFailureReason}; use crate::sign::{NodeSigner, Recipient}; use crate::util::logger::Logger; +use crate::util::ser::Writeable; #[allow(unused_imports)] use crate::prelude::*; @@ -69,6 +70,16 @@ fn check_blinded_forward( Ok((amt_to_forward, outgoing_cltv_value)) } +fn check_trampoline_onion_constraints(outer_hop_data: &msgs::InboundTrampolineEntrypointPayload, trampoline_cltv_value: u32, trampoline_amount: u64) -> Result<(), LocalHTLCFailureReason> { + if outer_hop_data.outgoing_cltv_value < trampoline_cltv_value { + return Err(LocalHTLCFailureReason::FinalIncorrectCLTVExpiry); + } + if outer_hop_data.multipath_trampoline_data.as_ref().map_or(outer_hop_data.amt_to_forward, |mtd| mtd.total_msat) < trampoline_amount { + return Err(LocalHTLCFailureReason::FinalIncorrectHTLCAmount); + } + Ok(()) +} + enum RoutingInfo { Direct { short_channel_id: u64, @@ -129,7 +140,25 @@ pub(super) fn create_fwd_pending_htlc_info( reason: LocalHTLCFailureReason::InvalidOnionPayload, err_data: Vec::new(), }), - onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + onion_utils::Hop::TrampolineForward { ref outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + check_trampoline_onion_constraints(outer_hop_data, next_trampoline_hop_data.outgoing_cltv_value, next_trampoline_hop_data.amt_to_forward).map_err(|reason| { + let mut err_data = Vec::new(); + match reason { + LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => { + outer_hop_data.outgoing_cltv_value.write(&mut err_data).unwrap(); + } + LocalHTLCFailureReason::FinalIncorrectHTLCAmount => { + outer_hop_data.amt_to_forward.write(&mut err_data).unwrap(); + } + _ => unreachable!() + } + // The Trampoline onion's amt and CLTV values cannot exceed the outer onion's + InboundHTLCErr { + reason, + err_data, + msg: "Underflow calculating outbound amount or CLTV value for Trampoline forward", + } + })?; ( RoutingInfo::Trampoline { next_trampoline: next_trampoline_hop_data.next_trampoline, @@ -144,7 +173,7 @@ pub(super) fn create_fwd_pending_htlc_info( None ) }, - onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + onion_utils::Hop::TrampolineBlindedForward { ref 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 ).map_err(|()| { @@ -156,6 +185,15 @@ pub(super) fn create_fwd_pending_htlc_info( err_data: vec![0; 32], } })?; + check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward).map_err(|_| { + // The Trampoline onion's amt and CLTV values cannot exceed the outer onion's, but + // we're inside a blinded path + InboundHTLCErr { + reason: LocalHTLCFailureReason::InvalidOnionBlinding, + err_data: vec![0; 32], + msg: "Underflow calculating outbound amount or CLTV value for Trampoline forward", + } + })?; ( RoutingInfo::Trampoline { next_trampoline: next_trampoline_hop_data.next_trampoline, @@ -274,14 +312,35 @@ pub(super) fn create_recv_pending_htlc_info( intro_node_blinding_point.is_none(), true, invoice_request) } onion_utils::Hop::TrampolineReceive { + ref outer_hop_data, trampoline_hop_data: msgs::InboundOnionReceivePayload { payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, cltv_expiry_height, payment_metadata, .. }, .. - } => + } => { + check_trampoline_onion_constraints(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat).map_err(|reason| { + let mut err_data = Vec::new(); + match reason { + LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => { + outer_hop_data.outgoing_cltv_value.write(&mut err_data).unwrap(); + } + LocalHTLCFailureReason::FinalIncorrectHTLCAmount => { + outer_hop_data.amt_to_forward.write(&mut err_data).unwrap(); + } + _ => unreachable!() + } + // The Trampoline onion's amt and CLTV values cannot exceed the outer onion's + InboundHTLCErr { + reason, + err_data, + msg: "Underflow calculating skimmable amount or CLTV value for Trampoline receive", + } + })?; (payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, - cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None), + cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None) + }, onion_utils::Hop::TrampolineBlindedReceive { + ref outer_hop_data, 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, @@ -298,6 +357,15 @@ pub(super) fn create_recv_pending_htlc_info( msg: "Amount or cltv_expiry violated blinded payment constraints within Trampoline onion", } })?; + check_trampoline_onion_constraints(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat).map_err(|_| { + // The Trampoline onion's amt and CLTV values cannot exceed the outer onion's, but + // we're inside a blinded path + InboundHTLCErr { + reason: LocalHTLCFailureReason::InvalidOnionBlinding, + err_data: vec![0; 32], + msg: "Underflow calculating skimmable amount or CLTV value for Trampoline receive", + } + })?; 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), @@ -583,7 +651,54 @@ where outgoing_cltv_value }) } - 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, .. } => { + onion_utils::Hop::TrampolineForward { ref outer_hop_data, next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, outer_shared_secret, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { + if let Err(reason) = check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward) { + let mut data = Vec::new(); + match reason { + LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => { + outer_hop_data.outgoing_cltv_value.write(&mut data).unwrap(); + } + LocalHTLCFailureReason::FinalIncorrectHTLCAmount => { + outer_hop_data.amt_to_forward.write(&mut data).unwrap(); + } + _ => unreachable!() + } + return encode_relay_error("Underflow calculating outbound amount or CLTV value for Trampoline forward", + reason, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &data); + } + let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, + incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); + Some(NextPacketDetails { + next_packet_pubkey: next_trampoline_packet_pubkey, + outgoing_connector: HopConnector::Trampoline(next_trampoline), + outgoing_amt_msat: amt_to_forward, + outgoing_cltv_value, + }) + } + onion_utils::Hop::TrampolineBlindedForward { ref outer_hop_data, next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload { next_trampoline, ref payment_relay, ref payment_constraints, ref features, .. }, outer_shared_secret, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { + let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward( + msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features + ) { + Ok((amt, cltv)) => (amt, cltv), + Err(()) => { + return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded forward", + LocalHTLCFailureReason::InvalidOnionBlinding, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &[0; 32]); + } + }; + if let Err(reason) = check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward) { + let mut data = Vec::new(); + match reason { + LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => { + outer_hop_data.outgoing_cltv_value.write(&mut data).unwrap(); + } + LocalHTLCFailureReason::FinalIncorrectHTLCAmount => { + outer_hop_data.amt_to_forward.write(&mut data).unwrap(); + } + _ => unreachable!() + } + return encode_relay_error("Underflow calculating outbound amount or CLTV value for Trampoline forward", + reason, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &data); + } let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index cfdd2c459da..3b50ebcde16 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1606,6 +1606,13 @@ pub enum LocalHTLCFailureReason { HTLCMaximum, /// The HTLC was failed because our remote peer is offline. PeerOffline, + /// We have been unable to forward a payment to the next Trampoline node, but may be able to + /// later. + TemporaryTrampolineFailure, + /// The amount or CLTV expiry were insufficient to route the payment to the next Trampoline node. + TrampolineFeeOrExpiryInsufficient, + /// The specified next Trampoline node cannot be reached from our node. + UnknownNextTrampoline, } impl LocalHTLCFailureReason { @@ -1647,6 +1654,9 @@ impl LocalHTLCFailureReason { Self::InvalidOnionPayload | Self::InvalidTrampolinePayload => PERM | 22, Self::MPPTimeout => 23, Self::InvalidOnionBlinding => BADONION | PERM | 24, + Self::TemporaryTrampolineFailure => NODE | 25, + Self::TrampolineFeeOrExpiryInsufficient => NODE | 26, + Self::UnknownNextTrampoline => PERM | 27, Self::UnknownFailureCode { code } => *code, } } @@ -1707,6 +1717,12 @@ impl From for LocalHTLCFailureReason { LocalHTLCFailureReason::MPPTimeout } else if value == (BADONION | PERM | 24) { LocalHTLCFailureReason::InvalidOnionBlinding + } else if value == (NODE | 25) { + LocalHTLCFailureReason::TemporaryTrampolineFailure + } else if value == (NODE | 26) { + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + } else if value == (PERM | 27) { + LocalHTLCFailureReason::UnknownNextTrampoline } else { LocalHTLCFailureReason::UnknownFailureCode { code: value } } @@ -1759,6 +1775,9 @@ impl_writeable_tlv_based_enum!(LocalHTLCFailureReason, (81, HTLCMinimum) => {}, (83, HTLCMaximum) => {}, (85, PeerOffline) => {}, + (87, TemporaryTrampolineFailure) => {}, + (89, TrampolineFeeOrExpiryInsufficient) => {}, + (91, UnknownNextTrampoline) => {}, ); #[derive(Clone)] // See Channel::revoke_and_ack for why, tl;dr: Rust bug @@ -1915,6 +1934,9 @@ impl HTLCFailReason { debug_assert!(false, "Unknown failure code: {}", code) } }, + LocalHTLCFailureReason::TemporaryTrampolineFailure => debug_assert!(data.is_empty()), + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient => debug_assert_eq!(data.len(), 10), + LocalHTLCFailureReason::UnknownNextTrampoline => debug_assert!(data.is_empty()), } Self(HTLCFailReasonRepr::Reason { data, failure_reason }) diff --git a/lightning/src/util/errors.rs b/lightning/src/util/errors.rs index 7b9a24f891f..f241337b986 100644 --- a/lightning/src/util/errors.rs +++ b/lightning/src/util/errors.rs @@ -145,6 +145,8 @@ pub(crate) fn get_onion_error_description(error_code: u16) -> (&'static str, &'s _c if _c == 21 => ("Node indicated the CLTV expiry in the HTLC is too far in the future", "expiry_too_far"), _c if _c == PERM|22 => ("Node indicated that the decrypted onion per-hop payload was not understood by it or is incomplete", "invalid_onion_payload"), _c if _c == 23 => ("The final node indicated the complete amount of the multi-part payment was not received within a reasonable time", "mpp_timeout"), + _c if _c == NODE|25 => ("The Trampoline node was unable to relay the payment to the subsequent Trampoline node.", "temporary_trampoline_failure"), + _c if _c == NODE|26 => ("Node indicated the fee amount or CLTV value was below that required by the Trampoline node", "trampoline_fee_or_expiry_insufficient"), _ => ("Unknown", ""), } } From 9888bbf9f65fe28ee79b4daa00910600be9c14a8 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 17 Apr 2025 10:57:47 -0700 Subject: [PATCH 3/9] f: remove duplicated trampoline onion constraint checks --- lightning/src/ln/onion_payment.rs | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index edc8b031730..e523a0f41dc 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -652,20 +652,6 @@ where }) } onion_utils::Hop::TrampolineForward { ref outer_hop_data, next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, outer_shared_secret, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { - if let Err(reason) = check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward) { - let mut data = Vec::new(); - match reason { - LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => { - outer_hop_data.outgoing_cltv_value.write(&mut data).unwrap(); - } - LocalHTLCFailureReason::FinalIncorrectHTLCAmount => { - outer_hop_data.amt_to_forward.write(&mut data).unwrap(); - } - _ => unreachable!() - } - return encode_relay_error("Underflow calculating outbound amount or CLTV value for Trampoline forward", - reason, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &data); - } let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { @@ -685,20 +671,6 @@ where LocalHTLCFailureReason::InvalidOnionBlinding, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &[0; 32]); } }; - if let Err(reason) = check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward) { - let mut data = Vec::new(); - match reason { - LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => { - outer_hop_data.outgoing_cltv_value.write(&mut data).unwrap(); - } - LocalHTLCFailureReason::FinalIncorrectHTLCAmount => { - outer_hop_data.amt_to_forward.write(&mut data).unwrap(); - } - _ => unreachable!() - } - return encode_relay_error("Underflow calculating outbound amount or CLTV value for Trampoline forward", - reason, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &data); - } let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { From 7a875d23387f8727f0538ac9e204c6cc77ca7e06 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 17 Apr 2025 11:03:38 -0700 Subject: [PATCH 4/9] f: formatting --- lightning/src/ln/onion_utils.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 3b50ebcde16..ea3638bb3b6 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1935,7 +1935,9 @@ impl HTLCFailReason { } }, LocalHTLCFailureReason::TemporaryTrampolineFailure => debug_assert!(data.is_empty()), - LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient => debug_assert_eq!(data.len(), 10), + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient => { + debug_assert_eq!(data.len(), 10) + }, LocalHTLCFailureReason::UnknownNextTrampoline => debug_assert!(data.is_empty()), } From a258d01a78c5a978727c7761f9188ba9458bc472 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 17 Apr 2025 11:07:25 -0700 Subject: [PATCH 5/9] f: last tramp error --- lightning/src/util/errors.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lightning/src/util/errors.rs b/lightning/src/util/errors.rs index f241337b986..1d3acb0ee5f 100644 --- a/lightning/src/util/errors.rs +++ b/lightning/src/util/errors.rs @@ -145,8 +145,9 @@ pub(crate) fn get_onion_error_description(error_code: u16) -> (&'static str, &'s _c if _c == 21 => ("Node indicated the CLTV expiry in the HTLC is too far in the future", "expiry_too_far"), _c if _c == PERM|22 => ("Node indicated that the decrypted onion per-hop payload was not understood by it or is incomplete", "invalid_onion_payload"), _c if _c == 23 => ("The final node indicated the complete amount of the multi-part payment was not received within a reasonable time", "mpp_timeout"), - _c if _c == NODE|25 => ("The Trampoline node was unable to relay the payment to the subsequent Trampoline node.", "temporary_trampoline_failure"), + _c if _c == NODE|25 => ("The Trampoline node was unable to relay the payment to the subsequent Trampoline node", "temporary_trampoline_failure"), _c if _c == NODE|26 => ("Node indicated the fee amount or CLTV value was below that required by the Trampoline node", "trampoline_fee_or_expiry_insufficient"), + _c if _c == PERM|27 => ("The next Trampoline node specified in outgoing_node_id could not be found", "unknown_next_trampoline"), _ => ("Unknown", ""), } } From a79ba4497f60fdb118520ec06e488161572e42d7 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Mon, 7 Apr 2025 00:43:08 -0700 Subject: [PATCH 6/9] Create TrampolineForward HTLCSource variant To process errors returned from downstream nodes when forwarding between Trampoline nodes, we need to store information beyond what's available in `HTLCPreviousHopData`, such as the used hops and the newly generated outer onion's session_priv. To that end, we add a new variant to `HTLCSource` in this commit, which also future-proofs mapping an outbound forward to an incoming MPP. --- lightning/src/chain/channelmonitor.rs | 4 +- lightning/src/ln/channelmanager.rs | 239 +++++++++++++++++++++++++- lightning/src/ln/onion_utils.rs | 5 + lightning/src/util/ser.rs | 2 + 4 files changed, 246 insertions(+), 4 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index a74cb7d7685..d1273f09bd2 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -2432,6 +2432,7 @@ impl ChannelMonitorImpl { None => panic!("Outbound HTLCs should have a source"), Some(&HTLCSource::PreviousHopData(_)) => false, Some(&HTLCSource::OutboundRoute { .. }) => true, + Some(&HTLCSource::TrampolineForward { .. }) => false, }; return Some(Balance::MaybeTimeoutClaimableHTLC { amount_satoshis: htlc.amount_msat / 1000, @@ -2646,6 +2647,7 @@ impl ChannelMonitor { None => panic!("Outbound HTLCs should have a source"), Some(HTLCSource::PreviousHopData(_)) => false, Some(HTLCSource::OutboundRoute { .. }) => true, + Some(HTLCSource::TrampolineForward { .. }) => false, }; if outbound_payment { outbound_payment_htlc_rounded_msat += rounded_value_msat; @@ -3171,7 +3173,7 @@ impl ChannelMonitorImpl { } else { false } })); } - self.counterparty_fulfilled_htlcs.insert(*claimed_htlc_id, *claimed_preimage); + self.counterparty_fulfilled_htlcs.insert(claimed_htlc_id.clone(), *claimed_preimage); } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 363f2ffdb65..28999b6017d 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -56,8 +56,8 @@ use crate::ln::channel_state::ChannelDetails; use crate::types::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; #[cfg(any(feature = "_test_utils", test))] use crate::types::features::Bolt11InvoiceFeatures; -use crate::routing::router::{BlindedTail, InFlightHtlcs, Path, Payee, PaymentParameters, RouteParameters, RouteParametersConfig, Router, FixedRouter, Route}; -use crate::ln::onion_payment::{check_incoming_htlc_cltv, create_recv_pending_htlc_info, create_fwd_pending_htlc_info, decode_incoming_update_add_htlc_onion, HopConnector, InboundHTLCErr, NextPacketDetails, invalid_payment_err_data}; +use crate::routing::router::{BlindedTail, FixedRouter, InFlightHtlcs, Path, Payee, PaymentParameters, Route, RouteHop, RouteParameters, RouteParametersConfig, Router}; +use crate::ln::onion_payment::{check_incoming_htlc_cltv, create_recv_pending_htlc_info, create_fwd_pending_htlc_info, decode_incoming_update_add_htlc_onion, HopConnector, InboundHTLCErr, invalid_payment_err_data, NextPacketDetails}; use crate::ln::msgs; use crate::ln::onion_utils::{self}; use crate::ln::onion_utils::{HTLCFailReason, LocalHTLCFailureReason}; @@ -626,10 +626,17 @@ impl Readable for InterceptId { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) struct PreviousHopIdData { + short_channel_id: u64, + htlc_id: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] /// Uniquely describes an HTLC by its source. Just the guaranteed-unique subset of [`HTLCSource`]. pub(crate) enum SentHTLCId { PreviousHopData { short_channel_id: u64, htlc_id: u64 }, OutboundRoute { session_priv: [u8; SECRET_KEY_SIZE] }, + TrampolineForward { session_priv: [u8; SECRET_KEY_SIZE], previous_hop_data: Vec } } impl SentHTLCId { pub(crate) fn from_source(source: &HTLCSource) -> Self { @@ -640,9 +647,20 @@ impl SentHTLCId { }, HTLCSource::OutboundRoute { session_priv, .. } => Self::OutboundRoute { session_priv: session_priv.secret_bytes() }, + HTLCSource::TrampolineForward { previous_hop_data, session_priv, .. } => Self::TrampolineForward { + session_priv: session_priv.secret_bytes(), + previous_hop_data: previous_hop_data.iter().map(|hop_data| PreviousHopIdData { + short_channel_id: hop_data.short_channel_id, + htlc_id: hop_data.htlc_id, + }).collect(), + }, } } } +impl_writeable_tlv_based!(PreviousHopIdData, { + (0, short_channel_id, required), + (2, htlc_id, required), +}); impl_writeable_tlv_based_enum!(SentHTLCId, (0, PreviousHopData) => { (0, short_channel_id, required), @@ -651,6 +669,10 @@ impl_writeable_tlv_based_enum!(SentHTLCId, (2, OutboundRoute) => { (0, session_priv, required), }, + (4, TrampolineForward) => { + (0, session_priv, required), + (2, previous_hop_data, required_vec), + }, ); mod fuzzy_channelmanager { @@ -661,6 +683,16 @@ mod fuzzy_channelmanager { #[derive(Clone, Debug, PartialEq, Eq)] pub enum HTLCSource { PreviousHopData(HTLCPreviousHopData), + TrampolineForward { + /// We might be forwarding an incoming payment that was received over MPP, and therefore + /// need to store the vector of corresponding `HTLCPreviousHopData` values. + previous_hop_data: Vec, + incoming_trampoline_shared_secret: [u8; 32], + hops: Vec, + /// In order to decode inter-Trampoline errors, we need to store the session_priv key + /// given we're effectively creating new outbound routes. + session_priv: SecretKey, + }, OutboundRoute { path: Path, session_priv: SecretKey, @@ -712,6 +744,13 @@ impl core::hash::Hash for HTLCSource { payment_id.hash(hasher); first_hop_htlc_msat.hash(hasher); }, + HTLCSource::TrampolineForward { previous_hop_data, incoming_trampoline_shared_secret, hops, session_priv } => { + 2u8.hash(hasher); + previous_hop_data.hash(hasher); + incoming_trampoline_shared_secret.hash(hasher); + hops.hash(hasher); + session_priv[..].hash(hasher); + }, } } } @@ -6999,7 +7038,7 @@ where // Note that we MUST NOT end up calling methods on self.chain_monitor here - we're called // from block_connected which may run during initialization prior to the chain_monitor // being fully configured. See the docs for `ChannelManagerReadArgs` for more. - let mut push_forward_event; + let mut push_forward_event = true; match source { HTLCSource::OutboundRoute { ref path, ref session_priv, ref payment_id, .. } => { push_forward_event = self.pending_outbound_payments.fail_htlc(source, payment_hash, onion_error, path, @@ -7056,6 +7095,65 @@ where failed_next_destination: destination, }, None)); }, + HTLCSource::TrampolineForward { previous_hop_data, incoming_trampoline_shared_secret, .. } => { + // todo: what do we want to do with this given we do not wish to propagate it directly? + let _decoded_onion_failure = onion_error.decode_onion_failure(&self.secp_ctx, &self.logger, &source); + + for current_hop_data in previous_hop_data { + let incoming_packet_shared_secret = current_hop_data.incoming_packet_shared_secret; + let channel_id = current_hop_data.channel_id; + let short_channel_id = current_hop_data.short_channel_id; + let htlc_id = current_hop_data.htlc_id; + let blinded_failure = current_hop_data.blinded_failure; + log_trace!( + WithContext::from(&self.logger, None, Some(channel_id), Some(*payment_hash)), + "Failing {}HTLC with payment_hash {} backwards from us following Trampoline forwarding failure: {:?}", + if blinded_failure.is_some() { "blinded " } else { "" }, &payment_hash, onion_error + ); + let failure = match blinded_failure { + Some(BlindedFailure::FromIntroductionNode) => { + let blinded_onion_error = HTLCFailReason::reason(LocalHTLCFailureReason::InvalidOnionBlinding, vec![0; 32]); + let err_packet = blinded_onion_error.get_encrypted_failure_packet( + &incoming_packet_shared_secret, &Some(incoming_trampoline_shared_secret.clone()) + ); + HTLCForwardInfo::FailHTLC { htlc_id, err_packet } + }, + Some(BlindedFailure::FromBlindedNode) => { + HTLCForwardInfo::FailMalformedHTLC { + htlc_id, + failure_code: LocalHTLCFailureReason::InvalidOnionBlinding.failure_code(), + sha256_of_onion: [0; 32] + } + }, + None => { + let err_packet = HTLCFailReason::reason(LocalHTLCFailureReason::TemporaryTrampolineFailure, Vec::new()) + .get_encrypted_failure_packet(&incoming_packet_shared_secret, &Some(incoming_trampoline_shared_secret.clone())); + HTLCForwardInfo::FailHTLC { htlc_id, err_packet } + } + }; + + push_forward_event = self.decode_update_add_htlcs.lock().unwrap().is_empty(); + let mut forward_htlcs = self.forward_htlcs.lock().unwrap(); + push_forward_event &= forward_htlcs.is_empty(); + + match forward_htlcs.entry(short_channel_id) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().push(failure); + }, + hash_map::Entry::Vacant(entry) => { + entry.insert(vec!(failure)); + } + } + + mem::drop(forward_htlcs); + + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back((events::Event::HTLCHandlingFailed { + prev_channel_id: channel_id, + failed_next_destination: destination.clone(), + }, None)); + } + }, } push_forward_event } @@ -7514,6 +7612,63 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }); }, + HTLCSource::TrampolineForward { previous_hop_data, .. } => { + for current_previous_hop_data in previous_hop_data { + let prev_channel_id = current_previous_hop_data.channel_id; + let prev_user_channel_id = current_previous_hop_data.user_channel_id; + let prev_node_id = current_previous_hop_data.counterparty_node_id; + let completed_blocker = RAAMonitorUpdateBlockingAction::from_prev_hop_data(¤t_previous_hop_data); + self.claim_funds_from_hop(current_previous_hop_data, payment_preimage, None, + |htlc_claim_value_msat, definitely_duplicate| { + let chan_to_release = Some(EventUnblockedChannel { + counterparty_node_id: next_channel_counterparty_node_id, + funding_txo: next_channel_outpoint, + channel_id: next_channel_id, + blocking_action: completed_blocker, + }); + + if definitely_duplicate && startup_replay { + // On startup we may get redundant claims which are related to + // monitor updates still in flight. In that case, we shouldn't + // immediately free, but instead let that monitor update complete + // in the background. + (None, None) + } else if definitely_duplicate { + if let Some(other_chan) = chan_to_release { + (Some(MonitorUpdateCompletionAction::FreeOtherChannelImmediately { + downstream_counterparty_node_id: other_chan.counterparty_node_id, + downstream_funding_outpoint: other_chan.funding_txo, + downstream_channel_id: other_chan.channel_id, + blocking_action: other_chan.blocking_action, + }), None) + } else { (None, None) } + } else { + let total_fee_earned_msat = if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat { + if let Some(claimed_htlc_value) = htlc_claim_value_msat { + Some(claimed_htlc_value - forwarded_htlc_value) + } else { None } + } else { None }; + debug_assert!(skimmed_fee_msat <= total_fee_earned_msat, + "skimmed_fee_msat must always be included in total_fee_earned_msat"); + (Some(MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel { + event: events::Event::PaymentForwarded { + prev_channel_id: Some(prev_channel_id), + next_channel_id: Some(next_channel_id), + prev_user_channel_id, + next_user_channel_id, + prev_node_id, + next_node_id: Some(next_channel_counterparty_node_id), + total_fee_earned_msat, + skimmed_fee_msat, + claim_from_onchain_tx: from_onchain, + outbound_amount_forwarded_msat: forwarded_htlc_value_msat, + }, + downstream_counterparty_and_funding_outpoint: chan_to_release, + }), None) + } + }); + } + }, } } @@ -13163,6 +13318,24 @@ impl Readable for HTLCSource { }) } 1 => Ok(HTLCSource::PreviousHopData(Readable::read(reader)?)), + 2 => { + let mut previous_hop_data = Vec::new(); + let mut incoming_trampoline_shared_secret: crate::util::ser::RequiredWrapper<[u8; 32]> = crate::util::ser::RequiredWrapper(None); + let mut session_priv: crate::util::ser::RequiredWrapper = crate::util::ser::RequiredWrapper(None); + let mut hops = Vec::new(); + read_tlv_fields!(reader, { + (0, previous_hop_data, required_vec), + (2, incoming_trampoline_shared_secret, required), + (4, session_priv, required), + (6, hops, required_vec), + }); + Ok(HTLCSource::TrampolineForward { + previous_hop_data, + incoming_trampoline_shared_secret: incoming_trampoline_shared_secret.0.unwrap(), + hops, + session_priv: session_priv.0.unwrap(), + }) + }, _ => Err(DecodeError::UnknownRequiredFeature), } } @@ -13188,6 +13361,17 @@ impl Writeable for HTLCSource { 1u8.write(writer)?; field.write(writer)?; } + HTLCSource::TrampolineForward { previous_hop_data: previous_hop_data_ref, ref incoming_trampoline_shared_secret, ref session_priv, hops: hops_ref } => { + 2u8.write(writer)?; + let previous_hop_data = previous_hop_data_ref.clone(); + let hops = hops_ref.clone(); + write_tlv_fields!(writer, { + (0, previous_hop_data, required_vec), + (2, incoming_trampoline_shared_secret, required), + (4, session_priv, required), + (6, hops, required_vec), + }); + } } Ok(()) } @@ -14368,6 +14552,55 @@ where } else { true } }); }, + HTLCSource::TrampolineForward { previous_hop_data, .. } => { + for current_previous_hop_data in previous_hop_data { + let pending_forward_matches_htlc = |info: &PendingAddHTLCInfo| { + info.prev_funding_outpoint == current_previous_hop_data.outpoint && + info.prev_htlc_id == current_previous_hop_data.htlc_id + }; + // The ChannelMonitor is now responsible for this HTLC's + // failure/success and will let us know what its outcome is. If we + // still have an entry for this HTLC in `forward_htlcs` or + // `pending_intercepted_htlcs`, we were apparently not persisted after + // the monitor was when forwarding the payment. + decode_update_add_htlcs.retain(|scid, update_add_htlcs| { + update_add_htlcs.retain(|update_add_htlc| { + let matches = *scid == current_previous_hop_data.short_channel_id && + update_add_htlc.htlc_id == current_previous_hop_data.htlc_id; + if matches { + log_info!(logger, "Removing pending to-decode HTLC with hash {} as it was forwarded to the closed channel {}", + &htlc.payment_hash, &monitor.channel_id()); + } + !matches + }); + !update_add_htlcs.is_empty() + }); + forward_htlcs.retain(|_, forwards| { + forwards.retain(|forward| { + if let HTLCForwardInfo::AddHTLC(htlc_info) = forward { + if pending_forward_matches_htlc(&htlc_info) { + log_info!(logger, "Removing pending to-forward HTLC with hash {} as it was forwarded to the closed channel {}", + &htlc.payment_hash, &monitor.channel_id()); + false + } else { true } + } else { true } + }); + !forwards.is_empty() + }); + pending_intercepted_htlcs.as_mut().unwrap().retain(|intercepted_id, htlc_info| { + if pending_forward_matches_htlc(&htlc_info) { + log_info!(logger, "Removing pending intercepted HTLC with hash {} as it was forwarded to the closed channel {}", + &htlc.payment_hash, &monitor.channel_id()); + pending_events_read.retain(|(event, _)| { + if let Event::HTLCIntercepted { intercept_id: ev_id, .. } = event { + intercepted_id != ev_id + } else { true } + }); + false + } else { true } + }); + } + } HTLCSource::OutboundRoute { payment_id, session_priv, path, .. } => { if let Some(preimage) = preimage_opt { let pending_events = Mutex::new(pending_events_read); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index ea3638bb3b6..187028395b5 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -992,8 +992,13 @@ pub fn process_onion_failure( where L::Target: Logger, { + let mut trampoline_forward_path_option = None; let (path, primary_session_priv) = match htlc_source { HTLCSource::OutboundRoute { ref path, ref session_priv, .. } => (path, session_priv), + HTLCSource::TrampolineForward { ref hops, ref session_priv, .. } => { + trampoline_forward_path_option.replace(Path { hops: hops.clone(), blinded_tail: None }); + (trampoline_forward_path_option.as_ref().unwrap(), session_priv) + }, _ => unreachable!(), }; diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 2560c3af1b9..b203fca1911 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1075,12 +1075,14 @@ impl Readable for Vec { impl_for_vec!(ecdsa::Signature); impl_for_vec!(crate::chain::channelmonitor::ChannelMonitorUpdate); +impl_for_vec!(crate::ln::channelmanager::HTLCPreviousHopData); impl_for_vec!(crate::ln::channelmanager::MonitorUpdateCompletionAction); impl_for_vec!(crate::ln::channelmanager::PaymentClaimDetails); impl_for_vec!(crate::ln::msgs::SocketAddress); impl_for_vec!((A, B), A, B); impl_writeable_for_vec!(&crate::routing::router::BlindedTail); impl_readable_for_vec!(crate::routing::router::BlindedTail); +impl_for_vec!(crate::routing::router::RouteHop); impl_for_vec!(crate::routing::router::TrampolineHop); impl_for_vec_with_element_length_prefix!(crate::ln::msgs::UpdateAddHTLC); impl_writeable_for_vec_with_element_length_prefix!(&crate::ln::msgs::UpdateAddHTLC); From 2e49bc3382b35f4deaf198ddcbaebb5cb61543ad Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 17 Apr 2025 11:39:17 -0700 Subject: [PATCH 7/9] f: rename PreviousHopIdData --- lightning/src/ln/channelmanager.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 28999b6017d..819cb5554c4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -626,7 +626,7 @@ impl Readable for InterceptId { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub(crate) struct PreviousHopIdData { +pub(crate) struct PreviousHopId { short_channel_id: u64, htlc_id: u64, } @@ -636,7 +636,7 @@ pub(crate) struct PreviousHopIdData { pub(crate) enum SentHTLCId { PreviousHopData { short_channel_id: u64, htlc_id: u64 }, OutboundRoute { session_priv: [u8; SECRET_KEY_SIZE] }, - TrampolineForward { session_priv: [u8; SECRET_KEY_SIZE], previous_hop_data: Vec } + TrampolineForward { session_priv: [u8; SECRET_KEY_SIZE], previous_hop_ids: Vec } } impl SentHTLCId { pub(crate) fn from_source(source: &HTLCSource) -> Self { @@ -649,7 +649,7 @@ impl SentHTLCId { Self::OutboundRoute { session_priv: session_priv.secret_bytes() }, HTLCSource::TrampolineForward { previous_hop_data, session_priv, .. } => Self::TrampolineForward { session_priv: session_priv.secret_bytes(), - previous_hop_data: previous_hop_data.iter().map(|hop_data| PreviousHopIdData { + previous_hop_ids: previous_hop_data.iter().map(|hop_data| PreviousHopId { short_channel_id: hop_data.short_channel_id, htlc_id: hop_data.htlc_id, }).collect(), @@ -657,7 +657,7 @@ impl SentHTLCId { } } } -impl_writeable_tlv_based!(PreviousHopIdData, { +impl_writeable_tlv_based!(PreviousHopId, { (0, short_channel_id, required), (2, htlc_id, required), }); From 037403e3e134c6cf1301bab9c6a0a08d17b25d04 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Wed, 9 Apr 2025 17:28:03 -0700 Subject: [PATCH 8/9] Expand HTLCDestination variants for Trampoline forwards The previously existing `HTLCDestination` do not map nicely to the failure event of a Trampoline forward, so we introduce a new variant to fill the gap. --- lightning/src/events/mod.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 8d28c9b4191..3e4534a702a 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -490,6 +490,14 @@ pub enum HTLCDestination { /// Short channel id we are requesting to forward an HTLC to. requested_forward_scid: u64 }, + /// We couldn't forward to the next Trampoline node. That may happen if we cannot find a route, + /// or if the route we found didn't work out + FailedTrampolineForward { + /// The node ID of the next Trampoline hop we tried forwarding to + requested_next_node_id: PublicKey, + /// The channel we tried forwarding over, if we have settled on one + forward_scid: Option, + }, /// We couldn't decode the incoming onion to obtain the forwarding details. InvalidOnion, /// Failure scenario where an HTLC may have been forwarded to be intended for us, @@ -523,6 +531,10 @@ impl_writeable_tlv_based_enum_upgradable!(HTLCDestination, (4, FailedPayment) => { (0, payment_hash, required), }, + (5, FailedTrampolineForward) => { + (0, requested_next_node_id, required), + (2, forward_scid, option), + }, ); /// Will be used in [`Event::HTLCIntercepted`] to identify the next hop in the HTLC's path. From 0b86a9847db2b168c99bf422c44863b78571f0a6 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Wed, 9 Apr 2025 12:38:34 -0700 Subject: [PATCH 9/9] Find path and route to next Trampoline node In this commit, we expand our forwarding logic to do ad-hoc pathfinding to subsequent Trampoline nodes, covering both blinded and unblinded scenarios. We allow the forwards to use MPP, but we do not yet implement inbound Trampoline MPP handling, nor retry logic. We further modify our error propagation logic to handle Trampoline forward HTLC failures eagerly, i.e. when we receive a failure from the downstream node, we immediately fail or inbound MPP components. --- lightning/src/ln/blinded_payment_tests.rs | 910 ++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 295 ++++++- lightning/src/ln/onion_utils.rs | 20 +- lightning/src/routing/router.rs | 5 +- 4 files changed, 1211 insertions(+), 19 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 1115c879ea7..a1d20f101a8 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -7,6 +7,7 @@ // You may not use this file except in accordance with one or both of these // licenses. +use std::cmp::PartialEq; use bitcoin::hashes::hex::FromHex; use bitcoin::hex::DisplayHex; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, schnorr}; @@ -2496,3 +2497,912 @@ fn test_trampoline_constraint_enforcement() { expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); } } + +#[derive(PartialEq)] +enum TrampolineForwardFailureScenario { + NoRoute, + InvalidRecipientOnion, + InvalidInterTrampolineOnion, +} + +fn do_test_unblinded_trampoline_forward(failure_scenario: Option) { + // Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2) -> D(Trampoline(receive)) (3) + // trampoline hops C -> T0 (4) -> D + // make it fail at B, then at C's outer onion, then at C's inner onion + const TOTAL_NODE_COUNT: usize = 5; + 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); + if failure_scenario != Some(TrampolineForwardFailureScenario::NoRoute) { + let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 1_000_000, 0); + } + let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 4, 3, 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 dave_node_id = nodes[3].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[3], 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, // forwarding fee to Carol + 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: 2000, // fee for the usage of the entire blinded path, including Trampoline + 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: 176, // let her cook + }, + + // Dave (recipient) + TrampolineHop { + pubkey: dave_node_id, + node_features: Features::empty(), + fee_msat: 0, // no need to charge a fee as the recipient + cltv_expiry_delta: 24, + }, + ], + hops: vec![ + // Dave's blinded node id + BlindedHop { + blinded_node_id: pubkey_from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be"), + encrypted_payload: bytes_from_hex("0ccf3c8a58deaa603f657ee2a5ed9d604eb5c8ca1e5f801989afa8f3ea6d789bbdde2c7e7a1ef9ca8c38d2c54760febad8446d3f273ddb537569ef56613846ccd3aba78a"), + } + ], + 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(); + + 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: 96, + }); + + let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys(&secp_ctx, &route.paths[0].blinded_tail.as_ref().unwrap(), &trampoline_secret_key); + 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); + 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_alice_bob = 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_alice_bob.map(|msg| { + msg.onion_routing_packet = replacement_onion.clone(); + }); + } + + match failure_scenario { + None => { + let route: &[&Node] = &[&nodes[1], &nodes[2], &nodes[4], &nodes[3]]; + 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], &nodes[4], &nodes[3]], payment_preimage); + } + Some(TrampolineForwardFailureScenario::NoRoute) => { + let route = &[&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::FailedTrampolineForward { requested_next_node_id: dave_node_id, forward_scid: None }); + 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(LocalHTLCFailureReason::TemporaryTrampolineFailure, &[0; 0]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + } + } + Some(TrampolineForwardFailureScenario::InvalidInterTrampolineOnion) => { + let route_alice_carol: &[&Node] = &[&nodes[1], &nodes[2]]; + pass_along_path(&nodes[0], route_alice_carol, amt_msat, payment_hash.clone(), + None, first_message_event.clone(), false, Some(payment_preimage)); + + check_added_monitors!(&nodes[2], 1); + let mut events = nodes[2].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + let mut update_message_carol_t0 = remove_first_msg_event_to_node(&nodes[4].node.get_our_node_id(), &mut events); + { + let mut update_message = match update_message_carol_t0 { + 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| { + // corrupt the onion packet + msg.onion_routing_packet.hmac = [1; 32]; + }); + } + + let route_carol_t0: &[&Node] = &[&nodes[4]]; + let args = PassAlongPathArgs::new(&nodes[2], route_carol_t0, amt_msat, payment_hash, update_message_carol_t0.clone()).expect_failure(HTLCDestination::InvalidOnion); + do_pass_along_path(args); + + { + let downstream_id = 4; + let upstream_id = 2; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_malformed_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_malformed_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 2; + let upstream_id = 1; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 1; + let upstream_id = 0; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, false, false); + } + { + let payment_failed_conditions = PaymentFailedConditions::new() + .expected_htlc_error_data(LocalHTLCFailureReason::TemporaryTrampolineFailure, &[0; 0]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + } + } + Some(TrampolineForwardFailureScenario::InvalidRecipientOnion) => { + let route_alice_t0: &[&Node] = &[&nodes[1], &nodes[2], &nodes[4]]; + pass_along_path(&nodes[0], route_alice_t0, amt_msat, payment_hash.clone(), + None, first_message_event.clone(), false, Some(payment_preimage)); + + check_added_monitors!(&nodes[4], 1); + let mut events = nodes[4].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + let mut update_message_t0_dave = remove_first_msg_event_to_node(&nodes[3].node.get_our_node_id(), &mut events); + { + let mut update_message = match update_message_t0_dave { + 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| { + // corrupt the onion packet + msg.onion_routing_packet.hmac = [1; 32]; + }); + } + + let route_to_dave: &[&Node] = &[&nodes[3]]; + let args = PassAlongPathArgs::new(&nodes[4], route_to_dave, amt_msat, payment_hash, update_message_t0_dave.clone()).expect_failure(HTLCDestination::InvalidOnion); + do_pass_along_path(args); + + { + let downstream_id = 3; + let upstream_id = 4; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_malformed_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_malformed_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 4; + let upstream_id = 2; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 2; + let upstream_id = 1; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 1; + let upstream_id = 0; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, false, false); + } + { + let payment_failed_conditions = PaymentFailedConditions::new() + .expected_htlc_error_data(LocalHTLCFailureReason::TemporaryTrampolineFailure, &[0; 0]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + } + } + } +} + +#[test] +fn test_unblinded_trampoline_forward() { + do_test_unblinded_trampoline_forward(None); + do_test_unblinded_trampoline_forward(Some(TrampolineForwardFailureScenario::NoRoute)); + do_test_unblinded_trampoline_forward(Some(TrampolineForwardFailureScenario::InvalidInterTrampolineOnion)); + do_test_unblinded_trampoline_forward(Some(TrampolineForwardFailureScenario::InvalidRecipientOnion)); +} + +fn do_test_blinded_trampoline_forward(failure_scenario: Option) { + // Simulate a payment of A (0) -> B (1) -> C(Trampoline (blinded forward)) (2) -> D(Trampoline(blinded receive)) (3) + // trampoline hops C -> T0 (4) -> D + // make it fail at B, then at C's outer onion, then at C's inner onion + const TOTAL_NODE_COUNT: usize = 5; + 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); + if failure_scenario != Some(TrampolineForwardFailureScenario::NoRoute) { + let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 1_000_000, 0); + } + let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 4, 3, 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 bob_node_id = nodes[1].node().get_our_node_id(); + let carol_node_id = nodes[2].node().get_our_node_id(); + let dave_node_id = nodes[3].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[3], Some(amt_msat), None); + + let alice_carol_trampoline_shared_secret = secret_from_hex("a0f4b8d7b6c2d0ffdfaf718f76e9decaef4d9fb38a8c4addb95c4007cc3eee03"); + let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &alice_carol_trampoline_shared_secret); + + let forwarding_tlvs = blinded_path::payment::TrampolineForwardTlvs { + next_trampoline: dave_node_id, + payment_relay: PaymentRelay { + cltv_expiry_delta: 224, + fee_proportional_millionths: 0, + fee_base_msat: 2000, + }, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: amt_msat + }, + features: BlindedHopFeatures::empty(), + next_blinding_override: None, + }; + let carol_unblinded_tlvs = forwarding_tlvs.encode(); + + 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[3].keys_manager.get_inbound_payment_key(); + let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); + let dave_unblinded_tlvs = payee_tlvs.encode(); + + let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs)), (dave_node_id, WithoutLength(&dave_unblinded_tlvs))]; + let blinded_hops = blinded_path::utils::construct_blinded_hops( + &secp_ctx, path.into_iter(), &alice_carol_trampoline_shared_secret, + ).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, // forwarding fee to Carol + 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: 2000, // fee for the usage of the entire blinded path, including Trampoline + 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: 176, // let her cook + }, + ], + hops: 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); + + match failure_scenario { + None => { + pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[4], &nodes[3]]], amt_msat, payment_hash, payment_secret); + claim_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[4], &nodes[3]], payment_preimage); + }, + Some(TrampolineForwardFailureScenario::NoRoute) => { + 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 = &[&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::FailedTrampolineForward { requested_next_node_id: dave_node_id, forward_scid: None }); + do_pass_along_path(args); + + { + let downstream_id = 2; + let upstream_id = 1; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 1; + let upstream_id = 0; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, false, false); + } + { + let payment_failed_conditions = PaymentFailedConditions::new() + .expected_htlc_error_data(LocalHTLCFailureReason::InvalidOnionBlinding, &[0; 32]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + } + } + Some(TrampolineForwardFailureScenario::InvalidInterTrampolineOnion) => { + 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_alice_carol: &[&Node] = &[&nodes[1], &nodes[2]]; + pass_along_path(&nodes[0], route_alice_carol, amt_msat, payment_hash.clone(), + None, first_message_event.clone(), false, Some(payment_preimage)); + + check_added_monitors!(&nodes[2], 1); + let mut events = nodes[2].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + let mut update_message_carol_t0 = remove_first_msg_event_to_node(&nodes[4].node.get_our_node_id(), &mut events); + { + let mut update_message = match update_message_carol_t0 { + 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| { + // corrupt the onion packet + msg.onion_routing_packet.hmac = [1; 32]; + }); + } + + let route_carol_t0: &[&Node] = &[&nodes[4]]; + let args = PassAlongPathArgs::new(&nodes[2], route_carol_t0, amt_msat, payment_hash, update_message_carol_t0.clone()).expect_failure(HTLCDestination::InvalidOnion); + do_pass_along_path(args); + + { + let downstream_id = 4; + let upstream_id = 2; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_malformed_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_malformed_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 2; + let upstream_id = 1; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 1; + let upstream_id = 0; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, false, false); + } + { + let payment_failed_conditions = PaymentFailedConditions::new() + .expected_htlc_error_data(LocalHTLCFailureReason::InvalidOnionBlinding, &[0; 32]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + } + } + Some(TrampolineForwardFailureScenario::InvalidRecipientOnion) => { + 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_alice_t0: &[&Node] = &[&nodes[1], &nodes[2], &nodes[4]]; + pass_along_path(&nodes[0], route_alice_t0, amt_msat, payment_hash.clone(), + None, first_message_event.clone(), false, Some(payment_preimage)); + + check_added_monitors!(&nodes[4], 1); + let mut events = nodes[4].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + let mut update_message_t0_dave = remove_first_msg_event_to_node(&nodes[3].node.get_our_node_id(), &mut events); + { + let mut update_message = match update_message_t0_dave { + 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| { + // corrupt the onion packet + msg.onion_routing_packet.hmac = [1; 32]; + }); + } + + let route_to_dave: &[&Node] = &[&nodes[3]]; + let args = PassAlongPathArgs::new(&nodes[4], route_to_dave, amt_msat, payment_hash, update_message_t0_dave.clone()).expect_failure(HTLCDestination::InvalidOnion); + do_pass_along_path(args); + + { + let downstream_id = 3; + let upstream_id = 4; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_malformed_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_malformed_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 4; + let upstream_id = 2; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 2; + let upstream_id = 1; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 1; + let upstream_id = 0; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, false, false); + } + { + let payment_failed_conditions = PaymentFailedConditions::new() + .expected_htlc_error_data(LocalHTLCFailureReason::InvalidOnionBlinding, &[0; 32]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + } + } + } + + +} + +#[test] +fn test_blinded_trampoline_forward() { + do_test_blinded_trampoline_forward(None); + do_test_blinded_trampoline_forward(Some(TrampolineForwardFailureScenario::NoRoute)); + do_test_blinded_trampoline_forward(Some(TrampolineForwardFailureScenario::InvalidInterTrampolineOnion)); + do_test_blinded_trampoline_forward(Some(TrampolineForwardFailureScenario::InvalidRecipientOnion)); +} + +#[test] +fn test_trampoline_mpp_rejection() { + // Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2) -> D(Trampoline(receive)) (3) + // MPP segment A: trampoline hops C -> T0 (4) -> D + // MPP segment B: trampoline hops C -> T1 (5) -> D + const TOTAL_NODE_COUNT: usize = 6; + 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 large_channel_size = 21_000; + let small_channel_size = 15_000; + let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, large_channel_size, 0); + let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, large_channel_size, 0); + + // inter-Trampoline-path A + let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 2, 4, small_channel_size, 0); + let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 4, 3, small_channel_size, 0); + + // inter-Trampoline-path B + let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 2, 5, small_channel_size, 0); + let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 5, 3, small_channel_size, 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 dave_node_id = nodes[3].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 = 2_000_000; // send 2k satoshis over channels allowing 20k satoshis + let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[3], 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, // forwarding fee to Carol + 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: 3000, // fee for the usage of the entire blinded path, including Trampoline + 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: 176, // let her cook + }, + + // Dave (recipient) + TrampolineHop { + pubkey: dave_node_id, + node_features: Features::empty(), + fee_msat: 0, // no need to charge a fee as the recipient + cltv_expiry_delta: 24, + }, + ], + hops: vec![ + // Dave's blinded node id + BlindedHop { + blinded_node_id: pubkey_from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be"), + encrypted_payload: bytes_from_hex("0ccf3c8a58deaa603f657ee2a5ed9d604eb5c8ca1e5f801989afa8f3ea6d789bbdde2c7e7a1ef9ca8c38d2c54760febad8446d3f273ddb537569ef56613846ccd3aba78a"), + } + ], + 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(); + + 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: 96, + }); + + let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys(&secp_ctx, &route.paths[0].blinded_tail.as_ref().unwrap(), &trampoline_secret_key); + 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); + 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 unforked_route: &[&Node] = &[&nodes[1], &nodes[2]]; + pass_along_path(&nodes[0], unforked_route, amt_msat, payment_hash.clone(), + None, first_message_event.clone(), false, Some(payment_preimage)); + + check_added_monitors!(&nodes[2], 2); + let mut events = nodes[2].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 2); + + let intermediate_message_event_a = remove_first_msg_event_to_node(&nodes[4].node.get_our_node_id(), &mut events); + let intermediate_message_event_b = remove_first_msg_event_to_node(&nodes[5].node.get_our_node_id(), &mut events); + + let route_via_t0: &[&Node] = &[&nodes[4], &nodes[3]]; + let route_via_t1: &[&Node] = &[&nodes[5], &nodes[3]]; + let args_a = PassAlongPathArgs::new(&nodes[2], route_via_t0, amt_msat, payment_hash, intermediate_message_event_a.clone()) + .expect_failure(HTLCDestination::FailedPayment { payment_hash }); + let args_b = PassAlongPathArgs::new(&nodes[2], route_via_t1, amt_msat, payment_hash, intermediate_message_event_b.clone()) + .expect_failure(HTLCDestination::FailedPayment { payment_hash }); + + { + do_pass_along_path(args_a); + { + let downstream_id = 3; + let upstream_id = 4; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 4; + let upstream_id = 2; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 2; + let upstream_id = 1; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 1; + let upstream_id = 0; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, false, false); + } + { + let payment_failed_conditions = PaymentFailedConditions::new() + .expected_htlc_error_data(LocalHTLCFailureReason::TemporaryTrampolineFailure, &[0; 0]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + } + } + + { + do_pass_along_path(args_b); + { + let downstream_id = 3; + let upstream_id = 5; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, true, false); + } + { + let downstream_id = 5; + let upstream_id = 2; + let unblinded_node_updates = get_htlc_update_msgs!(nodes[downstream_id], nodes[upstream_id].node.get_our_node_id()); + nodes[upstream_id].node.handle_update_fail_htlc( + nodes[downstream_id].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] + ); + do_commitment_signed_dance(&nodes[upstream_id], &nodes[downstream_id], &unblinded_node_updates.commitment_signed, false, false); + } + { + let events = nodes[2].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + match events[0] { + Event::HTLCHandlingFailed { .. } => {} + _ => panic!("unexpected event") + }; + } + } +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 819cb5554c4..750fb9d0fb3 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -54,14 +54,13 @@ use crate::ln::channel::{self, Channel, ChannelError, ChannelUpdateStatus, Funde use crate::ln::channel::PendingV2Channel; use crate::ln::channel_state::ChannelDetails; use crate::types::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; -#[cfg(any(feature = "_test_utils", test))] use crate::types::features::Bolt11InvoiceFeatures; -use crate::routing::router::{BlindedTail, FixedRouter, InFlightHtlcs, Path, Payee, PaymentParameters, Route, RouteHop, RouteParameters, RouteParametersConfig, Router}; +use crate::routing::router::{BlindedTail, DEFAULT_MAX_PATH_COUNT, FixedRouter, InFlightHtlcs, MAX_PATH_LENGTH_ESTIMATE, Path, Payee, PaymentParameters, Route, RouteHop, RouteParameters, RouteParametersConfig, Router}; use crate::ln::onion_payment::{check_incoming_htlc_cltv, create_recv_pending_htlc_info, create_fwd_pending_htlc_info, decode_incoming_update_add_htlc_onion, HopConnector, InboundHTLCErr, invalid_payment_err_data, NextPacketDetails}; use crate::ln::msgs; use crate::ln::onion_utils::{self}; use crate::ln::onion_utils::{HTLCFailReason, LocalHTLCFailureReason}; -use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, DecodeError, LightningError, MessageSendEvent}; +use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, DecodeError, FinalOnionHopData, LightningError, MessageSendEvent}; #[cfg(test)] use crate::ln::outbound_payment; use crate::ln::outbound_payment::{Bolt11PaymentError, OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration}; @@ -5864,16 +5863,23 @@ where // Now process the HTLC on the outgoing channel if it's a forward. if let Some(next_packet_details) = next_packet_details_opt.as_ref() { - if let Err((err, reason)) = self.can_forward_htlc( - &update_add_htlc, next_packet_details - ) { - let htlc_fail = self.htlc_failure_from_update_add_err( - &update_add_htlc, &incoming_counterparty_node_id, err, reason, - is_intro_node_blinded_forward, &shared_secret, - ); - let htlc_destination = get_failed_htlc_destination(outgoing_scid_opt, update_add_htlc.payment_hash); - htlc_fails.push((htlc_fail, htlc_destination)); - continue; + match next_packet_details.outgoing_connector { + HopConnector::ShortChannelId(_) => { + if let Err((err, reason)) = self.can_forward_htlc( + &update_add_htlc, next_packet_details + ) { + let htlc_fail = self.htlc_failure_from_update_add_err( + &update_add_htlc, &incoming_counterparty_node_id, err, reason, + is_intro_node_blinded_forward, &shared_secret, + ); + let htlc_destination = get_failed_htlc_destination(outgoing_scid_opt, update_add_htlc.payment_hash); + htlc_fails.push((htlc_fail, htlc_destination)); + continue; + } + } + HopConnector::Trampoline(_) => { + // we don't know the next scid yet, so there is nothing to check + } } } @@ -6219,6 +6225,269 @@ where } else { 'next_forwardable_htlc: for forward_info in pending_forwards.drain(..) { match forward_info { + HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo { + prev_short_channel_id, prev_htlc_id, prev_channel_id, prev_funding_outpoint, + prev_user_channel_id, prev_counterparty_node_id, forward_info: PendingHTLCInfo { + incoming_shared_secret: incoming_outer_shared_secret, payment_hash, outgoing_amt_msat, outgoing_cltv_value, + routing: PendingHTLCRouting::TrampolineForward { + ref onion_packet, blinded, incoming_cltv_expiry, incoming_shared_secret: incoming_trampoline_shared_secret, node_id: next_node_id, .. + }, skimmed_fee_msat, incoming_amt_msat + }, + }) => { + let htlc_source = HTLCSource::TrampolineForward { + // dummy value + session_priv: SecretKey::from_slice(&self.entropy_source.get_secure_random_bytes()).unwrap(), + previous_hop_data: vec![HTLCPreviousHopData { + short_channel_id: prev_short_channel_id, + user_channel_id: Some(prev_user_channel_id), + counterparty_node_id: prev_counterparty_node_id, + channel_id: prev_channel_id, + outpoint: prev_funding_outpoint, + htlc_id: prev_htlc_id, + incoming_packet_shared_secret: incoming_outer_shared_secret, + // Phantom payments are only PendingHTLCRouting::Receive. + phantom_shared_secret: None, + blinded_failure: blinded.map(|b| b.failure), + cltv_expiry: Some(incoming_cltv_expiry), + }], + incoming_trampoline_shared_secret, + hops: Vec::new(), + }; + + let mut push_trampoline_forwarding_failure = |msg: String, htlc_source: HTLCSource, forward_scid: Option, reason: LocalHTLCFailureReason, err_data: Vec| { + let logger = WithContext::from(&self.logger, Some(next_node_id), Some(prev_channel_id), Some(payment_hash)); + log_info!(logger, "Failed to forward incoming Trampoline HTLC: {}", msg); + + failed_forwards.push((htlc_source, payment_hash, + HTLCFailReason::reason(reason, err_data), + HTLCDestination::FailedTrampolineForward { requested_next_node_id: next_node_id, forward_scid } + )); + }; + + let next_blinding_point = blinded.and_then(|b| { + b.next_blinding_override.or_else(|| { + let encrypted_tlvs_ss = self.node_signer.ecdh( + Recipient::Node, &b.inbound_blinding_point, None + ).unwrap().secret_bytes(); + onion_utils::next_hop_pubkey( + &self.secp_ctx, b.inbound_blinding_point, &encrypted_tlvs_ss + ).ok() + }) + }); + + let incoming_amount = match incoming_amt_msat { + Some(amount) => amount, + None => { + push_trampoline_forwarding_failure(format!("Missing incoming amount to calculate routing parameters to next Trampoline hop {next_node_id}"), htlc_source, None, LocalHTLCFailureReason::TemporaryTrampolineFailure, Vec::new()); + continue; + } + }; + + let proportional_fee = self.default_configuration.channel_config.forwarding_fee_proportional_millionths as u64 * outgoing_amt_msat / 1_000_000; + let forwarding_fee = proportional_fee + self.default_configuration.channel_config.forwarding_fee_base_msat as u64; + let cltv_expiry_delta = incoming_cltv_expiry - outgoing_cltv_value; + + let max_total_routing_fee_msat = match incoming_amount.checked_sub(forwarding_fee + outgoing_amt_msat) { + Some(amount) => amount, + None => { + let mut data = Vec::new(); + self.default_configuration.channel_config.forwarding_fee_base_msat.write(&mut data); + self.default_configuration.channel_config.forwarding_fee_proportional_millionths.write(&mut data); + // todo: error handling + u16::try_from(cltv_expiry_delta).unwrap().write(&mut data); + push_trampoline_forwarding_failure(format!("Insufficient fee to forward to the next Trampoline hop {next_node_id}"), htlc_source, None, LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient, Vec::new()); + continue; + } + }; + + let usable_channels: Vec = self.list_usable_channels(); + + // assume any Trampoline node supports MPP + let mut recipient_features = Bolt11InvoiceFeatures::empty(); + recipient_features.set_basic_mpp_optional(); + + let route = match self.router.find_route( + &self.node_signer.get_node_id(Recipient::Node).unwrap(), + &RouteParameters { + payment_params: PaymentParameters { + payee: Payee::Clear { + node_id: next_node_id, + route_hints: vec![], + features: Some(recipient_features), + final_cltv_expiry_delta: 0, + }, + expiry_time: None, + max_total_cltv_expiry_delta: cltv_expiry_delta, + max_path_count: DEFAULT_MAX_PATH_COUNT, + max_path_length: MAX_PATH_LENGTH_ESTIMATE / 2, + max_channel_saturation_power_of_half: 2, + previously_failed_channels: vec![], + previously_failed_blinded_path_idxs: vec![], + }, + final_value_msat: outgoing_amt_msat, + max_total_routing_fee_msat: Some(max_total_routing_fee_msat), + }, + Some(&usable_channels.iter().collect::>()), + self.compute_inflight_htlcs() + ) { + Ok(route) => route, + Err(_) => { + push_trampoline_forwarding_failure(format!("Could not find route to next Trampoline hop {next_node_id}"), htlc_source, None, LocalHTLCFailureReason::TemporaryTrampolineFailure, Vec::new()); + continue; + } + }; + + let inter_trampoline_payment_secret = PaymentSecret(self.entropy_source.get_secure_random_bytes()); + for current_path in route.paths { + let inter_trampoline_session_priv = SecretKey::from_slice(&self.entropy_source.get_secure_random_bytes()).unwrap(); + let inter_trampoline_hops = current_path.hops.clone(); + let mut current_htlc_source = htlc_source.clone(); + if let HTLCSource::TrampolineForward { ref mut session_priv, ref mut hops, .. } = current_htlc_source { + *session_priv = inter_trampoline_session_priv; + *hops = inter_trampoline_hops.clone(); + }; + + let outgoing_scid = match inter_trampoline_hops.first() { + Some(hop) => hop.short_channel_id, + None => { + push_trampoline_forwarding_failure(format!("Could not find route to next Trampoline hop {next_node_id}"), current_htlc_source, None, LocalHTLCFailureReason::UnknownNextTrampoline, Vec::new()); + break; + } + }; + + let chan_info_opt = self.short_to_chan_info.read().unwrap().get(&outgoing_scid).cloned(); + let (counterparty_node_id, forward_chan_id) = match chan_info_opt { + Some((cp_id, chan_id)) => (cp_id, chan_id), + None => { + push_trampoline_forwarding_failure(format!("Could not find forwarding channel {outgoing_scid} to route to next Trampoline hop {next_node_id}"), current_htlc_source, Some(outgoing_scid), LocalHTLCFailureReason::TemporaryTrampolineFailure, Vec::new()); + break; + } + }; + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex_opt = per_peer_state.get(&counterparty_node_id); + if peer_state_mutex_opt.is_none() { + push_trampoline_forwarding_failure(format!("Could not to route to next Trampoline hop {next_node_id} via forwarding channel {outgoing_scid}"), current_htlc_source, Some(outgoing_scid), LocalHTLCFailureReason::TemporaryTrampolineFailure, Vec::new()); + break; + } + let mut peer_state_lock = peer_state_mutex_opt.unwrap().lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + let (outer_onion_packet, outer_value_msat, outer_cltv) = { + let path = Path { + hops: inter_trampoline_hops, + blinded_tail: None, + }; + let recipient_onion = RecipientOnionFields::spontaneous_empty(); + let (mut onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + &path, + current_path.final_value_msat(), + &recipient_onion, + outgoing_cltv_value, + &None, + None, + None, + ).unwrap(); + + let multipath_trampoline_data = Some(FinalOnionHopData { payment_secret: inter_trampoline_payment_secret, total_msat: outgoing_amt_msat }); + if let Some(last_payload) = onion_payloads.last_mut() { + match last_payload { + msgs::OutboundOnionPayload::Receive { sender_intended_htlc_amt_msat, cltv_expiry_height, .. } => { + *last_payload = match next_blinding_point { + None => msgs::OutboundOnionPayload::TrampolineEntrypoint { + amt_to_forward: *sender_intended_htlc_amt_msat, + outgoing_cltv_value: *cltv_expiry_height, + multipath_trampoline_data, + trampoline_packet: onion_packet.clone(), + }, + Some(blinding_point) => msgs::OutboundOnionPayload::BlindedTrampolineEntrypoint { + amt_to_forward: *sender_intended_htlc_amt_msat, + outgoing_cltv_value: *cltv_expiry_height, + multipath_trampoline_data, + trampoline_packet: onion_packet.clone(), + current_path_key: blinding_point, + } + }; + } + _ => { + unreachable!("Last element must always initially be of type Receive."); + } + } + } + + let onion_keys = onion_utils::construct_onion_keys(&self.secp_ctx, &path, &inter_trampoline_session_priv); + let outer_onion_prng_seed = self.entropy_source.get_secure_random_bytes(); + let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, outer_onion_prng_seed, &payment_hash).unwrap(); + + (onion_packet, htlc_msat, htlc_cltv) + }; + + // Forward the HTLC over the most appropriate channel with the corresponding peer, + // applying non-strict forwarding. + // The channel with the least amount of outbound liquidity will be used to maximize the + // probability of being able to successfully forward a subsequent HTLC. + let maybe_optimal_channel = peer_state.channel_by_id.values_mut() + .filter_map(Channel::as_funded_mut) + .filter_map(|chan| { + let balances = chan.get_available_balances(&self.fee_estimator); + if outer_value_msat <= balances.next_outbound_htlc_limit_msat && + outer_value_msat >= balances.next_outbound_htlc_minimum_msat && + chan.context.is_usable() { + Some((chan, balances)) + } else { + None + } + }) + .min_by_key(|(_, balances)| balances.next_outbound_htlc_limit_msat).map(|(c, _)| c); + let optimal_channel = match maybe_optimal_channel { + Some(chan) => chan, + None => { + // Fall back to the specified channel to return an appropriate error. + if let Some(chan) = peer_state.channel_by_id + .get_mut(&forward_chan_id) + .and_then(Channel::as_funded_mut) + { + chan + } else { + push_trampoline_forwarding_failure(format!("Could not to route to next Trampoline hop {next_node_id} via forwarding channel {outgoing_scid}"), current_htlc_source, Some(outgoing_scid), LocalHTLCFailureReason::TemporaryTrampolineFailure, Vec::new()); + break; + } + } + }; + + let logger = WithChannelContext::from(&self.logger, &optimal_channel.context, Some(payment_hash)); + let channel_description = if optimal_channel.context.get_short_channel_id() == Some(short_chan_id) { + "specified" + } else { + "alternate" + }; + log_trace!(logger, "Forwarding HTLC from SCID {} with payment_hash {} and next hop SCID {} over {} channel {} with corresponding peer {}", + prev_short_channel_id, &payment_hash, short_chan_id, channel_description, optimal_channel.context.channel_id(), &counterparty_node_id); + // Note that for inter-Trampoline forwards, we never add the blinding point to the UpdateAddHTLC message + if let Err((reason, msg)) = optimal_channel.queue_add_htlc(outer_value_msat, + payment_hash, outer_cltv, current_htlc_source.clone(), + outer_onion_packet.clone(), skimmed_fee_msat, None, &self.fee_estimator, + &&logger) + { + log_trace!(logger, "Failed to forward HTLC with payment_hash {} to peer {}: {}", &payment_hash, &counterparty_node_id, msg); + + if let Some(chan) = peer_state.channel_by_id + .get_mut(&forward_chan_id) + .and_then(Channel::as_funded_mut) + { + let data = self.get_htlc_inbound_temp_fail_data(reason); + failed_forwards.push((current_htlc_source, payment_hash, + HTLCFailReason::reason(reason, data), + HTLCDestination::NextHopChannel { node_id: Some(chan.context.get_counterparty_node_id()), channel_id: forward_chan_id } + )); + break; + } else { + push_trampoline_forwarding_failure(format!("Could not to route to next Trampoline hop {next_node_id} via forwarding channel {outgoing_scid}"), current_htlc_source, Some(outgoing_scid), LocalHTLCFailureReason::TemporaryTrampolineFailure, Vec::new()); + break; + } + } + } + () + }, HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo { prev_short_channel_id, prev_htlc_id, prev_channel_id, prev_funding_outpoint, prev_user_channel_id, prev_counterparty_node_id, forward_info: PendingHTLCInfo { diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 187028395b5..89fdb089737 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2027,8 +2027,8 @@ impl HTLCFailReason { // failures here, but that would be insufficient as find_route // generally ignores its view of our own channels as we provide them via // ChannelDetails. - if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source { - DecodedOnionFailure { + match htlc_source { + HTLCSource::OutboundRoute { ref path, .. } => DecodedOnionFailure { network_update: None, payment_failed_permanently: false, short_channel_id: Some(path.hops[0].short_channel_id), @@ -2038,9 +2038,19 @@ impl HTLCFailReason { onion_error_code: Some(failure_reason.failure_code()), #[cfg(any(test, feature = "_test_utils"))] onion_error_data: Some(data.clone()), - } - } else { - unreachable!(); + }, + HTLCSource::TrampolineForward { ref hops, .. } => DecodedOnionFailure { + network_update: None, + payment_failed_permanently: false, + short_channel_id: hops.first().map(|h| h.short_channel_id), + failed_within_blinded_path: false, + hold_times: Vec::new(), + #[cfg(any(test, feature = "_test_utils"))] + onion_error_code: Some(failure_reason.failure_code()), + #[cfg(any(test, feature = "_test_utils"))] + onion_error_data: Some(data.clone()), + }, + _ => unreachable!(), } }, } diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 14d06355bc0..0977283a3b3 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -1068,7 +1068,10 @@ impl PaymentParameters { found_blinded_tail = true; } } - debug_assert!(found_blinded_tail); + if failed_blinded_tail.trampoline_hops.is_empty() { + // do not mandate path hints when paying using blinded Trampoline hops + debug_assert!(found_blinded_tail); + } } }