From 50f826ff5a9ae44ed0d93768191bf47af31ec81a Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 17 Apr 2025 11:04:31 -0700 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 05/11] 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 06/11] 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 07/11] 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 b2986959521061bcd979d7c7e5fee6d2c85b6eef Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 17 Apr 2025 19:27:58 -0700 Subject: [PATCH 08/11] f: htlc source thingy --- lightning/src/ln/onion_utils.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) 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!(), } }, } From 3963223d3530314c867b357562c2ea838ba065e1 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Wed, 9 Apr 2025 17:28:03 -0700 Subject: [PATCH 09/11] 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 3a1b4805a752fb37e233ca45bebe36e9c7e75b64 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 17 Apr 2025 19:28:03 -0700 Subject: [PATCH 10/11] 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/msgs.rs | 1 - lightning/src/routing/router.rs | 5 +- 4 files changed, 1196 insertions(+), 15 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/msgs.rs b/lightning/src/ln/msgs.rs index c27db4a55b4..ee1ddedde48 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -2189,7 +2189,6 @@ mod fuzzy_internal_msgs { /// This is used for Trampoline hops that are not the blinded path intro hop. /// We would only ever construct this variant when we are a Trampoline node forwarding a /// payment along a blinded path. - #[allow(unused)] BlindedTrampolineEntrypoint { amt_to_forward: u64, outgoing_cltv_value: u32, 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); + } } } From 43d2661648223d0b60ece489286aa6cfcced9b98 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Mon, 19 May 2025 09:20:35 -0700 Subject: [PATCH 11/11] wip: use outbound payments instead --- lightning/src/ln/blinded_payment_tests.rs | 6 +- lightning/src/ln/channelmanager.rs | 129 ++++++++++++---- lightning/src/ln/onion_payment.rs | 13 ++ lightning/src/ln/onion_utils.rs | 96 ++++++++++-- lightning/src/ln/outbound_payment.rs | 179 +++++++++++++++++----- 5 files changed, 341 insertions(+), 82 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index a1d20f101a8..5c3cecc35d8 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2835,9 +2835,9 @@ fn do_test_unblinded_trampoline_forward(failure_scenario: Option) { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 750fb9d0fb3..01e80cc5710 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -63,7 +63,7 @@ use crate::ln::onion_utils::{HTLCFailReason, LocalHTLCFailureReason}; 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}; +use crate::ln::outbound_payment::{Bolt11PaymentError, NextTrampolineHopInfo, OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, StaleExpiration, TrampolineForwardInfo}; use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice}; use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestBuilder}; @@ -670,7 +670,7 @@ impl_writeable_tlv_based_enum!(SentHTLCId, }, (4, TrampolineForward) => { (0, session_priv, required), - (2, previous_hop_data, required_vec), + (2, previous_hop_ids, required_vec), }, ); @@ -4667,24 +4667,35 @@ where let _lck = self.total_consistency_lock.read().unwrap(); self.send_payment_along_path(SendAlongPathArgs { path, payment_hash, recipient_onion: &recipient_onion, total_value, - cur_height, payment_id, keysend_preimage, invoice_request: None, session_priv_bytes + cur_height, payment_id, keysend_preimage, invoice_request: None, session_priv_bytes, + trampoline_forward_info: None }) } fn send_payment_along_path(&self, args: SendAlongPathArgs) -> Result<(), APIError> { let SendAlongPathArgs { path, payment_hash, recipient_onion, total_value, cur_height, payment_id, keysend_preimage, - invoice_request, session_priv_bytes + invoice_request, session_priv_bytes, trampoline_forward_info } = args; // The top-level caller should hold the total_consistency_lock read lock. debug_assert!(self.total_consistency_lock.try_write().is_err()); let prng_seed = self.entropy_source.get_secure_random_bytes(); let session_priv = SecretKey::from_slice(&session_priv_bytes[..]).expect("RNG is busted"); - let (onion_packet, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion( - &self.secp_ctx, &path, &session_priv, total_value, recipient_onion, cur_height, - payment_hash, keysend_preimage, invoice_request, prng_seed - ).map_err(|e| { + let onion_result = if let Some(trampoline_forward_info) = trampoline_forward_info { + // todo: ensure inter-Trampoline payment secret is always available for Trampoline forwards + onion_utils::create_trampoline_forward_onion( + &self.secp_ctx, &path, &session_priv, total_value, recipient_onion.payment_secret.unwrap(), cur_height, + payment_hash, keysend_preimage, &trampoline_forward_info.next_hop_info, prng_seed + ) + } else { + onion_utils::create_payment_onion( + &self.secp_ctx, &path, &session_priv, total_value, recipient_onion, cur_height, + payment_hash, keysend_preimage, invoice_request, prng_seed + ) + }; + + let (onion_packet, htlc_msat, htlc_cltv) = onion_result.map_err(|e| { let logger = WithContext::from(&self.logger, Some(path.hops.first().unwrap().pubkey), None, Some(*payment_hash)); log_error!(logger, "Failed to build an onion for path for payment hash {}", payment_hash); e @@ -4718,13 +4729,25 @@ where } let funding_txo = chan.funding.get_funding_txo().unwrap(); let logger = WithChannelContext::from(&self.logger, &chan.context, Some(*payment_hash)); - let send_res = chan.send_htlc_and_commit(htlc_msat, payment_hash.clone(), - htlc_cltv, HTLCSource::OutboundRoute { + + let htlc_source = match trampoline_forward_info { + None => HTLCSource::OutboundRoute { path: path.clone(), session_priv: session_priv.clone(), first_hop_htlc_msat: htlc_msat, payment_id, - }, onion_packet, None, &self.fee_estimator, &&logger); + }, + Some(trampoline_forward_info) => HTLCSource::TrampolineForward { + previous_hop_data: trampoline_forward_info.previous_hop_data.clone(), + // todo: fix + incoming_trampoline_shared_secret: [0; 32], + session_priv: session_priv.clone(), + hops: path.clone().hops, + } + }; + + let send_res = chan.send_htlc_and_commit(htlc_msat, payment_hash.clone(), + htlc_cltv, htlc_source, onion_packet, None, &self.fee_estimator, &&logger); match break_channel_entry!(self, peer_state, send_res, chan_entry) { Some(monitor_update) => { match handle_new_monitor_update!(self, funding_txo, monitor_update, peer_state_lock, peer_state, per_peer_state, chan) { @@ -6300,33 +6323,74 @@ where } }; - 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![], + println!("PATH CONSTRUCTION CLTV: {} -> {} (delta: {})", incoming_cltv_expiry, outgoing_cltv_value, cltv_expiry_delta); + + let route_parameters = RouteParameters { + payment_params: PaymentParameters { + payee: Payee::Clear { + node_id: next_node_id, + route_hints: vec![], + features: Some(recipient_features), + final_cltv_expiry_delta: 4, }, - final_value_msat: outgoing_amt_msat, - max_total_routing_fee_msat: Some(max_total_routing_fee_msat), + 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), + }; + + self.pending_outbound_payments.send_payment_for_trampoline_forward( + PaymentId(payment_hash.0), + payment_hash, + TrampolineForwardInfo { + next_hop_info: NextTrampolineHopInfo { + onion_packet: onion_packet.clone(), + blinding_point: next_blinding_point, + }, + 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), + }], + }, + Retry::Attempts(3), + route_parameters.clone(), + &self.router, + self.list_usable_channels(), + || self.compute_inflight_htlcs(), + &self.entropy_source, + &self.node_signer, + self.current_best_block().height, + &self.logger, + &self.pending_events, + |args| self.send_payment_along_path(args) + ); + + continue; + + let usable_channels: Vec = self.list_usable_channels(); + + let route = match self.router.find_route( + &self.node_signer.get_node_id(Recipient::Node).unwrap(), + &route_parameters, Some(&usable_channels.iter().collect::>()), self.compute_inflight_htlcs() ) { @@ -6414,6 +6478,7 @@ where } } + 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(); diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index e523a0f41dc..322d9166fa0 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -71,6 +71,13 @@ fn check_blinded_forward( } fn check_trampoline_onion_constraints(outer_hop_data: &msgs::InboundTrampolineEntrypointPayload, trampoline_cltv_value: u32, trampoline_amount: u64) -> Result<(), LocalHTLCFailureReason> { + println!("outer amt_to_forward: {}", outer_hop_data.amt_to_forward); + println!("outer CLTV: {}", outer_hop_data.outgoing_cltv_value); + println!("inner amt_to_forward: {}", trampoline_amount); + println!("inner CLTV: {}", trampoline_cltv_value); + + // panic!("ENOUGH!"); + if outer_hop_data.outgoing_cltv_value < trampoline_cltv_value { return Err(LocalHTLCFailureReason::FinalIncorrectCLTVExpiry); } @@ -329,6 +336,12 @@ pub(super) fn create_recv_pending_htlc_info( } _ => unreachable!() } + + println!("outer amt_to_forward: {}", outer_hop_data.amt_to_forward); + println!("outer CLTV: {}", outer_hop_data.outgoing_cltv_value); + println!("inner amt_to_forward: {}", sender_intended_htlc_amt_msat); + println!("inner CLTV: {}", cltv_expiry_height); + // The Trampoline onion's amt and CLTV values cannot exceed the outer onion's InboundHTLCErr { reason, diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 89fdb089737..ac4ce6701f5 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -7,7 +7,7 @@ // You may not use this file except in accordance with one or both of these // licenses. -use super::msgs::OnionErrorPacket; +use super::msgs::{FinalOnionHopData, OnionErrorPacket}; use crate::blinded_path::BlindedHop; use crate::crypto::chacha20::ChaCha20; use crate::crypto::streams::ChaChaReader; @@ -37,7 +37,8 @@ use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; use crate::io::{Cursor, Read}; use core::ops::Deref; - +use types::payment::PaymentSecret; +use crate::ln::outbound_payment::{NextTrampolineHopInfo, TrampolineForwardInfo}; #[allow(unused_imports)] use crate::prelude::*; @@ -200,6 +201,7 @@ trait OnionPayload<'a, 'b> { fn new_trampoline_entry( total_msat: u64, amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, packet: msgs::TrampolineOnionPacket, + blinding_point: Option ) -> Result; } impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { @@ -249,15 +251,28 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { fn new_trampoline_entry( total_msat: u64, amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, packet: msgs::TrampolineOnionPacket, + blinding_point: Option ) -> Result { - Ok(Self::TrampolineEntrypoint { - amt_to_forward, - outgoing_cltv_value, - multipath_trampoline_data: recipient_onion - .payment_secret - .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }), - trampoline_packet: packet, - }) + if let Some(blinding_point) = blinding_point { + Ok(Self::BlindedTrampolineEntrypoint { + amt_to_forward, + outgoing_cltv_value, + multipath_trampoline_data: recipient_onion + .payment_secret + .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }), + trampoline_packet: packet, + current_path_key: blinding_point, + }) + } else { + Ok(Self::TrampolineEntrypoint { + amt_to_forward, + outgoing_cltv_value, + multipath_trampoline_data: recipient_onion + .payment_secret + .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }), + trampoline_packet: packet, + }) + } } } impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { @@ -301,6 +316,7 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { fn new_trampoline_entry( _total_msat: u64, _amt_to_forward: u64, _outgoing_cltv_value: u32, _recipient_onion: &'a RecipientOnionFields, _packet: msgs::TrampolineOnionPacket, + _blinding_point: Option ) -> Result { Err(APIError::InvalidRoute { err: "Trampoline onions cannot contain Trampoline entrypoints!".to_string(), @@ -447,6 +463,7 @@ pub(super) fn build_onion_payloads<'a>( if let Some(trampoline_packet) = trampoline_packet { return BlindedTailDetails::TrampolineEntry { trampoline_packet, + blinding_point: None, final_value_msat: bt.final_value_msat, }; } @@ -483,6 +500,7 @@ enum BlindedTailDetails<'a, I: Iterator> { }, TrampolineEntry { trampoline_packet: msgs::TrampolineOnionPacket, + blinding_point: Option, final_value_msat: u64, }, } @@ -557,6 +575,7 @@ where }, Some(BlindedTailDetails::TrampolineEntry { trampoline_packet, + blinding_point, final_value_msat, }) => { cur_value_msat += final_value_msat; @@ -568,6 +587,7 @@ where cur_cltv, &recipient_onion, trampoline_packet, + blinding_point )?, ); }, @@ -2453,6 +2473,60 @@ pub fn create_payment_onion( ) } +pub fn create_trampoline_forward_onion( + secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, total_msat: u64, + payment_secret: PaymentSecret, cur_block_height: u32, payment_hash: &PaymentHash, + keysend_preimage: &Option, trampoline_forward_info: &NextTrampolineHopInfo, + prng_seed: [u8; 32], +) -> Result<(msgs::OnionPacket, u64, u32), APIError> { + let recipient_onion = RecipientOnionFields::spontaneous_empty(); + let (mut onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( + &path, + path.final_value_msat(), + // we don't need a real recipient onion + &recipient_onion, + cur_block_height, + keysend_preimage, + None, + // TODO: find idiomatic way of that being considered without post-processing in this method + Some(trampoline_forward_info.onion_packet.clone()), + )?; + + let multipath_trampoline_data = Some(FinalOnionHopData { payment_secret, total_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 trampoline_forward_info.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: trampoline_forward_info.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: trampoline_forward_info.onion_packet.clone(), + current_path_key: blinding_point, + } + }; + } + _ => { + unreachable!("Last element must always initially be of type Receive."); + } + } + } + + let onion_keys = construct_onion_keys(&secp_ctx, &path, session_priv); + let onion_packet = + construct_onion_packet(onion_payloads, onion_keys, prng_seed, payment_hash) + .map_err(|_| APIError::InvalidRoute { + err: "Route size too large considering onion data".to_owned(), + })?; + Ok((onion_packet, htlc_msat, htlc_cltv)) +} + /// Build a payment onion, returning the first hop msat and cltv values as well. /// `cur_block_height` should be set to the best known block height + 1. pub(crate) fn create_payment_onion_internal( @@ -2460,7 +2534,7 @@ pub(crate) fn create_payment_onion_internal( recipient_onion: &RecipientOnionFields, cur_block_height: u32, payment_hash: &PaymentHash, keysend_preimage: &Option, invoice_request: Option<&InvoiceRequest>, prng_seed: [u8; 32], secondary_session_priv: Option, - secondary_prng_seed: Option<[u8; 32]>, + secondary_prng_seed: Option<[u8; 32]> ) -> Result<(msgs::OnionPacket, u64, u32), APIError> { let mut outer_total_msat = total_msat; let mut outer_starting_htlc_offset = cur_block_height; diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 8cab698a59a..dabc2a05bd4 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -11,13 +11,13 @@ use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::secp256k1::{self, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; use lightning_invoice::Bolt11Invoice; use crate::blinded_path::{IntroductionNode, NodeIdLookUp}; use crate::events::{self, PaidBolt12Invoice, PaymentFailureReason}; use crate::ln::channel_state::ChannelDetails; -use crate::ln::channelmanager::{EventCompletionAction, HTLCSource, PaymentId}; +use crate::ln::channelmanager::{EventCompletionAction, HTLCPreviousHopData, HTLCSource, PaymentId}; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; use crate::offers::invoice::Bolt12Invoice; @@ -41,7 +41,7 @@ use core::fmt::{self, Display, Formatter}; use core::ops::Deref; use core::sync::atomic::{AtomicBool, Ordering}; use core::time::Duration; - +use crate::ln::msgs::TrampolineOnionPacket; use crate::prelude::*; use crate::sync::Mutex; @@ -51,6 +51,33 @@ use crate::sync::Mutex; /// [`ChannelManager::timer_tick_occurred`]: crate::ln::channelmanager::ChannelManager::timer_tick_occurred pub(crate) const IDEMPOTENCY_TIMEOUT_TICKS: u8 = 7; +#[derive(Clone)] +pub(crate) struct NextTrampolineHopInfo { + /// The Trampoline packet to include for the next Trampoline hop + pub(crate) onion_packet: TrampolineOnionPacket, + /// If blinded, the current_path_key to set at the next Trampoline hop + pub(crate) blinding_point: Option, +} + +impl_writeable_tlv_based!(NextTrampolineHopInfo, { + (0, onion_packet, required), + (1, blinding_point, option), +}); + +#[derive(Clone)] +pub(crate) struct TrampolineForwardInfo { + /// Information necessary to construct the onion packet for the next Trampoline hop + pub(crate) next_hop_info: NextTrampolineHopInfo, + /// Upstream hop data to correctly propagate claims and errors back to the origin. If the + /// inbound payment was split up via MPP, the vector will contain an entry for each component. + pub(crate) previous_hop_data: Vec +} + +impl_writeable_tlv_based!(TrampolineForwardInfo, { + (0, next_hop_info, required), + (1, previous_hop_data, required_vec), +}); + /// Stores the session_priv for each part of a payment that is still pending. For versions 0.0.102 /// and later, also stores information for retrying the payment. pub(crate) enum PendingOutboundPayment { @@ -103,6 +130,7 @@ pub(crate) enum PendingOutboundPayment { payment_hash: PaymentHash, payment_secret: Option, payment_metadata: Option>, + trampoline_forwarding_data: Option, keysend_preimage: Option, invoice_request: Option, // Storing the BOLT 12 invoice here to allow Proof of Payment after @@ -797,6 +825,7 @@ pub(super) struct SendAlongPathArgs<'a> { pub payment_id: PaymentId, pub keysend_preimage: &'a Option, pub invoice_request: Option<&'a InvoiceRequest>, + pub trampoline_forward_info: Option<&'a TrampolineForwardInfo>, pub session_priv_bytes: [u8; 32], } @@ -1043,7 +1072,7 @@ impl OutboundPayments { PendingOutboundPayment::InvoiceReceived { .. } => { let (retryable_payment, onion_session_privs) = Self::create_pending_payment( payment_hash, recipient_onion.clone(), keysend_preimage, None, Some(bolt12_invoice), &route, - Some(retry_strategy), payment_params, entropy_source, best_block_height, + Some(retry_strategy), payment_params, entropy_source, best_block_height, None ); *entry.into_mut() = retryable_payment; onion_session_privs @@ -1054,7 +1083,7 @@ impl OutboundPayments { } else { unreachable!() }; let (retryable_payment, onion_session_privs) = Self::create_pending_payment( payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), Some(bolt12_invoice), &route, - Some(retry_strategy), payment_params, entropy_source, best_block_height + Some(retry_strategy), payment_params, entropy_source, best_block_height, None ); outbounds.insert(payment_id, retryable_payment); onion_session_privs @@ -1066,7 +1095,7 @@ impl OutboundPayments { core::mem::drop(outbounds); let result = self.pay_route_internal( - &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, payment_id, + &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, None, payment_id, Some(route_params.final_value_msat), &onion_session_privs, node_signer, best_block_height, &send_payment_along_path ); @@ -1230,6 +1259,7 @@ impl OutboundPayments { loop { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); let mut retry_id_route_params = None; + // let mut trampoline_next_hop_info for (pmt_id, pmt) in outbounds.iter_mut() { if pmt.is_auto_retryable_now() { if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, payment_params: Some(params), payment_hash, remaining_max_total_routing_fee_msat, .. } = pmt { @@ -1246,7 +1276,7 @@ impl OutboundPayments { } core::mem::drop(outbounds); if let Some((payment_hash, payment_id, route_params)) = retry_id_route_params { - self.find_route_and_send_payment(payment_hash, payment_id, route_params, router, first_hops(), &inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, &send_payment_along_path) + self.find_route_and_send_payment(payment_hash, payment_id, route_params, router, first_hops(), &inflight_htlcs, None, entropy_source, node_signer, best_block_height, logger, pending_events, &send_payment_along_path) } else { break } } @@ -1351,7 +1381,7 @@ impl OutboundPayments { let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion.clone(), payment_id, keysend_preimage, &route, Some(retry_strategy), - Some(route_params.payment_params.clone()), entropy_source, best_block_height, None) + Some(route_params.payment_params.clone()), entropy_source, best_block_height, None, None) .map_err(|_| { log_error!(logger, "Payment with id {} is already pending. New payment had payment hash {}", payment_id, payment_hash); @@ -1359,7 +1389,59 @@ impl OutboundPayments { })?; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, - keysend_preimage, None, payment_id, None, &onion_session_privs, node_signer, + keysend_preimage, None, None, payment_id, None, &onion_session_privs, node_signer, + best_block_height, &send_payment_along_path); + log_info!(logger, "Sending payment with id {} and hash {} returned {:?}", + payment_id, payment_hash, res); + if let Err(e) = res { + self.handle_pay_route_err( + e, payment_id, payment_hash, route, route_params, onion_session_privs, router, first_hops, + &inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, + &send_payment_along_path + ); + } + Ok(()) + } + + /// Errors immediately on [`RetryableSendFailure`] error conditions. Otherwise, further errors may + /// be surfaced asynchronously via [`Event::PaymentPathFailed`] and [`Event::PaymentFailed`]. + /// + /// [`Event::PaymentPathFailed`]: crate::events::Event::PaymentPathFailed + /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed + pub(super) fn send_payment_for_trampoline_forward( + &self, payment_id: PaymentId, payment_hash: PaymentHash, trampoline_forward_info: TrampolineForwardInfo, + retry_strategy: Retry, mut route_params: RouteParameters, + router: &R, first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, + node_signer: &NS, best_block_height: u32, logger: &L, + pending_events: &Mutex)>>, send_payment_along_path: SP, + ) -> Result<(), RetryableSendFailure> + where + R::Target: Router, + ES::Target: EntropySource, + NS::Target: NodeSigner, + L::Target: Logger, + IH: Fn() -> InFlightHtlcs, + SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + { + let inter_trampoline_payment_secret = PaymentSecret(entropy_source.get_secure_random_bytes()); + let recipient_onion = RecipientOnionFields::secret_only(inter_trampoline_payment_secret); + + let route = self.find_initial_route( + payment_id, payment_hash, &recipient_onion, None, None, &mut route_params, router, + &first_hops, &inflight_htlcs, node_signer, best_block_height, logger, + )?; + + let onion_session_privs = self.add_new_pending_payment(payment_hash, + recipient_onion.clone(), payment_id, None, &route, Some(retry_strategy), + Some(route_params.payment_params.clone()), entropy_source, best_block_height, None, Some(trampoline_forward_info.clone())) + .map_err(|_| { + log_error!(logger, "Payment with id {} is already pending. New payment had payment hash {}", + payment_id, payment_hash); + RetryableSendFailure::DuplicatePayment + })?; + + let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, + None, None, Some(&trampoline_forward_info), payment_id, None, &onion_session_privs, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Sending payment with id {} and hash {} returned {:?}", payment_id, payment_hash, res); @@ -1375,8 +1457,8 @@ impl OutboundPayments { fn find_route_and_send_payment( &self, payment_hash: PaymentHash, payment_id: PaymentId, route_params: RouteParameters, - router: &R, first_hops: Vec, inflight_htlcs: &IH, entropy_source: &ES, - node_signer: &NS, best_block_height: u32, logger: &L, + router: &R, first_hops: Vec, inflight_htlcs: &IH, trampoline_forward_info: Option, + entropy_source: &ES, node_signer: &NS, best_block_height: u32, logger: &L, pending_events: &Mutex)>>, send_payment_along_path: &SP, ) where @@ -1520,7 +1602,7 @@ impl OutboundPayments { } }; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, keysend_preimage, - invoice_request.as_ref(), payment_id, Some(total_msat), &onion_session_privs, node_signer, + invoice_request.as_ref(), trampoline_forward_info.as_ref(), payment_id, Some(total_msat), &onion_session_privs, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Result retrying payment id {}: {:?}", &payment_id, res); if let Err(e) = res { @@ -1552,7 +1634,7 @@ impl OutboundPayments { PaymentSendFailure::AllFailedResendSafe(errs) => { self.remove_session_privs(payment_id, route.paths.iter().zip(onion_session_privs.iter())); Self::push_path_failed_evs_and_scids(payment_id, payment_hash, &mut route_params, route.paths, errs.into_iter().map(|e| Err(e)), logger, pending_events); - self.find_route_and_send_payment(payment_hash, payment_id, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path); + self.find_route_and_send_payment(payment_hash, payment_id, route_params, router, first_hops, inflight_htlcs, None, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path); }, PaymentSendFailure::PartialFailure { failed_paths_retry: Some(mut retry), results, .. } => { debug_assert_eq!(results.len(), route.paths.len()); @@ -1572,7 +1654,7 @@ impl OutboundPayments { // Some paths were sent, even if we failed to send the full MPP value our recipient may // misbehave and claim the funds, at which point we have to consider the payment sent, so // return `Ok()` here, ignoring any retry errors. - self.find_route_and_send_payment(payment_hash, payment_id, retry, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path); + self.find_route_and_send_payment(payment_hash, payment_id, retry, router, first_hops, inflight_htlcs, None, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path); }, PaymentSendFailure::PartialFailure { failed_paths_retry: None, .. } => { // This may happen if we send a payment and some paths fail, but only due to a temporary @@ -1665,7 +1747,7 @@ impl OutboundPayments { let route = Route { paths: vec![path], route_params: None }; let onion_session_privs = self.add_new_pending_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret), payment_id, None, &route, None, None, - entropy_source, best_block_height, None + entropy_source, best_block_height, None, None ).map_err(|e| { debug_assert!(matches!(e, PaymentSendFailure::DuplicatePayment)); ProbeSendFailure::DuplicateProbe @@ -1673,7 +1755,7 @@ impl OutboundPayments { let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); match self.pay_route_internal(&route, payment_hash, &recipient_onion_fields, - None, None, payment_id, None, &onion_session_privs, node_signer, best_block_height, + None, None, None, payment_id, None, &onion_session_privs, node_signer, best_block_height, &send_payment_along_path ) { Ok(()) => Ok((payment_hash, payment_id)), @@ -1720,14 +1802,14 @@ impl OutboundPayments { &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, payment_id: PaymentId, route: &Route, retry_strategy: Option, entropy_source: &ES, best_block_height: u32 ) -> Result, PaymentSendFailure> where ES::Target: EntropySource { - self.add_new_pending_payment(payment_hash, recipient_onion, payment_id, None, route, retry_strategy, None, entropy_source, best_block_height, None) + self.add_new_pending_payment(payment_hash, recipient_onion, payment_id, None, route, retry_strategy, None, entropy_source, best_block_height, None, None) } pub(super) fn add_new_pending_payment( &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, payment_id: PaymentId, keysend_preimage: Option, route: &Route, retry_strategy: Option, payment_params: Option, entropy_source: &ES, best_block_height: u32, - bolt12_invoice: Option + bolt12_invoice: Option, trampoline_forward_info: Option ) -> Result, PaymentSendFailure> where ES::Target: EntropySource { let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); match pending_outbounds.entry(payment_id) { @@ -1735,7 +1817,7 @@ impl OutboundPayments { hash_map::Entry::Vacant(entry) => { let (payment, onion_session_privs) = Self::create_pending_payment( payment_hash, recipient_onion, keysend_preimage, None, bolt12_invoice, route, retry_strategy, - payment_params, entropy_source, best_block_height + payment_params, entropy_source, best_block_height, trampoline_forward_info ); entry.insert(payment); Ok(onion_session_privs) @@ -1747,7 +1829,8 @@ impl OutboundPayments { payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option, invoice_request: Option, bolt12_invoice: Option, route: &Route, retry_strategy: Option, - payment_params: Option, entropy_source: &ES, best_block_height: u32 + payment_params: Option, entropy_source: &ES, best_block_height: u32, + trampoline_forward_info: Option ) -> (PendingOutboundPayment, Vec<[u8; 32]>) where ES::Target: EntropySource, @@ -1767,6 +1850,7 @@ impl OutboundPayments { payment_hash, payment_secret: recipient_onion.payment_secret, payment_metadata: recipient_onion.payment_metadata, + trampoline_forwarding_data: trampoline_forward_info, keysend_preimage, invoice_request, bolt12_invoice, @@ -1866,8 +1950,9 @@ impl OutboundPayments { fn pay_route_internal( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields, keysend_preimage: Option, invoice_request: Option<&InvoiceRequest>, - payment_id: PaymentId, recv_value_msat: Option, onion_session_privs: &Vec<[u8; 32]>, - node_signer: &NS, best_block_height: u32, send_payment_along_path: &F + trampoline_forward_info: Option<&TrampolineForwardInfo>, payment_id: PaymentId, + recv_value_msat: Option, onion_session_privs: &Vec<[u8; 32]>, node_signer: &NS, + best_block_height: u32, send_payment_along_path: &F ) -> Result<(), PaymentSendFailure> where NS::Target: NodeSigner, @@ -1921,7 +2006,7 @@ impl OutboundPayments { let path_res = send_payment_along_path(SendAlongPathArgs { path: &path, payment_hash: &payment_hash, recipient_onion, total_value, cur_height, payment_id, keysend_preimage: &keysend_preimage, invoice_request, - session_priv_bytes: *session_priv_bytes + trampoline_forward_info, session_priv_bytes: *session_priv_bytes }); results.push(path_res); } @@ -1987,7 +2072,7 @@ impl OutboundPayments { F: Fn(SendAlongPathArgs) -> Result<(), APIError>, { self.pay_route_internal(route, payment_hash, &recipient_onion, - keysend_preimage, None, payment_id, recv_value_msat, &onion_session_privs, + keysend_preimage, None, None, payment_id, recv_value_msat, &onion_session_privs, node_signer, best_block_height, &send_payment_along_path) .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e }) } @@ -2023,14 +2108,34 @@ impl OutboundPayments { log_info!(logger, "Payment with id {} and hash {} sent!", payment_id, payment_hash); let fee_paid_msat = payment.get().get_pending_fee_msat(); let amount_msat = payment.get().total_msat(); - pending_events.push_back((events::Event::PaymentSent { - payment_id: Some(payment_id), - payment_preimage, - payment_hash, - amount_msat, - fee_paid_msat, - bolt12_invoice: payment.get().bolt12_invoice().cloned(), - }, Some(ev_completion_action.clone()))); + if let &PendingOutboundPayment::Retryable { trampoline_forwarding_data: Some(ref trampoline_forward_info), .. } = payment.get() { + // TODO: this should never be possible because this only gets triggered by an outbound HTLCSource + debug_assert!(false); + // due to inbound MPP possibility, we can have multiple + // for current_previous_hop in &trampoline_forward_info.previous_hop_data { + // pending_events.push_back((events::Event::PaymentForwarded { + // prev_channel_id: Some(current_previous_hop.channel_id), + // next_channel_id: None, + // prev_user_channel_id: current_previous_hop.user_channel_id, + // next_user_channel_id: None, + // prev_node_id: current_previous_hop.counterparty_node_id, + // next_node_id: None, + // total_fee_earned_msat: Some(1000), + // skimmed_fee_msat: None, + // claim_from_onchain_tx: false, + // outbound_amount_forwarded_msat: None, + // }, Some(ev_completion_action.clone()))); + // } + } else { + pending_events.push_back((events::Event::PaymentSent { + payment_id: Some(payment_id), + payment_preimage, + payment_hash, + amount_msat, + fee_paid_msat, + bolt12_invoice: payment.get().bolt12_invoice().cloned(), + }, Some(ev_completion_action.clone()))); + } payment.get_mut().mark_fulfilled(); } @@ -2386,6 +2491,7 @@ impl OutboundPayments { payment_hash, payment_secret: None, // only used for retries, and we'll never retry on startup payment_metadata: None, // only used for retries, and we'll never retry on startup + trampoline_forwarding_data: None, // todo: we might need to retry forwarding Trampolines on startup keysend_preimage: None, // only used for retries, and we'll never retry on startup invoice_request: None, // only used for retries, and we'll never retry on startup bolt12_invoice: None, // only used for retries, and we'll never retry on startup! @@ -2479,6 +2585,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (11, remaining_max_total_routing_fee_msat, option), (13, invoice_request, option), (15, bolt12_invoice, option), + (17, trampoline_forwarding_data, option), (not_written, retry_strategy, (static_value, None)), (not_written, attempts, (static_value, PaymentAttempts::new())), }, @@ -2630,10 +2737,10 @@ mod tests { outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(expired_route_params.payment_params.clone()), - &&keys_manager, 0, None).unwrap(); + &&keys_manager, 0, None, None).unwrap(); outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), expired_route_params, &&router, vec![], - &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &&logger, &pending_events, + &|| InFlightHtlcs::new(), None, &&keys_manager, &&keys_manager, 0, &&logger, &pending_events, &|_| Ok(())); let events = pending_events.lock().unwrap(); assert_eq!(events.len(), 1); @@ -2673,10 +2780,10 @@ mod tests { outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(route_params.payment_params.clone()), - &&keys_manager, 0, None).unwrap(); + &&keys_manager, 0, None, None).unwrap(); outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), route_params, &&router, vec![], - &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &&logger, &pending_events, + &|| InFlightHtlcs::new(), None, &&keys_manager, &&keys_manager, 0, &&logger, &pending_events, &|_| Ok(())); let events = pending_events.lock().unwrap(); assert_eq!(events.len(), 1);