diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 621c5103353..f7e19ba8f38 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -8,13 +8,15 @@ // licenses. use crate::blinded_path::payment::{ - BlindedPaymentPath, Bolt12RefundContext, DummyTlvs, ForwardTlvs, PaymentConstraints, - PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs, PAYMENT_PADDING_ROUND_OFF, + BlindedPaymentPath, Bolt12RefundContext, DummyTlvs, ForwardNode, ForwardTlvs, + PaymentConstraints, PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs, + TrampolineForwardTlvs, PAYMENT_PADDING_ROUND_OFF, }; use crate::blinded_path::utils::is_padded; use crate::blinded_path::{self, BlindedHop}; +use crate::chain::channelmonitor::HTLC_FAIL_BACK_BUFFER; use crate::events::{Event, HTLCHandlingFailureType, PaymentFailureReason}; -use crate::ln::channelmanager::{self, HTLCFailureMsg, PaymentId}; +use crate::ln::channelmanager::{self, HTLCFailureMsg, PaymentId, MPP_TIMEOUT_TICKS}; use crate::ln::functional_test_utils::*; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{ @@ -28,13 +30,15 @@ use crate::ln::outbound_payment::{ use crate::ln::types::ChannelId; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::prelude::*; +use crate::routing::gossip::RoutingFees; use crate::routing::router::{ - BlindedTail, Path, Payee, PaymentParameters, Route, RouteHop, RouteParameters, TrampolineHop, + compute_fees, BlindedTail, Path, Payee, PaymentParameters, Route, RouteHop, RouteParameters, + TrampolineHop, }; use crate::sign::{NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient}; use crate::types::features::{BlindedHopFeatures, ChannelFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentSecret}; -use crate::util::config::{HTLCInterceptionFlags, UserConfig}; +use crate::util::config::{ChannelConfig, HTLCInterceptionFlags, UserConfig}; use crate::util::ser::{WithoutLength, Writeable}; use crate::util::test_utils::{self, bytes_from_hex, pubkey_from_hex, secret_from_hex}; use bitcoin::hex::DisplayHex; @@ -2199,7 +2203,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { ).unwrap(); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat); - let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, 32, &None, None, Some(trampoline_packet)).unwrap(); + let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, 32, &None, None, Some((trampoline_packet, None))).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, @@ -2399,16 +2403,16 @@ impl<'a> TrampolineTestCase { } } - fn outer_onion_cltv(&self, outer_cltv: u32) -> u32 { + fn inner_onion_cltv(&self, outer_cltv: u32) -> u32 { if *self == TrampolineTestCase::OuterCLTVLessThanTrampoline { - return outer_cltv / 2; + return outer_cltv * 10; } outer_cltv } - fn outer_onion_amt(&self, original_amt: u64) -> u64 { + fn inner_onion_amt(&self, original_amt: u64) -> u64 { if *self == TrampolineTestCase::Underpayment { - return original_amt / 2; + return original_amt * 10; } original_amt } @@ -2432,19 +2436,20 @@ fn test_trampoline_blinded_receive() { // payloads that send to unblinded receives and invalid payloads. fn replacement_onion( test_case: TrampolineTestCase, secp_ctx: &Secp256k1, override_random_bytes: [u8; 32], - route: Route, original_amt_msat: u64, starting_htlc_offset: u32, excess_final_cltv: u32, - original_trampoline_cltv: u32, payment_hash: PaymentHash, payment_secret: PaymentSecret, - blinded: bool, + route: Route, fred_amt_msat: u64, fred_final_cltv: u32, excess_final_cltv_delta: u32, + payment_hash: PaymentHash, payment_secret: PaymentSecret, blinded: bool, + starting_htlc_offset: u32, carol: PublicKey, eve: (PublicKey, &PaymentRelay), fred: PublicKey, ) -> msgs::OnionPacket { + assert!(!blinded || !matches!(test_case, TrampolineTestCase::Success)); let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap(); let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(original_amt_msat); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(fred_amt_msat); let blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); - // Rebuild our trampoline packet from the original route. If we want to test Carol receiving - // as an unblinded trampoline hop, we switch out her inner trampoline onion with a direct - // receive payload because LDK doesn't support unblinded trampoline receives. + // Rebuild our trampoline packet from the original route. If we want to test Fred receiving + // as an unblinded trampoline hop, we switch out the trampoline packets with unblinded ones + // because LDK doesn't support unblinded trampoline receives. let (trampoline_packet, outer_total_msat) = { let (mut trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads( @@ -2456,21 +2461,106 @@ fn replacement_onion( .unwrap(); if !blinded { - trampoline_payloads = vec![msgs::OutboundTrampolinePayload::Receive { - payment_data: Some(msgs::FinalOnionHopData { - payment_secret, - total_msat: original_amt_msat, - }), - sender_intended_htlc_amt_msat: original_amt_msat, - cltv_expiry_height: original_trampoline_cltv - + starting_htlc_offset - + excess_final_cltv, - }]; + let eve_trampoline_fees = compute_fees( + fred_amt_msat, + RoutingFees { + base_msat: eve.1.fee_base_msat, + proportional_millionths: eve.1.fee_proportional_millionths, + }, + ) + .unwrap(); + + trampoline_payloads = vec![ + // Carol must forward to Eve with enough fees + CLTV to cover her policy. + msgs::OutboundTrampolinePayload::Forward { + amt_to_forward: fred_amt_msat + eve_trampoline_fees, + outgoing_cltv_value: starting_htlc_offset + + fred_final_cltv + excess_final_cltv_delta + + eve.1.cltv_expiry_delta as u32, + outgoing_node_id: eve.0, + }, + // Eve should forward the final amount to fred, allowing enough CLTV to cover his + // final expiry delta and the excess that the sender added. + msgs::OutboundTrampolinePayload::Forward { + amt_to_forward: fred_amt_msat, + outgoing_cltv_value: starting_htlc_offset + + fred_final_cltv + excess_final_cltv_delta, + outgoing_node_id: fred, + }, + // Fred just needs to receive the amount he's expecting, and since this is an + // unblinded route he'll expect an outgoing cltv that accounts for his final + // expiry delta and excess that the sender added. + msgs::OutboundTrampolinePayload::Receive { + payment_data: Some(msgs::FinalOnionHopData { + payment_secret, + total_msat: fred_amt_msat, + }), + sender_intended_htlc_amt_msat: fred_amt_msat, + cltv_expiry_height: starting_htlc_offset + + fred_final_cltv + excess_final_cltv_delta, + }, + ]; + } + + match trampoline_payloads.last_mut().unwrap() { + msgs::OutboundTrampolinePayload::Receive { + sender_intended_htlc_amt_msat, + cltv_expiry_height, + .. + } => { + *sender_intended_htlc_amt_msat = + test_case.inner_onion_amt(*sender_intended_htlc_amt_msat); + *cltv_expiry_height = test_case.inner_onion_cltv(*cltv_expiry_height); + }, + msgs::OutboundTrampolinePayload::BlindedReceive { + sender_intended_htlc_amt_msat, + cltv_expiry_height, + .. + } => { + *sender_intended_htlc_amt_msat = + test_case.inner_onion_amt(*sender_intended_htlc_amt_msat); + *cltv_expiry_height = test_case.inner_onion_cltv(*cltv_expiry_height); + }, + _ => panic!("unexpected final trampoline payload type"), } + // TODO: clean this up + let key_derivation_tail = if !blinded { + BlindedTail { + // Note: this tail isn't *actually* used in our trampoline key derivation, we just + // have to have one to be able to use the helper function. + trampoline_hops: vec![ + TrampolineHop { + pubkey: carol, + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + TrampolineHop { + pubkey: eve.0, + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + TrampolineHop { + pubkey: fred, + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + ], + hops: vec![], + blinding_point: blinded_tail.blinding_point, + excess_final_cltv_expiry_delta: excess_final_cltv_delta, + final_value_msat: fred_amt_msat, + } + } else { + blinded_tail.clone() + }; + let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys( &secp_ctx, - &blinded_tail, + &key_derivation_tail, &trampoline_session_priv, ); let trampoline_packet = onion_utils::construct_trampoline_onion_packet( @@ -2494,27 +2584,11 @@ fn replacement_onion( starting_htlc_offset, &None, None, - Some(trampoline_packet), + Some((trampoline_packet, None)), ) .unwrap(); assert_eq!(outer_payloads.len(), 2); - // If we're trying to test invalid payloads, we modify Carol's *outer* onion to have values - // that are inconsistent with her inner onion. We need to do this manually because we - // (obviously) can't construct an invalid onion with LDK's built in functions. - match &mut outer_payloads[1] { - msgs::OutboundOnionPayload::TrampolineEntrypoint { - amt_to_forward, - outgoing_cltv_value, - .. - } => { - *amt_to_forward = test_case.outer_onion_amt(original_amt_msat); - let outer_cltv = original_trampoline_cltv + starting_htlc_offset + excess_final_cltv; - *outgoing_cltv_value = test_case.outer_onion_cltv(outer_cltv); - }, - _ => panic!("final payload is not trampoline entrypoint"), - } - let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv); onion_utils::construct_onion_packet( @@ -2532,7 +2606,7 @@ fn replacement_onion( // - To hit validation errors by manipulating the trampoline's outer packet. Without this, we would // have to manually construct the onion. fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { - const TOTAL_NODE_COUNT: usize = 3; + const TOTAL_NODE_COUNT: usize = 6; let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); @@ -2543,34 +2617,114 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { let alice_bob_chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); let bob_carol_chan = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let carol_dave_chan = + create_announced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); + let dave_eve_chan = create_announced_chan_between_nodes_with_value(&nodes, 3, 4, 1_000_000, 0); + let eve_fred_chan = create_announced_chan_between_nodes_with_value(&nodes, 4, 5, 1_000_000, 0); let starting_htlc_offset = (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1; for i in 0..TOTAL_NODE_COUNT { connect_blocks(&nodes[i], starting_htlc_offset - 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 eve_node_id = nodes[4].node().get_our_node_id(); + let fred_node_id = nodes[5].node().get_our_node_id(); let alice_bob_scid = get_scid_from_channel_id(&nodes[0], alice_bob_chan.2); let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan.2); - let original_amt_msat = 1000; - // Note that for TrampolineTestCase::OuterCLTVLessThanTrampoline to work properly, - // (starting_htlc_offset + excess_final_cltv) / 2 < (starting_htlc_offset + excess_final_cltv + original_trampoline_cltv) - // otherwise dividing the CLTV value by 2 won't kick us under the outer trampoline CLTV. - let original_trampoline_cltv = 42; + let fred_recv_amt = 1000; + let fred_cltv_final = 72; let excess_final_cltv = 70; + let carol_dave_policy = carol_dave_chan.1.contents; + let dave_eve_policy = dave_eve_chan.1.contents; + let eve_fred_policy = eve_fred_chan.1.contents; + + let carol_trampoline_cltv_delta = + carol_dave_policy.cltv_expiry_delta + dave_eve_policy.cltv_expiry_delta; + let carol_trampoline_fee_prop = + carol_dave_policy.fee_proportional_millionths + dave_eve_policy.fee_proportional_millionths; + let carol_trampoline_fee_base = carol_dave_policy.fee_base_msat + dave_eve_policy.fee_base_msat; + + let eve_trampoline_relay = PaymentRelay { + // Note that we add 1 to eve's required CLTV so that she has a non-zero CLTV budget, because + // our pathfinding doesn't support a zero cltv detla. In reality, we'd include a larger + // margin than a single node's delta for trampoline payments, so we don't worry about it. + cltv_expiry_delta: eve_fred_policy.cltv_expiry_delta + 1, + fee_proportional_millionths: eve_fred_policy.fee_proportional_millionths, + fee_base_msat: eve_fred_policy.fee_base_msat, + }; let (payment_preimage, payment_hash, payment_secret) = - get_payment_preimage_hash(&nodes[2], Some(original_amt_msat), None); + get_payment_preimage_hash(&nodes[5], Some(fred_recv_amt), None); // We need the session priv to replace the onion packet later. let override_random_bytes = [42; 32]; *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(override_random_bytes); - let route = Route { + // Create a blinded tail where Carol and Eve are trampoline hops, sending to Fred. In our + // unblinded test cases, we'll override this anyway (with a tail sending to an unblinded + // receive, which LDK doesn't allow). + let carol_relay = PaymentRelay { + // The policy for a blinded trampoline hop needs to cover all the fees for the path to + // the next trampoline. Here we're using the exact values, but IRL the receiving node + // would probably set more general values. + cltv_expiry_delta: carol_trampoline_cltv_delta, + fee_proportional_millionths: carol_trampoline_fee_prop, + fee_base_msat: carol_trampoline_fee_base, + }; + let no_payment_constraints = + PaymentConstraints { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: fred_recv_amt }; + let intermediate_nodes = [ + ForwardNode { + tlvs: TrampolineForwardTlvs { + next_trampoline: eve_node_id, + payment_relay: carol_relay.clone(), + payment_constraints: no_payment_constraints.clone(), + features: BlindedHopFeatures::empty(), + next_blinding_override: None, + }, + node_id: carol_node_id, + htlc_maximum_msat: u64::max_value(), + }, + ForwardNode { + tlvs: TrampolineForwardTlvs { + next_trampoline: fred_node_id, + payment_relay: eve_trampoline_relay.clone(), + payment_constraints: no_payment_constraints.clone(), + features: BlindedHopFeatures::empty(), + next_blinding_override: None, + }, + node_id: eve_node_id, + htlc_maximum_msat: u64::max_value(), + }, + ]; + + let blinded_tail = create_trampoline_forward_blinded_tail( + &secp_ctx, + &nodes[5].keys_manager, + &intermediate_nodes, + fred_node_id, + nodes[5].keys_manager.get_receive_auth_key(), + ReceiveTlvs { + payment_secret, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: fred_recv_amt, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + }, + fred_cltv_final, + excess_final_cltv, + fred_recv_amt, + ); + assert_eq!(blinded_tail.trampoline_hops.len(), 1); + assert_eq!(blinded_tail.hops.len(), 3); + + let mut route = Route { paths: vec![Path { hops: vec![ RouteHop { @@ -2587,42 +2741,59 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { node_features: NodeFeatures::empty(), short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), - fee_msat: 0, - cltv_expiry_delta: original_trampoline_cltv + excess_final_cltv, + fee_msat: blinded_tail.trampoline_hops[0].fee_msat, + cltv_expiry_delta: blinded_tail.trampoline_hops[0].cltv_expiry_delta, maybe_announced_channel: false, }, ], - // Create a blinded tail where Carol is receiving. In our unblinded test cases, we'll - // override this anyway (with a tail sending to an unblinded receive, which LDK doesn't - // allow). - blinded_tail: Some(create_trampoline_forward_blinded_tail( - &secp_ctx, - &nodes[2].keys_manager, - &[], - carol_node_id, - nodes[2].keys_manager.get_receive_auth_key(), - ReceiveTlvs { - payment_secret, - payment_constraints: PaymentConstraints { - max_cltv_expiry: u32::max_value(), - htlc_minimum_msat: original_amt_msat, - }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), - }, - original_trampoline_cltv, - excess_final_cltv, - original_amt_msat, - )), + blinded_tail: Some(blinded_tail), }], route_params: None, }; + // For unblinded tests, replace the blinded tail with an unblinded trampoline structure so + // that Alice's stored path has the correct trampoline shared secrets for error decoding. + // The replacement onion (constructed below) uses the same unblinded key derivation. + if !blinded { + let bt = route.paths[0].blinded_tail.as_ref().unwrap(); + let blinding_point = bt.blinding_point; + let total_cltv = route.paths[0].hops.last().unwrap().cltv_expiry_delta; + let eve_cltv = eve_trampoline_relay.cltv_expiry_delta as u32; + let carol_cltv = total_cltv - eve_cltv; + route.paths[0].blinded_tail = Some(BlindedTail { + trampoline_hops: vec![ + TrampolineHop { + pubkey: carol_node_id, + node_features: NodeFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: carol_cltv, + }, + TrampolineHop { + pubkey: eve_node_id, + node_features: NodeFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: eve_cltv, + }, + TrampolineHop { + pubkey: fred_node_id, + node_features: NodeFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + ], + hops: vec![], + blinding_point, + excess_final_cltv_expiry_delta: 0, + final_value_msat: fred_recv_amt, + }); + } + nodes[0] .node .send_payment_with_route( route.clone(), payment_hash, - RecipientOnionFields::spontaneous_empty(original_amt_msat), + RecipientOnionFields::spontaneous_empty(fred_recv_amt), PaymentId(payment_hash.0), ) .unwrap(); @@ -2643,35 +2814,36 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { // Replace the onion to test different scenarios: // - If !blinded: Creates a payload sending to an unblinded trampoline // - If blinded: Modifies outer onion to create outer/inner mismatches if testing failures - update_message.map(|msg| { - msg.onion_routing_packet = replacement_onion( - test_case, - &secp_ctx, - override_random_bytes, - route, - original_amt_msat, - starting_htlc_offset, - original_trampoline_cltv, - excess_final_cltv, - payment_hash, - payment_secret, - blinded, - ) - }); - - let route: &[&Node] = &[&nodes[1], &nodes[2]]; - let args = PassAlongPathArgs::new( - &nodes[0], - route, - original_amt_msat, - payment_hash, - first_message_event, - ); + if !blinded || !matches!(test_case, TrampolineTestCase::Success) { + update_message.map(|msg| { + msg.onion_routing_packet = replacement_onion( + test_case, + &secp_ctx, + override_random_bytes, + route, + fred_recv_amt, + fred_cltv_final, + excess_final_cltv, + payment_hash, + payment_secret, + blinded, + // Our internal send payment helpers add one block to the current height to + // create our payments. Do the same here so that our replacement onion will have + // the right cltv. + starting_htlc_offset + 1, + carol_node_id, + (eve_node_id, &eve_trampoline_relay), + fred_node_id, + ) + }); + } - let amt_bytes = test_case.outer_onion_amt(original_amt_msat).to_be_bytes(); - let cltv_bytes = test_case - .outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset + excess_final_cltv) - .to_be_bytes(); + // We add two blocks to the minimum height that fred will accept because we added one block + // extra CLTV for Eve's forwarding CLTV "budget" and our dispatch adds one block to the + // current height. + let final_cltv_height = fred_cltv_final + starting_htlc_offset + excess_final_cltv + 2; + let amt_bytes = fred_recv_amt.to_be_bytes(); + let cltv_bytes = final_cltv_height.to_be_bytes(); let payment_failure = test_case.payment_failed_conditions(&amt_bytes, &cltv_bytes).map(|p| { if blinded { PaymentFailedConditions::new() @@ -2680,34 +2852,115 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { p } }); + let route: &[&Node] = &[&nodes[1], &nodes[2], &nodes[3], &nodes[4], &nodes[5]]; + let args = + PassAlongPathArgs::new(&nodes[0], route, fred_recv_amt, payment_hash, first_message_event); let args = if payment_failure.is_some() { args.with_payment_preimage(payment_preimage) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) } else { - let htlc_cltv = starting_htlc_offset + original_trampoline_cltv + excess_final_cltv; - args.with_payment_secret(payment_secret).with_payment_claimable_cltv(htlc_cltv) + args.with_payment_secret(payment_secret).with_payment_claimable_cltv(final_cltv_height) }; do_pass_along_path(args); if let Some(failure) = payment_failure { - let node_updates = get_htlc_update_msgs(&nodes[2], &bob_node_id); - nodes[1].node.handle_update_fail_htlc(carol_node_id, &node_updates.update_fail_htlcs[0]); + let alice_node_id = nodes[0].node.get_our_node_id(); + + // Fred is a blinded introduction node recipient, so will fail back with fail htlc. + let updates_fred = get_htlc_update_msgs(&nodes[5], &eve_node_id); + assert_eq!(updates_fred.update_fail_htlcs.len(), 1); + nodes[4].node.handle_update_fail_htlc(fred_node_id, &updates_fred.update_fail_htlcs[0]); + do_commitment_signed_dance( + &nodes[4], + &nodes[5], + &updates_fred.commitment_signed, + false, + false, + ); + + // Eve is a relaying blinded trampoline, so will fail back with malformed htlc. + expect_and_process_pending_htlcs_and_htlc_handling_failed( + &nodes[4], + &[HTLCHandlingFailureType::TrampolineForward {}], + ); + check_added_monitors(&nodes[4], 1); + + let updates_eve = get_htlc_update_msgs(&nodes[4], &dave_node_id); + if blinded { + assert_eq!(updates_eve.update_fail_malformed_htlcs.len(), 1); + nodes[3].node.handle_update_fail_malformed_htlc( + eve_node_id, + &updates_eve.update_fail_malformed_htlcs[0], + ); + } else { + assert_eq!(updates_eve.update_fail_htlcs.len(), 1); + nodes[3].node.handle_update_fail_htlc(eve_node_id, &updates_eve.update_fail_htlcs[0]); + } + + do_commitment_signed_dance( + &nodes[3], + &nodes[4], + &updates_eve.commitment_signed, + true, + false, + ); + + // Dave is a regular forwarding node, so will fail back with fail htlc. + let updates_dave = get_htlc_update_msgs(&nodes[3], &carol_node_id); + assert_eq!(updates_dave.update_fail_htlcs.len(), 1); + nodes[2].node.handle_update_fail_htlc(dave_node_id, &updates_dave.update_fail_htlcs[0]); + do_commitment_signed_dance( + &nodes[2], + &nodes[3], + &updates_dave.commitment_signed, + false, + false, + ); + + // Carol is a blinded trampoline introduction node, so will fail back with htlc fail. + expect_and_process_pending_htlcs_and_htlc_handling_failed( + &nodes[2], + &[HTLCHandlingFailureType::TrampolineForward {}], + ); + + check_added_monitors(&nodes[2], 1); + + let updates_carol = get_htlc_update_msgs(&nodes[2], &bob_node_id); + assert_eq!(updates_carol.update_fail_htlcs.len(), 1); + nodes[1].node.handle_update_fail_htlc(carol_node_id, &updates_carol.update_fail_htlcs[0]); + let bob_carol_chan = nodes[1] + .node + .list_channels() + .iter() + .find(|c| c.counterparty.node_id == carol_node_id) + .unwrap() + .channel_id; do_commitment_signed_dance( &nodes[1], &nodes[2], - &node_updates.commitment_signed, - true, + &updates_carol.commitment_signed, + false, false, ); - let node_updates = get_htlc_update_msgs(&nodes[1], &alice_node_id); - nodes[0].node.handle_update_fail_htlc(bob_node_id, &node_updates.update_fail_htlcs[0]); + // Bob is a regular forwarding node, so will fail back with htlc fail. + expect_and_process_pending_htlcs_and_htlc_handling_failed( + &nodes[1], + &[HTLCHandlingFailureType::Forward { + node_id: Some(carol_node_id), + channel_id: bob_carol_chan, + }], + ); + check_added_monitors(&nodes[1], 1); + let updates_bob = get_htlc_update_msgs(&nodes[1], &alice_node_id); + assert_eq!(updates_bob.update_fail_htlcs.len(), 1); + nodes[0].node.handle_update_fail_htlc(bob_node_id, &updates_bob.update_fail_htlcs[0]); do_commitment_signed_dance( &nodes[0], &nodes[1], - &node_updates.commitment_signed, + &updates_bob.commitment_signed, false, false, ); @@ -2717,129 +2970,289 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { // Because we support blinded paths, we also assert on our expected logs to make sure // that the failure reason hidden by obfuscated blinded errors is as expected. if let Some((module, line, count)) = test_case.expected_log() { - nodes[2].logger.assert_log_contains(module, line, count); + nodes[5].logger.assert_log_contains(module, line, count); } } else { - claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); + claim_payment(&nodes[0], route, payment_preimage); } } -#[test] -#[rustfmt::skip] -fn test_trampoline_forward_rejection() { - const TOTAL_NODE_COUNT: usize = 3; - - let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); - let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]); - let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs); - - let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); - let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); - - for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks - connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1); - } - - let alice_node_id = nodes[0].node().get_our_node_id(); - let bob_node_id = nodes[1].node().get_our_node_id(); - let carol_node_id = nodes[2].node().get_our_node_id(); +/// Sets up channels and sends a trampoline MPP payment across two paths. +/// +/// Topology: +/// Alice (0) --> Bob (1) --> Carol (2, trampoline node) +/// Alice (0) --> Barry (3) --> Carol (2, trampoline node) +/// +/// Carol's inner trampoline onion is a forward to an unknown next node, which is intentionally +/// faked to force a forwarding failure after our MPP parts have accumulated. +/// +/// Returns (payment_hash, per_path_amount, ev_to_bob, ev_to_barry). +fn send_trampoline_mpp_payment<'a, 'b, 'c>( + nodes: &'a Vec>, +) -> (PaymentHash, u64, MessageSendEvent, MessageSendEvent) { + let secp_ctx = Secp256k1::new(); - 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 alice_bob_chan = + create_announced_chan_between_nodes_with_value(nodes, 0, 1, 1_000_000, 0).2; + let bob_carol_chan = + create_announced_chan_between_nodes_with_value(nodes, 1, 2, 1_000_000, 0).2; + let alice_barry_chan = + create_announced_chan_between_nodes_with_value(nodes, 0, 3, 1_000_000, 0).2; + let barry_carol_chan = + create_announced_chan_between_nodes_with_value(nodes, 3, 2, 1_000_000, 0).2; + + let per_path_amt = 500_000; + let total_amt = per_path_amt * 2; + let (_, payment_hash, payment_secret) = + get_payment_preimage_hash(&nodes[2], Some(total_amt), None); + + let bob_node_id = nodes[1].node.get_our_node_id(); + let carol_node_id = nodes[2].node.get_our_node_id(); + let barry_node_id = nodes[3].node.get_our_node_id(); + + let alice_bob_scid = get_scid_from_channel_id(&nodes[0], alice_bob_chan); + let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan); + let alice_barry_scid = get_scid_from_channel_id(&nodes[0], alice_barry_chan); + let barry_carol_scid = get_scid_from_channel_id(&nodes[3], barry_carol_chan); + + let trampoline_cltv = 42; + let excess_final_cltv = 70; - let amt_msat = 1000; - let (payment_preimage, payment_hash, _) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); + // Note we don't actually have an outgoing channel for Carol, we just use our default fee + // policy. + let carol_relay = ChannelConfig::default(); - let route = Route { - paths: vec![Path { - hops: vec![ - // Bob - RouteHop { - pubkey: bob_node_id, - node_features: NodeFeatures::empty(), - short_channel_id: alice_bob_scid, - channel_features: ChannelFeatures::empty(), - fee_msat: 1000, - cltv_expiry_delta: 48, - maybe_announced_channel: false, + let next_trampoline = PublicKey::from_slice(&[2; 33]).unwrap(); + let fwd_tail = || { + let intermediate_nodes = [ForwardNode { + tlvs: blinded_path::payment::TrampolineForwardTlvs { + next_trampoline, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: 1, + }, + features: BlindedHopFeatures::empty(), + payment_relay: PaymentRelay { + cltv_expiry_delta: carol_relay.cltv_expiry_delta, + fee_proportional_millionths: carol_relay.forwarding_fee_proportional_millionths, + fee_base_msat: carol_relay.forwarding_fee_base_msat, }, + next_blinding_override: None, + }, + node_id: carol_node_id, + htlc_maximum_msat: u64::max_value(), + }]; + let payee_tlvs = ReceiveTlvs { + payment_secret: PaymentSecret([0; 32]), + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: 1, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + }; + create_trampoline_forward_blinded_tail( + &secp_ctx, + &nodes[2].keys_manager, + &intermediate_nodes, + next_trampoline, + ReceiveAuthKey([0; 32]), + payee_tlvs, + trampoline_cltv, + excess_final_cltv, + per_path_amt, + ) + }; - // Carol - RouteHop { - pubkey: carol_node_id, - node_features: NodeFeatures::empty(), - short_channel_id: bob_carol_scid, - channel_features: ChannelFeatures::empty(), - fee_msat: 0, - cltv_expiry_delta: 24 + 24 + 39, - maybe_announced_channel: false, - } - ], - blinded_tail: Some(BlindedTail { - trampoline_hops: vec![ - // Carol - TrampolineHop { - pubkey: carol_node_id, - node_features: Features::empty(), - fee_msat: amt_msat, - cltv_expiry_delta: 24, - }, + let hop = |pubkey, short_channel_id, fee_msat, cltv_expiry_delta| RouteHop { + pubkey, + node_features: NodeFeatures::empty(), + short_channel_id, + channel_features: ChannelFeatures::empty(), + fee_msat, + cltv_expiry_delta, + maybe_announced_channel: true, + }; + let build_path_hops = |first_hop_node_id, first_hop_scid, second_hop_scid| { + vec![ + hop(first_hop_node_id, first_hop_scid, 1000, 48), + hop(carol_node_id, second_hop_scid, 0, trampoline_cltv + excess_final_cltv), + ] + }; - // Alice (unreachable) - TrampolineHop { - pubkey: alice_node_id, - node_features: Features::empty(), - fee_msat: amt_msat, - cltv_expiry_delta: 24 + 39, - }, - ], - hops: vec![BlindedHop{ - // Fake public key - blinded_node_id: alice_node_id, - encrypted_payload: vec![], - }], - blinding_point: alice_node_id, - excess_final_cltv_expiry_delta: 39, - final_value_msat: amt_msat, - }) - }], + let placeholder_tail = fwd_tail(); + let mut route = Route { + paths: vec![ + Path { + hops: build_path_hops(bob_node_id, alice_bob_scid, bob_carol_scid), + blinded_tail: Some(placeholder_tail.clone()), + }, + Path { + hops: build_path_hops(barry_node_id, alice_barry_scid, barry_carol_scid), + blinded_tail: Some(placeholder_tail), + }, + ], route_params: None, }; - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0)).unwrap(); + let cur_height = nodes[0].best_block_info().1 + 1; + let payment_id = PaymentId(payment_hash.0); + let onion = RecipientOnionFields::secret_only(payment_secret, total_amt); + let session_privs = nodes[0] + .node + .test_add_new_pending_payment(payment_hash, onion.clone(), payment_id, &route) + .unwrap(); - check_added_monitors(&nodes[0], 1); + route.paths[0].blinded_tail = Some(fwd_tail()); + route.paths[1].blinded_tail = Some(fwd_tail()); + + for (i, path) in route.paths.iter().enumerate() { + nodes[0] + .node + .test_send_payment_along_path( + path, + &payment_hash, + onion.clone(), + cur_height, + payment_id, + &None, + session_privs[i], + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + } let mut events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - let first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + assert_eq!(events.len(), 2); + let ev_bob = remove_first_msg_event_to_node(&bob_node_id, &mut events); + let ev_barry = remove_first_msg_event_to_node(&barry_node_id, &mut events); + (payment_hash, per_path_amt, ev_bob, ev_barry) +} - let route: &[&Node] = &[&nodes[1], &nodes[2]]; - let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event) - .with_payment_preimage(payment_preimage) - .without_claimable_event() - .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); +/// How an incomplete trampoline MPP times out (if at all). +enum TrampolineTimeout { + /// Tick timers until MPP timeout fires. + Ticks, + /// Mine blocks until on-chain CLTV timeout fires. + OnChain, +} + +fn do_trampoline_mpp_test(timeout: Option) { + let chanmon_cfgs = create_chanmon_cfgs(4); + let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &vec![None; 4]); + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + + let (payment_hash, per_path_amt, ev_bob, ev_barry) = send_trampoline_mpp_payment(&nodes); + let send_both = timeout.is_none(); + + let bob_path: &[&Node] = &[&nodes[1], &nodes[2]]; + let barry_path: &[&Node] = &[&nodes[3], &nodes[2]]; + + // Pass first part along Alice -> Bob -> Carol. + let args = PassAlongPathArgs::new(&nodes[0], bob_path, per_path_amt, payment_hash, ev_bob) + .without_claimable_event(); 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); + // Either complete the MPP (triggering trampoline rejection) or trigger a timeout. + let expected_reason = match timeout { + None => { + let args = + PassAlongPathArgs::new(&nodes[0], barry_path, per_path_amt, payment_hash, ev_barry) + .without_clearing_recipient_events(); + do_pass_along_path(args); + LocalHTLCFailureReason::TemporaryTrampolineFailure + }, + Some(TrampolineTimeout::Ticks) => { + for _ in 0..MPP_TIMEOUT_TICKS { + nodes[2].node.timer_tick_occurred(); + } + LocalHTLCFailureReason::MPPTimeout + }, + Some(TrampolineTimeout::OnChain) => { + let current_height = nodes[2].best_block_info().1; + let send_height = nodes[0].best_block_info().1; + let htlc_cltv = send_height + 1 + 48 + 42 + 70; + connect_blocks(&nodes[2], htlc_cltv - HTLC_FAIL_BACK_BUFFER - current_height); + LocalHTLCFailureReason::CLTVExpiryTooSoon + }, + }; + + // Carol rejects the trampoline forward (either after MPP completion or timeout). + let events = nodes[2].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + crate::events::Event::HTLCHandlingFailed { + ref failure_type, ref failure_reason, .. + } => { + assert_eq!(failure_type, &HTLCHandlingFailureType::TrampolineForward {}); + match failure_reason { + Some(crate::events::HTLCHandlingFailureReason::Local { reason }) => { + assert_eq!(*reason, expected_reason) + }, + Some(_) | None => panic!("expected failure_reason for failed trampoline"), + } + }, + _ => panic!("Unexpected destination"), } - { - let unblinded_node_updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); - nodes[0].node.handle_update_fail_htlc( - nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] - ); - do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false); + expect_and_process_pending_htlcs(&nodes[2], false); + assert!(nodes[2].node.get_and_clear_pending_events().is_empty()); + + // Propagate failures back through each forwarded path to Alice. + let both: [&[&Node]; 2] = [bob_path, barry_path]; + let one: [&[&Node]; 1] = [bob_path]; + let forwarded: &[&[&Node]] = if send_both { &both } else { &one }; + let carol_id = nodes[2].node.get_our_node_id(); + check_added_monitors(&nodes[2], forwarded.len()); + let mut carol_msgs = nodes[2].node.get_and_clear_pending_msg_events(); + assert_eq!(carol_msgs.len(), forwarded.len()); + for path in forwarded { + let hop = path[0]; + let hop_id = hop.node.get_our_node_id(); + let ev = remove_first_msg_event_to_node(&hop_id, &mut carol_msgs); + let updates = match ev { + MessageSendEvent::UpdateHTLCs { updates, .. } => updates, + _ => panic!("Expected UpdateHTLCs"), + }; + hop.node.handle_update_fail_htlc(carol_id, &updates.update_fail_htlcs[0]); + do_commitment_signed_dance(hop, &nodes[2], &updates.commitment_signed, true, false); + + let fwd = get_htlc_update_msgs(hop, &nodes[0].node.get_our_node_id()); + nodes[0].node.handle_update_fail_htlc(hop_id, &fwd.update_fail_htlcs[0]); + do_commitment_signed_dance(&nodes[0], hop, &fwd.commitment_signed, false, false); } - { - // 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]); - expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + + // Check Alice's failure events. + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), if send_both { 3 } else { 1 }); + for ev in &events[..forwarded.len()] { + match ev { + Event::PaymentPathFailed { payment_hash: h, payment_failed_permanently, .. } => { + assert_eq!(*h, payment_hash); + assert!(!payment_failed_permanently); + }, + _ => panic!("Expected PaymentPathFailed, got {:?}", ev), + } } + if send_both { + match &events[2] { + Event::PaymentFailed { payment_hash: h, reason, .. } => { + assert_eq!(*h, Some(payment_hash)); + assert_eq!(*reason, Some(PaymentFailureReason::RetriesExhausted)); + }, + _ => panic!("Expected PaymentFailed, got {:?}", events[2]), + } + + // Verify no spurious timeout fires after the MPP set was dispatched. + for _ in 0..(MPP_TIMEOUT_TICKS * 3) { + nodes[2].node.timer_tick_occurred(); + } + assert!(nodes[2].node.get_and_clear_pending_events().is_empty()); + } +} + +#[test] +fn test_trampoline_mpp_accumulation() { + do_trampoline_mpp_test(None); + do_trampoline_mpp_test(Some(TrampolineTimeout::Ticks)); + do_trampoline_mpp_test(Some(TrampolineTimeout::OnChain)); } diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index fbf2a4caa9f..ada5421387e 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -52,8 +52,8 @@ use crate::ln::channel_state::{ use crate::ln::channelmanager::{ self, BlindedFailure, ChannelReadyOrder, FundingConfirmedMessage, HTLCFailureMsg, HTLCPreviousHopData, HTLCSource, OpenChannelMessage, PaymentClaimDetails, PendingHTLCInfo, - PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, TrustedChannelFeatures, BREAKDOWN_TIMEOUT, - MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, + PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, TrampolineDispatch, TrustedChannelFeatures, + BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, }; use crate::ln::funding::{ FeeRateAdjustmentError, FundingContribution, FundingTemplate, FundingTxInput, PriorContribution, @@ -357,6 +357,15 @@ enum InboundUpdateAdd { blinded_failure: Option, outbound_hop: OutboundHop, }, + /// This inbound HTLC is a forward that was irrevocably committed to outbound edge(s) as part + /// of a trampoline forward, allowing its onion to be pruned and no longer persisted. + /// + /// Contains data that is useful if we need to fail or claim this HTLC backwards after a + /// restart and it's missing in the outbound edge. + TrampolineForwarded { + previous_hop_data: Vec, + outbound_hops: Vec<(OutboundHop, TrampolineDispatch)>, + }, /// This HTLC was received pre-LDK 0.3, before we started persisting the onion for inbound /// committed HTLCs. Legacy, @@ -374,6 +383,10 @@ impl_writeable_tlv_based_enum_upgradable!(InboundUpdateAdd, (6, trampoline_shared_secret, option), (8, blinded_failure, option), }, + (6, TrampolineForwarded) => { + (0, previous_hop_data, required_vec), + (2, outbound_hops, required_vec), + }, ); impl_writeable_for_vec!(&InboundUpdateAdd); @@ -1197,7 +1210,7 @@ pub(super) struct MonitorRestoreUpdates { /// The sources of outbound HTLCs that were forwarded and irrevocably committed on this channel /// (the outbound edge), along with their outbound amounts. Useful to store in the inbound HTLC /// to ensure it gets resolved. - pub committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>, + pub committed_outbound_htlc_sources: Vec<(HTLCSource, u64)>, } /// The return value of `signer_maybe_unblocked` @@ -7988,7 +8001,7 @@ where /// when reconstructing the set of pending HTLCs when deserializing the `ChannelManager`. pub(super) fn inbound_forwarded_htlcs( &self, - ) -> impl Iterator + '_ { + ) -> impl Iterator)> + '_ { // We don't want to return an HTLC as needing processing if it already has a resolution that's // pending in the holding cell. let htlc_resolution_in_holding_cell = |id: u64| -> bool { @@ -8037,7 +8050,32 @@ where counterparty_node_id: Some(counterparty_node_id), cltv_expiry: Some(htlc.cltv_expiry), }; - Some((htlc.payment_hash, prev_hop_data, *outbound_hop)) + Some(( + htlc.payment_hash, + vec![(HTLCSource::PreviousHopData(prev_hop_data), *outbound_hop)], + )) + }, + InboundHTLCState::Committed { + update_add_htlc: + InboundUpdateAdd::TrampolineForwarded { previous_hop_data, outbound_hops }, + } => { + if htlc_resolution_in_holding_cell(htlc.htlc_id) { + return None; + } + let trampoline_sources: Vec<(HTLCSource, OutboundHop)> = outbound_hops + .iter() + .map(|(hop, dispatch)| { + ( + HTLCSource::TrampolineForward { + previous_hop_data: previous_hop_data.clone(), + outbound_payment: Some(dispatch.clone()), + }, + *hop, + ) + }) + .collect(); + + Some((htlc.payment_hash, trampoline_sources)) }, _ => None, }) @@ -8048,12 +8086,12 @@ where /// present in the outbound edge, or else we'll double-forward. pub(super) fn outbound_htlc_forwards( &self, - ) -> impl Iterator + '_ { + ) -> impl Iterator + '_ { let holding_cell_outbounds = self.context.holding_cell_htlc_updates.iter().filter_map(|htlc| match htlc { HTLCUpdateAwaitingACK::AddHTLC { source, payment_hash, .. } => match source { - HTLCSource::PreviousHopData(prev_hop_data) => { - Some((*payment_hash, prev_hop_data.clone())) + HTLCSource::PreviousHopData(_) | HTLCSource::TrampolineForward { .. } => { + Some((*payment_hash, source.clone())) }, _ => None, }, @@ -8061,8 +8099,8 @@ where }); let committed_outbounds = self.context.pending_outbound_htlcs.iter().filter_map(|htlc| match &htlc.source { - HTLCSource::PreviousHopData(prev_hop_data) => { - Some((htlc.payment_hash, prev_hop_data.clone())) + HTLCSource::PreviousHopData(_) | HTLCSource::TrampolineForward { .. } => { + Some((htlc.payment_hash, htlc.source.clone())) }, _ => None, }); @@ -8087,20 +8125,66 @@ where /// This inbound HTLC was irrevocably forwarded to the outbound edge, so we no longer need to /// persist its onion. pub(super) fn prune_inbound_htlc_onion( - &mut self, htlc_id: u64, prev_hop_data: &HTLCPreviousHopData, - outbound_hop_data: OutboundHop, + &mut self, htlc_id: u64, htlc_source: &HTLCSource, outbound_hop_data: OutboundHop, ) { for htlc in self.context.pending_inbound_htlcs.iter_mut() { + // TODO: all these returns are super mif if htlc.htlc_id == htlc_id { - if let InboundHTLCState::Committed { ref mut update_add_htlc } = htlc.state { - *update_add_htlc = InboundUpdateAdd::Forwarded { - incoming_packet_shared_secret: prev_hop_data.incoming_packet_shared_secret, - phantom_shared_secret: prev_hop_data.phantom_shared_secret, - trampoline_shared_secret: prev_hop_data.trampoline_shared_secret, - blinded_failure: prev_hop_data.blinded_failure, - outbound_hop: outbound_hop_data, - }; - return; + match &mut htlc.state { + InboundHTLCState::Committed { + update_add_htlc: InboundUpdateAdd::TrampolineForwarded { outbound_hops, .. }, + } => { + if let HTLCSource::TrampolineForward { + outbound_payment: Some(trampoline_dispatch), + .. + } = htlc_source + { + if !outbound_hops.iter().any(|(_, dispatch)| { + dispatch.session_priv == trampoline_dispatch.session_priv + }) { + outbound_hops.push((outbound_hop_data, trampoline_dispatch.clone())) + } + return; + } else { + debug_assert!(false, "prune inbound onion called for trampoline with no dispatch or on non-trampoline inbound"); + return; + } + }, + InboundHTLCState::Committed { update_add_htlc } => { + *update_add_htlc = match htlc_source { + HTLCSource::PreviousHopData(prev_hop_data) => { + InboundUpdateAdd::Forwarded { + incoming_packet_shared_secret: prev_hop_data + .incoming_packet_shared_secret, + phantom_shared_secret: prev_hop_data.phantom_shared_secret, + trampoline_shared_secret: prev_hop_data + .trampoline_shared_secret, + blinded_failure: prev_hop_data.blinded_failure, + outbound_hop: outbound_hop_data, + } + }, + HTLCSource::TrampolineForward { + previous_hop_data, + outbound_payment, + } => { + InboundUpdateAdd::TrampolineForwarded { + previous_hop_data: previous_hop_data.to_vec(), + outbound_hops: vec![(outbound_hop_data, outbound_payment + .clone() // TODO: no clone / expect + .expect("trampoline shouldn't be pruned with no payment data"))], + } + }, + _ => { + debug_assert!( + false, + "outbound route should not prune inbound htlc" + ); + return; + }, + }; + return; + }, + _ => {}, } } } @@ -9787,8 +9871,8 @@ where mem::swap(&mut pending_update_adds, &mut self.context.monitor_pending_update_adds); let committed_outbound_htlc_sources = self.context.pending_outbound_htlcs.iter().filter_map(|htlc| { if let &OutboundHTLCState::LocalAnnounced(_) = &htlc.state { - if let HTLCSource::PreviousHopData(prev_hop_data) = &htlc.source { - return Some((prev_hop_data.clone(), htlc.amount_msat)) + if let HTLCSource::PreviousHopData(_) | HTLCSource::TrampolineForward { .. } = &htlc.source { + return Some((htlc.source.clone(), htlc.amount_msat)) } } None diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 570639d8995..5399b275e9a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -89,9 +89,10 @@ use crate::ln::outbound_payment; #[cfg(any(test, feature = "_externalize_tests"))] use crate::ln::outbound_payment::PaymentSendFailure; use crate::ln::outbound_payment::{ - Bolt11PaymentError, Bolt12PaymentError, OutboundPayments, PendingOutboundPayment, - ProbeSendFailure, RecipientCustomTlvs, RecipientOnionFields, Retry, RetryableInvoiceRequest, - RetryableSendFailure, SendAlongPathArgs, StaleExpiration, + Bolt11PaymentError, Bolt12PaymentError, NextTrampolineHopInfo, OutboundPayments, + PendingOutboundPayment, ProbeSendFailure, RecipientCustomTlvs, RecipientOnionFields, Retry, + RetryableInvoiceRequest, RetryableSendFailure, SendAlongPathArgs, StaleExpiration, + TrampolineForwardInfo, }; use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; @@ -116,14 +117,14 @@ use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; use crate::routing::gossip::NodeId; use crate::routing::router::{ BlindedTail, FixedRouter, InFlightHtlcs, Path, Payee, PaymentParameters, Route, - RouteParameters, RouteParametersConfig, Router, + RouteParameters, RouteParametersConfig, Router, DEFAULT_MAX_PATH_COUNT, + MAX_PATH_LENGTH_ESTIMATE, }; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider}; -#[cfg(any(feature = "_test_utils", test))] -use crate::types::features::Bolt11InvoiceFeatures; use crate::types::features::{ - Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures, + Bolt11InvoiceFeatures, Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, + InitFeatures, NodeFeatures, }; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::types::string::UntrustedString; @@ -237,6 +238,12 @@ pub enum PendingHTLCRouting { blinded: Option, /// The absolute CLTV of the inbound HTLC incoming_cltv_expiry: u32, + /// MPP data for accumulating incoming HTLCs before dispatching an outbound payment. + incoming_multipath_data: Option, + /// The amount that the next trampoline is expecting to receive. + next_trampoline_amt_msat: u64, + /// The CLTV expiry height that the next trampoline is expecting to receive. + next_trampoline_cltv_expiry: u32, }, /// The onion indicates that this is a payment for an invoice (supposedly) generated by us. /// @@ -521,7 +528,7 @@ enum OnionPayload { } #[derive(PartialEq, Eq)] -struct MppPart { +pub(super) struct MppPart { prev_hop: HTLCPreviousHopData, cltv_expiry: u32, /// The amount (in msats) of this MPP part @@ -536,6 +543,20 @@ struct MppPart { } impl MppPart { + #[cfg(test)] + pub(super) fn new( + prev_hop: HTLCPreviousHopData, value: u64, sender_intended_value: u64, cltv_expiry: u32, + ) -> Self { + MppPart { + prev_hop, + cltv_expiry, + value, + sender_intended_value, + timer_ticks: 0, + total_value_received: None, + } + } + /// Returns a boolean indicating whether the HTLC has timed out on chain, accounting for a buffer /// that gives us time to resolve it. fn check_onchain_timeout(&self, height: u32) -> bool { @@ -858,7 +879,6 @@ mod fuzzy_channelmanager { /// 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], /// Track outbound payment details once the payment has been dispatched, will be `None` /// when waiting for incoming MPP to accumulate. outbound_payment: Option, @@ -963,14 +983,9 @@ impl core::hash::Hash for HTLCSource { first_hop_htlc_msat.hash(hasher); bolt12_invoice.hash(hasher); }, - HTLCSource::TrampolineForward { - previous_hop_data, - incoming_trampoline_shared_secret, - outbound_payment, - } => { + HTLCSource::TrampolineForward { previous_hop_data, outbound_payment } => { 2u8.hash(hasher); previous_hop_data.hash(hasher); - incoming_trampoline_shared_secret.hash(hasher); if let Some(payment) = outbound_payment { payment.payment_id.hash(hasher); payment.path.hash(hasher); @@ -1311,6 +1326,12 @@ fn check_mpp_timeout<'a>( timed_out } +/// Tracks trampoline HTLCs being accumulated before forwarding. +struct TrampolinePayment { + onion_fields: RecipientOnionFields, + htlcs: Vec, +} + /// Represent the channel funding transaction type. enum FundingType { /// This variant is useful when we want LDK to validate the funding transaction and @@ -1610,7 +1631,7 @@ enum PostMonitorUpdateChanResume { htlc_forwards: Vec, finalized_claimed_htlcs: Vec<(HTLCSource, Option)>, failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>, - committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>, + committed_outbound_htlc_sources: Vec<(HTLCSource, u64)>, }, } @@ -2886,6 +2907,16 @@ pub struct ChannelManager< /// [`ClaimablePayments`]' individual field docs for more info. claimable_payments: Mutex, + /// The sets of trampoline payments which are in the process of being accumulated on inbound + /// channel(s). + /// + /// Note that this map is currently not persisted, as there is ongoing work to refactor our + /// reload from disk depending only on channel managers. Until proper restart logic is added + /// we will "forget" about any HTLCs that are pending in this map on restart waiting for MPP + /// timeout. For this reason, we should not forward any trampoline HTLCs until properly + /// implemented. + awaiting_trampoline_forwards: Mutex>, + /// The set of outbound SCID aliases across all our channels, including unconfirmed channels /// and some closed channels which reached a usable state prior to being closed. This is used /// only to avoid duplicates, and is not persisted explicitly to disk, but rebuilt from the @@ -3712,6 +3743,7 @@ impl< forward_htlcs: Mutex::new(new_hash_map()), decode_update_add_htlcs: Mutex::new(new_hash_map()), claimable_payments: Mutex::new(ClaimablePayments { claimable_payments: new_hash_map(), pending_claiming_payments: new_hash_map() }), + awaiting_trampoline_forwards: Mutex::new(new_hash_map()), pending_intercepted_htlcs: Mutex::new(new_hash_map()), short_to_chan_info: FairRwLock::new(new_hash_map()), @@ -5156,6 +5188,7 @@ impl< fn can_forward_htlc_should_intercept( &self, msg: &msgs::UpdateAddHTLC, prev_chan_public: bool, next_hop: &NextPacketDetails, ) -> Result { + let cur_height = self.best_block.read().unwrap().height + 1; let outgoing_scid = match next_hop.outgoing_connector { HopConnector::ShortChannelId(scid) => scid, HopConnector::Dummy => { @@ -5163,8 +5196,34 @@ impl< debug_assert!(false, "Dummy hop reached HTLC handling."); return Err(LocalHTLCFailureReason::InvalidOnionPayload); }, + // We can't make forwarding checks on trampoline forwards where we don't know the + // outgoing channel on receipt of the incoming htlc. Our trampoline logic will check + // our required delta and fee later on, so here we just check that the forwarding node + // did not "skim" off some of the sender's intended fee/cltv. HopConnector::Trampoline(_) => { - return Err(LocalHTLCFailureReason::InvalidTrampolineForward); + // We do not yet support reloading our trampoline HTLCs on restart, so we just + // fail them for now (except in tests). + #[cfg(not(test))] + { + return Err(LocalHTLCFailureReason::InvalidTrampolineForward); + } + + #[cfg(test)] + { + if msg.amount_msat < next_hop.outgoing_amt_msat { + return Err(LocalHTLCFailureReason::FeeInsufficient); + } + + check_incoming_htlc_cltv( + cur_height, + next_hop.outgoing_cltv_value, + msg.cltv_expiry, + 0, + )?; + + // TODO: add interception flag specifically for trampoline + return Ok(false); + } }, }; // TODO: We do the fake SCID namespace check a bunch of times here (and indirectly via @@ -5203,7 +5262,6 @@ impl< }, }; - let cur_height = self.best_block.read().unwrap().height + 1; check_incoming_htlc_cltv( cur_height, next_hop.outgoing_cltv_value, @@ -5269,7 +5327,8 @@ impl< #[rustfmt::skip] fn construct_pending_htlc_fail_msg<'a>( &self, msg: &msgs::UpdateAddHTLC, counterparty_node_id: &PublicKey, - shared_secret: [u8; 32], inbound_err: InboundHTLCErr + shared_secret: [u8; 32], trampoline_shared_secret: &Option<[u8; 32]>, + inbound_err: InboundHTLCErr, ) -> HTLCFailureMsg { let logger = WithContext::from(&self.logger, Some(*counterparty_node_id), Some(msg.channel_id), Some(msg.payment_hash)); log_info!(logger, "Failed to accept/forward incoming HTLC: {}", inbound_err.msg); @@ -5286,7 +5345,7 @@ impl< } let failure = HTLCFailReason::reason(inbound_err.reason, inbound_err.err_data.to_vec()) - .get_encrypted_failure_packet(&shared_secret, &None); + .get_encrypted_failure_packet(&shared_secret, trampoline_shared_secret); return HTLCFailureMsg::Relay(msgs::UpdateFailHTLC { channel_id: msg.channel_id, htlc_id: msg.htlc_id, @@ -5444,6 +5503,7 @@ impl< keysend_preimage, invoice_request: None, bolt12_invoice: None, + trampoline_forward_info: None, session_priv_bytes, hold_htlc_at_next_hop: false, }) @@ -5459,6 +5519,7 @@ impl< keysend_preimage, invoice_request, bolt12_invoice, + trampoline_forward_info, session_priv_bytes, hold_htlc_at_next_hop, } = args; @@ -5474,18 +5535,32 @@ impl< Some(*payment_hash), payment_id, ); - let (onion_packet, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion( - &self.secp_ctx, - &path, - &session_priv, - 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 { + onion_utils::create_trampoline_forward_onion( + &self.secp_ctx, + &path, + &session_priv, + payment_hash, + recipient_onion, + keysend_preimage, + &trampoline_forward_info.next_hop_info, + prng_seed, + ) + } else { + onion_utils::create_payment_onion( + &self.secp_ctx, + &path, + &session_priv, + recipient_onion, + cur_height, + payment_hash, + keysend_preimage, + invoice_request, + prng_seed, + ) + }; + + let (onion_packet, htlc_msat, htlc_cltv) = onion_result.map_err(|e| { log_error!(logger, "Failed to build an onion for path"); e })?; @@ -5529,12 +5604,24 @@ impl< }); } let funding_txo = chan.funding.get_funding_txo().unwrap(); - let htlc_source = HTLCSource::OutboundRoute { - path: path.clone(), - session_priv: session_priv.clone(), - first_hop_htlc_msat: htlc_msat, - payment_id, - bolt12_invoice: bolt12_invoice.cloned(), + 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, + bolt12_invoice: bolt12_invoice.cloned(), + }, + Some(trampoline_forward_info) => HTLCSource::TrampolineForward { + previous_hop_data: trampoline_forward_info + .previous_hop_data + .clone(), + outbound_payment: Some(TrampolineDispatch { + payment_id, + path: path.clone(), + session_priv, + }), + }, }; let send_res = chan.send_htlc_and_commit( htlc_msat, @@ -5750,6 +5837,20 @@ impl< self.pending_outbound_payments.test_set_payment_metadata(payment_id, new_payment_metadata); } + #[cfg(test)] + pub(super) fn test_handle_trampoline_htlc( + &self, mpp_part: MppPart, onion_fields: RecipientOnionFields, payment_hash: PaymentHash, + next_hop_info: NextTrampolineHopInfo, next_node_id: PublicKey, + ) -> Result<(), (HTLCSource, onion_utils::HTLCFailReason)> { + self.handle_trampoline_htlc( + mpp_part, + onion_fields, + payment_hash, + next_hop_info, + next_node_id, + ) + } + /// Pays a [`Bolt11Invoice`] associated with the `payment_id`. See [`Self::send_payment`] for more info. /// /// # Payment Id @@ -7578,6 +7679,16 @@ impl< } } + // Extract the trampoline shared secret before `next_hop` is consumed, + // so we can double-encrypt errors for trampoline receives. + let trampoline_shared_secret = match &next_hop { + onion_utils::Hop::TrampolineReceive { trampoline_shared_secret, .. } + | onion_utils::Hop::TrampolineBlindedReceive { + trampoline_shared_secret, .. + } => Some(trampoline_shared_secret.secret_bytes()), + _ => None, + }; + match self.get_pending_htlc_info( &update_add_htlc, shared_secret, @@ -7683,6 +7794,7 @@ impl< &update_add_htlc, &incoming_counterparty_node_id, shared_secret, + &trampoline_shared_secret, inbound_err, ); htlc_fails.push((htlc_fail, failure_type, htlc_failure)); @@ -8424,13 +8536,8 @@ impl< receiver_node_id: Some(receiver_node_id), payment_hash, purpose, - amount_msat: claimable_payment - .htlcs - .iter() - .map(|htlc| htlc.mpp_part.value) - .sum(), - counterparty_skimmed_fee_msat: claimable_payment - .total_counterparty_skimmed_msat(), + amount_msat, + counterparty_skimmed_fee_msat, receiving_channel_ids: claimable_payment.receiving_channel_ids(), claim_deadline, onion_fields: Some(claimable_payment.onion_fields.clone()), @@ -8451,6 +8558,197 @@ impl< } } + // Handles the addition of a HTLC associated with a trampoline forward that we need to accumulate + // on the incoming link before forwarding onwards. If the HTLC is failed, it returns the source + // and error that should be used to fail the HTLC(s) back. + fn handle_trampoline_htlc( + &self, mpp_part: MppPart, onion_fields: RecipientOnionFields, payment_hash: PaymentHash, + next_hop_info: NextTrampolineHopInfo, next_node_id: PublicKey, + ) -> Result<(), (HTLCSource, HTLCFailReason)> { + let mut trampoline_payments = self.awaiting_trampoline_forwards.lock().unwrap(); + + // We should not fail if we're adding the first htlc to a ClaimablePayment (as our + // validation compares fields across parts, and our first part can't overflow maximum + // msats because each htlc's amount is individually validated - overflow is only possible + // with multiple parts). + let mut first_trampoline_htlc = false; + let trampoline_payment = trampoline_payments.entry(payment_hash).or_insert_with(|| { + first_trampoline_htlc = true; + TrampolinePayment { htlcs: Vec::new(), onion_fields: onion_fields.clone() } + }); + + // TODO: add restriction to specification that trampoline should be consistent across + // MPP parts? Currently, we'll accept a MPP trampoline payments that specify different + // next_node_id destinations (just forwarding to the last one that arrives). + + // If MPP hasn't fully arrived yet, return early (saving indentation below). + let prev_hop = mpp_part.prev_hop.clone(); + match self.check_incoming_mpp_part( + &mut trampoline_payment.htlcs, + &mut trampoline_payment.onion_fields, + mpp_part, + onion_fields, + payment_hash, + ) { + Ok(false) => return Ok(()), + Err(()) => { + debug_assert!( + !first_trampoline_htlc, + "first trampoline HTLC should not fail check_incoming_mpp_part" + ); + return Err(( + // When we couldn't add a new HTLC, we just fail back our last received htlc, + // allowing others to wait for more MPP parts to arrive. + HTLCSource::TrampolineForward { + previous_hop_data: vec![prev_hop], + outbound_payment: None, + }, + HTLCFailReason::reason( + LocalHTLCFailureReason::InvalidTrampolineForward, + vec![], + ), + )); + }, + Ok(true) => {}, + }; + + let trampoline_payment = trampoline_payments + .remove(&payment_hash) + .expect("payment was just accessed via entry()"); + + let incoming_amt_msat: u64 = trampoline_payment.htlcs.iter().map(|h| h.value).sum(); + let incoming_cltv_expiry = + trampoline_payment.htlcs.iter().map(|h| h.cltv_expiry).min().unwrap(); + + let (forwarding_fee_proportional_millionths, forwarding_fee_base_msat, cltv_delta) = { + let config = self.config.read().unwrap(); + ( + config.channel_config.forwarding_fee_proportional_millionths, + config.channel_config.forwarding_fee_base_msat, + config.channel_config.cltv_expiry_delta as u32, + ) + }; + + let proportional_fee = (forwarding_fee_proportional_millionths as u128 + * next_hop_info.amount_msat as u128 + / 1_000_000) as u64; + let our_forwarding_fee_msat = proportional_fee + forwarding_fee_base_msat as u64; + + let trampoline_source = || -> HTLCSource { + HTLCSource::TrampolineForward { + previous_hop_data: trampoline_payment + .htlcs + .iter() + .map(|htlc| htlc.prev_hop.clone()) + .collect(), + outbound_payment: None, + } + }; + let trampoline_failure = || -> HTLCFailReason { + let mut err_data = Vec::with_capacity(10); + err_data.extend_from_slice(&forwarding_fee_base_msat.to_be_bytes()); + err_data.extend_from_slice(&forwarding_fee_proportional_millionths.to_be_bytes()); + err_data.extend_from_slice(&(cltv_delta as u16).to_be_bytes()); + HTLCFailReason::reason( + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient, + err_data, + ) + }; + + let max_total_routing_fee_msat = match our_forwarding_fee_msat + .checked_add(next_hop_info.amount_msat) + .and_then(|total| incoming_amt_msat.checked_sub(total)) + { + Some(amount) => amount, + None => { + return Err((trampoline_source(), trampoline_failure())); + }, + }; + + let max_total_cltv_expiry_delta = match next_hop_info + .cltv_expiry_height + .checked_add(cltv_delta) + .and_then(|total| incoming_cltv_expiry.checked_sub(total)) + { + Some(cltv_delta) => cltv_delta, + None => { + return Err((trampoline_source(), trampoline_failure())); + }, + }; + + // Assume any Trampoline node supports MPP + let mut recipient_features = Bolt11InvoiceFeatures::empty(); + recipient_features.set_basic_mpp_optional(); + + let route_parameters = RouteParameters { + payment_params: PaymentParameters { + payee: Payee::Clear { + node_id: next_node_id, // TODO: this can be threaded through from above + route_hints: vec![], + features: Some(recipient_features), + // When sending a trampoline payment, we assume that the original sender has + // baked a final cltv into our instructions. + final_cltv_expiry_delta: 0, + }, + expiry_time: None, + max_total_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: next_hop_info.amount_msat, + max_total_routing_fee_msat: Some(max_total_routing_fee_msat), + }; + + #[cfg(not(any(test, feature = "_test_utils")))] + let retry_strategy = Retry::Attempts(3); + #[cfg(any(test, feature = "_test_utils"))] + let retry_strategy = Retry::Attempts(0); + + log_debug!( + self.logger, + "Attempting to forward trampoline payment that pays us {} with {} fee budget ({} total, {} cltv max)", + our_forwarding_fee_msat, + max_total_routing_fee_msat, + next_hop_info.amount_msat, + max_total_cltv_expiry_delta, + ); + let result = self.pending_outbound_payments.send_payment_for_trampoline_forward( + PaymentId(payment_hash.0), + payment_hash, + TrampolineForwardInfo { + next_hop_info, + previous_hop_data: trampoline_payment + .htlcs + .iter() + .map(|htlc| htlc.prev_hop.clone()) + .collect(), + forwading_fee_msat: our_forwarding_fee_msat, + }, + retry_strategy, + 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.pending_events, + |args| self.send_payment_along_path(args), + &WithContext::from(&self.logger, None, None, Some(payment_hash)), + ); + + if let Err(_retryable_send_failure) = result { + return Err(( + trampoline_source(), + HTLCFailReason::reason(LocalHTLCFailureReason::TemporaryTrampolineFailure, vec![]), + )); + }; + Ok(()) + } + fn process_receive_htlcs( &self, pending_forwards: &mut Vec, new_events: &mut VecDeque<(Event, Option)>, @@ -8485,6 +8783,7 @@ impl< has_recipient_created_payment_secret, invoice_request_opt, trampoline_shared_secret, + trampoline_info, ) = match routing { PendingHTLCRouting::Receive { payment_data, @@ -8513,6 +8812,7 @@ impl< true, None, trampoline_shared_secret, + None, ) }, PendingHTLCRouting::ReceiveKeysend { @@ -8547,6 +8847,62 @@ impl< has_recipient_created_payment_secret, invoice_request, None, + None, + ) + }, + PendingHTLCRouting::TrampolineForward { + trampoline_shared_secret: incoming_trampoline_shared_secret, + onion_packet, + node_id: next_trampoline, + blinded, + incoming_cltv_expiry, + incoming_multipath_data, + next_trampoline_amt_msat, + next_trampoline_cltv_expiry, + } => { + // Trampoline forwards only *need* to have MPP data if they're + // multi-part. + let onion_fields = match incoming_multipath_data { + Some(ref final_mpp) => RecipientOnionFields::secret_only( + final_mpp.payment_secret, + final_mpp.total_msat, + ), + None => RecipientOnionFields::spontaneous_empty(outgoing_amt_msat), + }; + + let next_hop_info = NextTrampolineHopInfo { + onion_packet, + 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() + }) + }), + amount_msat: next_trampoline_amt_msat, + cltv_expiry_height: next_trampoline_cltv_expiry, + }; + ( + incoming_cltv_expiry, + // Unused for trampoline forwards; MppPart is constructed + // directly below. + OnionPayload::Invoice { _legacy_hop_data: None }, + incoming_multipath_data, + None, + None, + onion_fields, + false, + None, + Some(incoming_trampoline_shared_secret), + Some((next_hop_info, next_trampoline)), ) }, _ => { @@ -8557,6 +8913,35 @@ impl< // if possible so that we don't prematurely mark MPP payments complete // if routing nodes overpay let value = incoming_amt_msat.unwrap_or(outgoing_amt_msat); + // For trampoline forwards, construct MppPart directly and handle separately + // from claimable HTLCs. + if let Some((next_hop_info, next_trampoline)) = trampoline_info { + let mpp_part = MppPart { + prev_hop, + cltv_expiry, + value, + sender_intended_value: outgoing_amt_msat, + timer_ticks: 0, + total_value_received: None, + }; + if let Err((htlc_source, failure_reason)) = self.handle_trampoline_htlc( + mpp_part, + onion_fields, + payment_hash, + next_hop_info, + next_trampoline, + ) { + failed_forwards.push(( + htlc_source, + payment_hash, + failure_reason, + HTLCHandlingFailureType::TrampolineForward {}, + )); + } + continue 'next_forwardable_htlc; + } + + // If we don't have a trampoline forward, we're dealing with a MPP receive. let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData { prev_outbound_scid_alias: prev_hop.prev_outbound_scid_alias, user_channel_id: prev_hop.user_channel_id, @@ -9074,6 +9459,26 @@ impl< }, ); + self.awaiting_trampoline_forwards.lock().unwrap().retain(|payment_hash, payment| { + if payment.htlcs.is_empty() { + debug_assert!(false); + return false; + } + let mpp_timeout = + check_mpp_timeout(payment.htlcs.iter_mut(), &payment.onion_fields); + if mpp_timeout { + let previous_hop_data = + payment.htlcs.drain(..).map(|claimable| claimable.prev_hop).collect(); + + timed_out_mpp_htlcs.push(( + HTLCSource::TrampolineForward { previous_hop_data, outbound_payment: None }, + *payment_hash, + HTLCHandlingFailureType::TrampolineForward {}, + )); + } + !mpp_timeout + }); + for (htlc_source, payment_hash, failure_type) in timed_out_mpp_htlcs.drain(..) { let failure_reason = LocalHTLCFailureReason::MPPTimeout; let reason = HTLCFailReason::from_failure_code(failure_reason); @@ -9378,73 +9783,102 @@ impl< None, )); }, - HTLCSource::TrampolineForward { - previous_hop_data, - incoming_trampoline_shared_secret, - .. - } => { - let decoded_onion_failure = - onion_error.decode_onion_failure(&self.secp_ctx, &self.logger, &source); - log_trace!( - WithContext::from(&self.logger, None, None, Some(*payment_hash)), - "Trampoline forward failed downstream on {}", - if let Some(scid) = decoded_onion_failure.short_channel_id { - scid.to_string() - } else { - "unknown channel".to_string() - }, - ); - let incoming_trampoline_shared_secret = Some(*incoming_trampoline_shared_secret); - - // TODO: when we receive a failure from a single outgoing trampoline HTLC, we don't - // necessarily want to fail all of our incoming HTLCs back yet. We may have other - // outgoing HTLCs that need to resolve first. This will be tracked in our - // pending_outbound_payments in a followup. - for current_hop_data in previous_hop_data { - let HTLCPreviousHopData { - prev_outbound_scid_alias, - htlc_id, - incoming_packet_shared_secret, - blinded_failure, - channel_id, - .. - } = current_hop_data; - 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 onion_error = HTLCFailReason::reason( + HTLCSource::TrampolineForward { previous_hop_data, outbound_payment, .. } => { + let trampoline_error = match outbound_payment { + Some(_) => self + .pending_outbound_payments + .trampoline_htlc_failed( + source, + payment_hash, + onion_error, + &self.secp_ctx, + &WithContext::from(&self.logger, None, None, Some(*payment_hash)), + ) + .map(|decoded| { + if let Some(onion_error_code) = decoded.onion_error_code { + log_trace!( + WithContext::from(&self.logger, None, None, Some(*payment_hash)), + "Received trampoline error intended for us: {:?}, returning TemporaryTrampolineFailure to original sender", + onion_error_code, + ); + HTLCFailReason::reason( + LocalHTLCFailureReason::TemporaryTrampolineFailure, + Vec::new(), + ) + } else if let Some(peeled_packet) = decoded.trampoline_peeled_packet { + log_trace!( + WithContext::from(&self.logger, None, None, Some(*payment_hash)), + "Received trampoline error intended for original sender, returning as-is", + ); + HTLCFailReason::from_onion_error_packet(peeled_packet) + } else { + log_trace!( + WithContext::from(&self.logger, None, None, Some(*payment_hash)), + "Received a malformed trampoline error, returning TemporaryTrampolineFailure to original sender", + ); + // Couldn't peel outer layers (e.g., packet too short or + // fail_malformed_htlc). Replace with our own error. + HTLCFailReason::reason( + LocalHTLCFailureReason::TemporaryTrampolineFailure, + Vec::new(), + ) + } + }), + None => Some(HTLCFailReason::reason( LocalHTLCFailureReason::TemporaryTrampolineFailure, Vec::new(), - ); - push_forward_htlcs_failure( - *prev_outbound_scid_alias, - get_htlc_forward_failure( - blinded_failure, - &onion_error, + )), + }; + + if let Some(err) = trampoline_error { + for current_hop_data in previous_hop_data { + let HTLCPreviousHopData { + prev_outbound_scid_alias, + htlc_id, incoming_packet_shared_secret, - &incoming_trampoline_shared_secret, - &None, - *htlc_id, - ), - ); - } + blinded_failure, + channel_id, + trampoline_shared_secret, + .. + } = current_hop_data; - // We only want to emit a single event for trampoline failures, so we do it once - // we've failed back all of our incoming HTLCs. - let mut pending_events = self.pending_events.lock().unwrap(); - pending_events.push_back(( - events::Event::HTLCHandlingFailed { - prev_channel_ids: previous_hop_data - .iter() - .map(|prev| prev.channel_id) - .collect(), - failure_type, - failure_reason: Some(onion_error.into()), - }, - None, - )); + 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, err, + ); + debug_assert!( + trampoline_shared_secret.is_some(), + "trampoline hop should have secret" + ); + push_forward_htlcs_failure( + *prev_outbound_scid_alias, + get_htlc_forward_failure( + blinded_failure, + &err, + incoming_packet_shared_secret, + &trampoline_shared_secret, + &None, + *htlc_id, + ), + ); + } + + // We only want to emit a single event for trampoline failures, so we do it once + // we've failed back all of our incoming HTLCs. + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::HTLCHandlingFailed { + prev_channel_ids: previous_hop_data + .iter() + .map(|prev| prev.channel_id) + .collect(), + failure_type, + failure_reason: Some(onion_error.into()), + }, + None, + )); + } }, } } @@ -10121,6 +10555,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); Some((source, hold_times)) + } else if let HTLCSource::TrampolineForward { .. } = source { + Some((source, Vec::new())) } else { None } @@ -10235,7 +10671,29 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ send_timestamp, ); }, - HTLCSource::TrampolineForward { previous_hop_data, .. } => { + HTLCSource::TrampolineForward { previous_hop_data, outbound_payment, .. } => { + let total_fee_earned_msat = match &outbound_payment { + Some(trampoline_dispatch) => { + let fee = self.pending_outbound_payments.claim_trampoline_forward( + &trampoline_dispatch.payment_id, + &trampoline_dispatch.session_priv, + from_onchain, + ); + debug_assert!( + fee.is_some(), + "Trampoline payment with unknown payment_id: {} settled", + trampoline_dispatch.payment_id + ); + fee + }, + None => { + debug_assert!( + false, + "Trampoline payment settled with no outbound payment dispatched" + ); + None + }, + }; // Only emit a single event for trampoline claims. let prev_htlcs: Vec = previous_hop_data.iter().map(Into::into).collect(); @@ -10254,10 +10712,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ user_channel_id: next_user_channel_id, node_id: Some(next_channel_counterparty_node_id), }], - // TODO: When trampoline payments are tracked in our - // pending_outbound_payments, we'll be able to lookup our total - // fee earnings. - total_fee_earned_msat: None, + total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx: from_onchain, // TODO: When trampoline payments are tracked in our @@ -10295,7 +10750,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ update_actions: Vec, htlc_forwards: Vec, finalized_claimed_htlcs: Vec<(HTLCSource, Option)>, failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>, - committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>, + committed_outbound_htlc_sources: Vec<(HTLCSource, u64)>, ) { // If the channel belongs to a batch funding transaction, the progress of the batch // should be updated as we have received funding_signed and persisted the monitor. @@ -10868,34 +11323,40 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn prune_persisted_inbound_htlc_onions( &self, outbound_channel_id: ChannelId, outbound_node_id: PublicKey, outbound_funding_txo: OutPoint, outbound_user_channel_id: u128, - committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>, + committed_outbound_htlc_sources: Vec<(HTLCSource, u64)>, ) { let per_peer_state = self.per_peer_state.read().unwrap(); for (source, outbound_amt_msat) in committed_outbound_htlc_sources { - let counterparty_node_id = match source.counterparty_node_id.as_ref() { - Some(id) => id, - None => continue, - }; - let mut peer_state = - match per_peer_state.get(counterparty_node_id).map(|state| state.lock().unwrap()) { + for previous_hop in source.previous_hop_data() { + let counterparty_node_id = match previous_hop.counterparty_node_id.as_ref() { + Some(id) => id, + None => continue, + }; + let mut peer_state = match per_peer_state + .get(counterparty_node_id) + .map(|state| state.lock().unwrap()) + { Some(peer_state) => peer_state, None => continue, }; - if let Some(chan) = - peer_state.channel_by_id.get_mut(&source.channel_id).and_then(|c| c.as_funded_mut()) - { - chan.prune_inbound_htlc_onion( - source.htlc_id, - &source, - OutboundHop { - amt_msat: outbound_amt_msat, - channel_id: outbound_channel_id, - node_id: outbound_node_id, - funding_txo: outbound_funding_txo, - user_channel_id: outbound_user_channel_id, - }, - ); + if let Some(chan) = peer_state + .channel_by_id + .get_mut(&previous_hop.channel_id) + .and_then(|c| c.as_funded_mut()) + { + chan.prune_inbound_htlc_onion( + previous_hop.htlc_id, + &source, + OutboundHop { + amt_msat: outbound_amt_msat, + channel_id: outbound_channel_id, + node_id: outbound_node_id, + funding_txo: outbound_funding_txo, + user_channel_id: outbound_user_channel_id, + }, + ); + } } } } @@ -16362,6 +16823,31 @@ impl< }, ); + self.awaiting_trampoline_forwards.lock().unwrap().retain(|payment_hash, payment| { + if payment.htlcs.is_empty() { + debug_assert!(false); + return false; + } + let htlc_timed_out = + payment.htlcs.iter().any(|htlc| htlc.check_onchain_timeout(height)); + if htlc_timed_out { + let previous_hop_data = + payment.htlcs.drain(..).map(|claimable| claimable.prev_hop).collect(); + + let failure_reason = LocalHTLCFailureReason::CLTVExpiryTooSoon; + timed_out_htlcs.push(( + HTLCSource::TrampolineForward { previous_hop_data, outbound_payment: None }, + *payment_hash, + HTLCFailReason::reason( + failure_reason, + self.get_htlc_inbound_temp_fail_data(failure_reason), + ), + HTLCHandlingFailureType::TrampolineForward {}, + )); + } + !htlc_timed_out + }); + let mut intercepted_htlcs = self.pending_intercepted_htlcs.lock().unwrap(); intercepted_htlcs.retain(|_, htlc| { if height >= htlc.forward_info.outgoing_cltv_value - HTLC_FAIL_BACK_BUFFER { @@ -17660,6 +18146,9 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting, (4, blinded, option), (6, node_id, required), (8, incoming_cltv_expiry, required), + (10, incoming_multipath_data, option), + (12, next_trampoline_amt_msat, required), + (14, next_trampoline_cltv_expiry, required), } ); @@ -17882,8 +18371,16 @@ impl Readable for HTLCSource { }) } 1 => Ok(HTLCSource::PreviousHopData(Readable::read(reader)?)), - // Note: we intentionally do not read HTLCSource::TrampolineForward because we do not - // want to allow downgrades with in-flight trampoline forwards. + 2 => { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (1, previous_hop_data, required_vec), + (3, outbound_payment, option), + }); + Ok(HTLCSource::TrampolineForward { + previous_hop_data: _init_tlv_based_struct_field!(previous_hop_data, required_vec), + outbound_payment, + }) + }, _ => Err(DecodeError::UnknownRequiredFeature), } } @@ -17916,16 +18413,11 @@ impl Writeable for HTLCSource { 1u8.write(writer)?; field.write(writer)?; }, - HTLCSource::TrampolineForward { - ref previous_hop_data, - incoming_trampoline_shared_secret, - ref outbound_payment, - } => { + HTLCSource::TrampolineForward { ref previous_hop_data, ref outbound_payment } => { 2u8.write(writer)?; write_tlv_fields!(writer, { (1, *previous_hop_data, required_vec), - (3, incoming_trampoline_shared_secret, required), - (5, outbound_payment, option), + (3, outbound_payment, option), }); }, } @@ -18887,35 +19379,38 @@ impl< // If the HTLC corresponding to `prev_hop_data` is present in `decode_update_add_htlcs`, remove it // from the map as it is already being stored and processed elsewhere. -fn dedup_decode_update_add_htlcs( +fn dedup_decode_update_add_htlcs<'a, L: Logger>( decode_update_add_htlcs: &mut HashMap>, - prev_hop_data: &HTLCPreviousHopData, removal_reason: &'static str, logger: &L, + previous_hops: impl Iterator, removal_reason: &'static str, + logger: &L, ) { - match decode_update_add_htlcs.entry(prev_hop_data.prev_outbound_scid_alias) { - hash_map::Entry::Occupied(mut update_add_htlcs) => { - update_add_htlcs.get_mut().retain(|update_add| { - let matches = update_add.htlc_id == prev_hop_data.htlc_id; - if matches { - let logger = WithContext::from( - logger, - prev_hop_data.counterparty_node_id, - Some(update_add.channel_id), - Some(update_add.payment_hash), - ); - log_info!( - logger, - "Removing pending to-decode HTLC with id {}: {}", - update_add.htlc_id, - removal_reason - ); + for prev_hop_data in previous_hops { + match decode_update_add_htlcs.entry(prev_hop_data.prev_outbound_scid_alias) { + hash_map::Entry::Occupied(mut update_add_htlcs) => { + update_add_htlcs.get_mut().retain(|update_add| { + let matches = update_add.htlc_id == prev_hop_data.htlc_id; + if matches { + let logger = WithContext::from( + logger, + prev_hop_data.counterparty_node_id, + Some(update_add.channel_id), + Some(update_add.payment_hash), + ); + log_info!( + logger, + "Removing pending to-decode HTLC with id {}: {}", + update_add.htlc_id, + removal_reason + ); + } + !matches + }); + if update_add_htlcs.get().is_empty() { + update_add_htlcs.remove(); } - !matches - }); - if update_add_htlcs.get().is_empty() { - update_add_htlcs.remove(); - } - }, - _ => {}, + }, + _ => {}, + } } } @@ -19611,10 +20106,11 @@ impl< // store an identifier for it here and verify that it is either (a) present in the outbound // edge or (b) removed from the outbound edge via claim. If it's in neither of these states, we // infer that it was removed from the outbound edge via fail, and fail it backwards to ensure - // that it is handled. + // that it is handled. For trampoline forwards where it is possible that we have multiple + // inbound HTLCs, each incoming HTLC's entry will store the full HTLCSource. let mut already_forwarded_htlcs: HashMap< (ChannelId, PaymentHash), - Vec<(HTLCPreviousHopData, OutboundHop)>, + Vec<(HTLCSource, OutboundHop)>, > = new_hash_map(); { // If we're tracking pending payments, ensure we haven't lost any by looking at the @@ -19652,13 +20148,17 @@ impl< .or_insert_with(Vec::new) .push(update_add_htlc); } - for (payment_hash, prev_hop, next_hop) in + for (payment_hash, source_and_hop) in funded_chan.inbound_forwarded_htlcs() { - already_forwarded_htlcs - .entry((prev_hop.channel_id, payment_hash)) - .or_insert_with(Vec::new) - .push((prev_hop, next_hop)); + for (htlc_source, next_hop) in source_and_hop { + for prev_hop in htlc_source.previous_hop_data() { + already_forwarded_htlcs + .entry((prev_hop.channel_id, payment_hash)) + .or_insert_with(Vec::new) + .push((htlc_source.clone(), next_hop)); + } + } } } } @@ -19707,17 +20207,18 @@ impl< if reconstruct_manager_from_monitors { if let Some(funded_chan) = chan.as_funded() { - for (payment_hash, prev_hop) in funded_chan.outbound_htlc_forwards() + for (payment_hash, htlc_source) in + funded_chan.outbound_htlc_forwards() { dedup_decode_update_add_htlcs( &mut decode_update_add_htlcs, - &prev_hop, + htlc_source.previous_hop_data().iter(), "HTLC already forwarded to the outbound edge", &args.logger, ); prune_forwarded_htlc( &mut already_forwarded_htlcs, - &prev_hop, + &htlc_source, &payment_hash, ); } @@ -19737,7 +20238,8 @@ impl< ); let htlc_id = SentHTLCId::from_source(&htlc_source); match htlc_source { - HTLCSource::PreviousHopData(prev_hop_data) => { + HTLCSource::PreviousHopData(_) + | HTLCSource::TrampolineForward { .. } => { reconcile_pending_htlcs_with_monitor( reconstruct_manager_from_monitors, &mut already_forwarded_htlcs, @@ -19746,29 +20248,12 @@ impl< &mut pending_intercepted_htlcs_legacy, &mut decode_update_add_htlcs, &mut decode_update_add_htlcs_legacy, - prev_hop_data, + &htlc_source, &logger, htlc.payment_hash, monitor.channel_id(), ); }, - HTLCSource::TrampolineForward { previous_hop_data, .. } => { - for prev_hop_data in previous_hop_data { - reconcile_pending_htlcs_with_monitor( - reconstruct_manager_from_monitors, - &mut already_forwarded_htlcs, - &mut forward_htlcs_legacy, - &mut pending_events_read, - &mut pending_intercepted_htlcs_legacy, - &mut decode_update_add_htlcs, - &mut decode_update_add_htlcs_legacy, - prev_hop_data, - &logger, - htlc.payment_hash, - monitor.channel_id(), - ); - } - }, HTLCSource::OutboundRoute { payment_id, session_priv, @@ -20134,27 +20619,25 @@ impl< // De-duplicate HTLCs that are present in both `failed_htlcs` and `decode_update_add_htlcs`. // Omitting this de-duplication could lead to redundant HTLC processing and/or bugs. for (src, payment_hash, _, _, _, _) in failed_htlcs.iter() { - if let HTLCSource::PreviousHopData(prev_hop_data) = src { + if let HTLCSource::PreviousHopData(_) | HTLCSource::TrampolineForward { .. } = src { dedup_decode_update_add_htlcs( &mut decode_update_add_htlcs, - prev_hop_data, + src.previous_hop_data().iter(), "HTLC was failed backwards during manager read", &args.logger, ); - prune_forwarded_htlc(&mut already_forwarded_htlcs, prev_hop_data, payment_hash); + prune_forwarded_htlc(&mut already_forwarded_htlcs, &src, payment_hash); } } // See above comment on `failed_htlcs`. - for htlcs in claimable_payments.values().map(|pmt| &pmt.htlcs) { - for htlc in htlcs.iter() { - dedup_decode_update_add_htlcs( - &mut decode_update_add_htlcs, - &htlc.mpp_part.prev_hop, - "HTLC was already decoded and marked as a claimable payment", - &args.logger, - ); - } + for claimable_htlcs in claimable_payments.values().map(|pmt| &pmt.htlcs) { + dedup_decode_update_add_htlcs( + &mut decode_update_add_htlcs, + claimable_htlcs.iter().map(|h| &h.mpp_part.prev_hop), + "HTLC was already decoded and marked as a claimable payment", + &args.logger, + ); } } @@ -20241,6 +20724,7 @@ impl< claimable_payments, pending_claiming_payments, }), + awaiting_trampoline_forwards: Mutex::new(new_hash_map()), outbound_scid_aliases: Mutex::new(outbound_scid_aliases), short_to_chan_info: FairRwLock::new(short_to_chan_info), fake_scid_rand_bytes: fake_scid_rand_bytes.unwrap(), @@ -20292,11 +20776,10 @@ impl< if let Some(forwarded_htlcs) = already_forwarded_htlcs.remove(&(*channel_id, payment_hash)) { - for (prev_hop, next_hop) in forwarded_htlcs { - let new_pending_claim = - !pending_claims_to_replay.iter().any(|(src, _, _, _, _, _, _, _)| { - matches!(src, HTLCSource::PreviousHopData(hop) if hop.htlc_id == prev_hop.htlc_id && hop.channel_id == prev_hop.channel_id) - }); + for (source, next_hop) in forwarded_htlcs { + let new_pending_claim = !pending_claims_to_replay + .iter() + .any(|(src, _, _, _, _, _, _, _)| *src == source); if new_pending_claim { let is_downstream_closed = channel_manager .per_peer_state @@ -20311,7 +20794,7 @@ impl< .contains_key(&next_hop.channel_id) }); pending_claims_to_replay.push(( - HTLCSource::PreviousHopData(prev_hop), + source, payment_preimage, next_hop.amt_msat, is_downstream_closed, @@ -20570,18 +21053,20 @@ impl< ); } for ((_, hash), htlcs) in already_forwarded_htlcs.into_iter() { - for (htlc, _) in htlcs { - let channel_id = htlc.channel_id; - let node_id = htlc.counterparty_node_id; - let source = HTLCSource::PreviousHopData(htlc); + for (source, next_hop) in htlcs { let failure_reason = LocalHTLCFailureReason::TemporaryChannelFailure; let failure_data = channel_manager.get_htlc_inbound_temp_fail_data(failure_reason); let reason = HTLCFailReason::reason(failure_reason, failure_data); - let receiver = HTLCHandlingFailureType::Forward { node_id, channel_id }; + let failure_type = source.failure_type(next_hop.node_id, next_hop.channel_id); // The event completion action is only relevant for HTLCs that originate from our node, not // forwarded HTLCs. - channel_manager - .fail_htlc_backwards_internal(&source, &hash, &reason, receiver, None); + channel_manager.fail_htlc_backwards_internal( + &source, + &hash, + &reason, + failure_type, + None, + ); } } @@ -20623,18 +21108,18 @@ impl< } fn prune_forwarded_htlc( - already_forwarded_htlcs: &mut HashMap< - (ChannelId, PaymentHash), - Vec<(HTLCPreviousHopData, OutboundHop)>, - >, - prev_hop: &HTLCPreviousHopData, payment_hash: &PaymentHash, + already_forwarded_htlcs: &mut HashMap<(ChannelId, PaymentHash), Vec<(HTLCSource, OutboundHop)>>, + htlc_source: &HTLCSource, payment_hash: &PaymentHash, ) { - if let hash_map::Entry::Occupied(mut entry) = - already_forwarded_htlcs.entry((prev_hop.channel_id, *payment_hash)) - { - entry.get_mut().retain(|(htlc, _)| prev_hop.htlc_id != htlc.htlc_id); - if entry.get().is_empty() { - entry.remove(); + for prev_hop in htlc_source.previous_hop_data() { + if let hash_map::Entry::Occupied(mut entry) = + already_forwarded_htlcs.entry((prev_hop.channel_id, *payment_hash)) + { + // TODO: check how we populate each of these sources to make sure they'll be equal. + entry.get_mut().retain(|(source, _)| source != htlc_source); + if entry.get().is_empty() { + entry.remove(); + } } } } @@ -20643,21 +21128,20 @@ fn prune_forwarded_htlc( /// cleaning up state mismatches that can occur during restart. fn reconcile_pending_htlcs_with_monitor( reconstruct_manager_from_monitors: bool, - already_forwarded_htlcs: &mut HashMap< - (ChannelId, PaymentHash), - Vec<(HTLCPreviousHopData, OutboundHop)>, - >, + already_forwarded_htlcs: &mut HashMap<(ChannelId, PaymentHash), Vec<(HTLCSource, OutboundHop)>>, forward_htlcs_legacy: &mut HashMap>, pending_events_read: &mut VecDeque<(Event, Option)>, pending_intercepted_htlcs_legacy: &mut HashMap, decode_update_add_htlcs: &mut HashMap>, decode_update_add_htlcs_legacy: &mut HashMap>, - prev_hop_data: HTLCPreviousHopData, logger: &impl Logger, payment_hash: PaymentHash, + htlc_source: &HTLCSource, logger: &impl Logger, payment_hash: PaymentHash, channel_id: ChannelId, ) { let pending_forward_matches_htlc = |info: &PendingAddHTLCInfo| { - info.prev_funding_outpoint == prev_hop_data.outpoint - && info.prev_htlc_id == prev_hop_data.htlc_id + htlc_source.previous_hop_data().iter().any(|prev_hop_data| { + info.prev_funding_outpoint == prev_hop_data.outpoint + && info.prev_htlc_id == prev_hop_data.htlc_id + }) }; // If `reconstruct_manager_from_monitors` is set, we always add all inbound committed @@ -20667,11 +21151,11 @@ fn reconcile_pending_htlcs_with_monitor( if reconstruct_manager_from_monitors { dedup_decode_update_add_htlcs( decode_update_add_htlcs, - &prev_hop_data, + htlc_source.previous_hop_data().iter(), "HTLC already forwarded to the outbound edge", &&logger, ); - prune_forwarded_htlc(already_forwarded_htlcs, &prev_hop_data, &payment_hash); + prune_forwarded_htlc(already_forwarded_htlcs, htlc_source, &payment_hash); } // The ChannelMonitor is now responsible for this HTLC's failure/success and will let us know @@ -20680,7 +21164,7 @@ fn reconcile_pending_htlcs_with_monitor( // not persisted after the monitor was when forwarding the payment. dedup_decode_update_add_htlcs( decode_update_add_htlcs_legacy, - &prev_hop_data, + htlc_source.previous_hop_data().iter(), "HTLC was forwarded to the closed channel", &&logger, ); diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index d6e0b92f1d0..30a8109fc43 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -118,6 +118,8 @@ mod reorg_tests; mod shutdown_tests; #[cfg(any(feature = "_test_utils", test))] pub mod splicing_tests; +#[cfg(test)] +mod trampoline_forward_tests; #[cfg(any(test, feature = "_externalize_tests"))] #[allow(unused_mut)] pub mod update_fee_tests; diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 6210d26893a..6ac00178536 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -2637,7 +2637,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/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index e8ff9788f3c..c7b1283aecc 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -111,6 +111,9 @@ enum RoutingInfo { next_hop_hmac: [u8; 32], shared_secret: SharedSecret, current_path_key: Option, + incoming_multipath_data: Option, + next_trampoline_amt_msat: u64, + next_trampoline_cltv: u32, }, } @@ -167,24 +170,27 @@ 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 { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { ( RoutingInfo::Trampoline { next_trampoline: next_trampoline_hop_data.next_trampoline, new_packet_bytes: new_trampoline_packet_bytes, next_hop_hmac: next_trampoline_hop_hmac, shared_secret: trampoline_shared_secret, - current_path_key: None + current_path_key: None, + incoming_multipath_data: outer_hop_data.multipath_trampoline_data, + next_trampoline_amt_msat: next_trampoline_hop_data.amt_to_forward, + next_trampoline_cltv: next_trampoline_hop_data.outgoing_cltv_value, }, - next_trampoline_hop_data.amt_to_forward, - next_trampoline_hop_data.outgoing_cltv_value, + outer_hop_data.amt_to_forward, + outer_hop_data.outgoing_cltv_value, None, None ) }, onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { - let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward( - msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features + let (next_hop_amount, next_hop_cltv) = check_blinded_forward( + outer_hop_data.multipath_trampoline_data.as_ref().map(|f| f.total_msat).unwrap_or(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(|()| { // We should be returning malformed here if `msg.blinding_point` is set, but this is // unreachable right now since we checked it in `decode_update_add_htlc_onion`. @@ -200,10 +206,13 @@ pub(super) fn create_fwd_pending_htlc_info( new_packet_bytes: new_trampoline_packet_bytes, next_hop_hmac: next_trampoline_hop_hmac, shared_secret: trampoline_shared_secret, - current_path_key: outer_hop_data.current_path_key + current_path_key: outer_hop_data.current_path_key, + incoming_multipath_data: outer_hop_data.multipath_trampoline_data, + next_trampoline_amt_msat: next_hop_amount, + next_trampoline_cltv: next_hop_cltv, }, - amt_to_forward, - outgoing_cltv_value, + outer_hop_data.amt_to_forward, + outer_hop_data.outgoing_cltv_value, next_trampoline_hop_data.intro_node_blinding_point, next_trampoline_hop_data.next_blinding_override ) @@ -233,7 +242,7 @@ pub(super) fn create_fwd_pending_htlc_info( }), } } - RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key } => { + RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key, incoming_multipath_data: multipath_trampoline_data, next_trampoline_amt_msat: next_hop_amount, next_trampoline_cltv: next_hop_cltv} => { let next_trampoline_packet_pubkey = match next_packet_pubkey_opt { Some(Ok(pubkey)) => pubkey, _ => return Err(InboundHTLCErr { @@ -260,7 +269,10 @@ pub(super) fn create_fwd_pending_htlc_info( failure: intro_node_blinding_point .map(|_| BlindedFailure::FromIntroductionNode) .unwrap_or(BlindedFailure::FromBlindedNode), - }) + }), + incoming_multipath_data: multipath_trampoline_data, + next_trampoline_amt_msat: next_hop_amount, + next_trampoline_cltv_expiry: next_hop_cltv, } } }; @@ -683,33 +695,24 @@ pub(super) fn decode_incoming_update_add_htlc_onion { + onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { next_trampoline, .. }, ref outer_hop_data, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { next_packet_pubkey: next_trampoline_packet_pubkey, outgoing_connector: HopConnector::Trampoline(next_trampoline), - outgoing_amt_msat: amt_to_forward, - outgoing_cltv_value, + outgoing_amt_msat: outer_hop_data.amt_to_forward, + outgoing_cltv_value: outer_hop_data.outgoing_cltv_value, }) } - onion_utils::Hop::TrampolineBlindedForward { 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 trampoline forward", - LocalHTLCFailureReason::InvalidOnionBlinding, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &[0; 32]); - } - }; + onion_utils::Hop::TrampolineBlindedForward { next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload { next_trampoline, .. }, ref outer_hop_data, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { next_packet_pubkey: next_trampoline_packet_pubkey, outgoing_connector: HopConnector::Trampoline(next_trampoline), - outgoing_amt_msat: amt_to_forward, - outgoing_cltv_value, + outgoing_amt_msat: outer_hop_data.amt_to_forward, + outgoing_cltv_value: outer_hop_data.outgoing_cltv_value, }) } _ => None diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index 019d8faf98c..610042a2add 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -2043,7 +2043,7 @@ fn test_trampoline_onion_payload_assembly_values() { cur_height, &None, None, - Some(trampoline_packet), + Some((trampoline_packet, None)), ) .unwrap(); assert_eq!(outer_payloads.len(), 2); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 602d731bac6..0153913e05b 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -18,7 +18,7 @@ use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::channelmanager::HTLCSource; use crate::ln::msgs::{self, DecodeError, InboundOnionDummyPayload, OnionPacket, UpdateAddHTLC}; use crate::ln::onion_payment::{HopConnector, NextPacketDetails}; -use crate::ln::outbound_payment::RecipientOnionFields; +use crate::ln::outbound_payment::{NextTrampolineHopInfo, RecipientOnionFields}; use crate::offers::invoice_request::InvoiceRequest; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters, TrampolineHop}; @@ -206,7 +206,7 @@ trait OnionPayload<'a, 'b> { ) -> Self; fn new_trampoline_entry( amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, - packet: msgs::TrampolineOnionPacket, + packet: msgs::TrampolineOnionPacket, blinding_point: Option, ) -> Result; } impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { @@ -258,19 +258,29 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { fn new_trampoline_entry( amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, - packet: msgs::TrampolineOnionPacket, + 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: recipient_onion.total_mpp_amount_msat, - } - }), - trampoline_packet: packet, - }) + let total_msat = recipient_onion.total_mpp_amount_msat; + let multipath_trampoline_data = recipient_onion + .payment_secret + .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }); + + if let Some(blinding_point) = blinding_point { + Ok(Self::BlindedTrampolineEntrypoint { + amt_to_forward, + outgoing_cltv_value, + multipath_trampoline_data, + trampoline_packet: packet, + current_path_key: blinding_point, + }) + } else { + Ok(Self::TrampolineEntrypoint { + amt_to_forward, + outgoing_cltv_value, + multipath_trampoline_data, + trampoline_packet: packet, + }) + } } } impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { @@ -314,6 +324,7 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { fn new_trampoline_entry( _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(), @@ -446,7 +457,7 @@ pub(super) fn build_trampoline_onion_payloads<'a>( pub(crate) fn test_build_onion_payloads<'a>( path: &'a Path, recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, - trampoline_packet: Option, + trampoline_packet: Option<(msgs::TrampolineOnionPacket, Option)>, ) -> Result<(Vec>, u64, u32), APIError> { build_onion_payloads( path, @@ -462,7 +473,7 @@ pub(crate) fn test_build_onion_payloads<'a>( fn build_onion_payloads<'a>( path: &'a Path, recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, - trampoline_packet: Option, + trampoline_packet: Option<(msgs::TrampolineOnionPacket, Option)>, ) -> Result<(Vec>, u64, u32), APIError> { let mut res: Vec = Vec::with_capacity( path.hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), @@ -472,10 +483,11 @@ fn build_onion_payloads<'a>( // means that the blinded path needs not be appended to the regular hops, and is only included // among the Trampoline onion payloads. let blinded_tail_with_hop_iter = path.blinded_tail.as_ref().map(|bt| { - if let Some(trampoline_packet) = trampoline_packet { + if let Some((trampoline_packet, blinding_point)) = trampoline_packet { return BlindedTailDetails::TrampolineEntry { trampoline_packet, final_value_msat: bt.final_value_msat, + blinding_point, }; } BlindedTailDetails::DirectEntry { @@ -511,6 +523,9 @@ enum BlindedTailDetails<'a, I: Iterator> { TrampolineEntry { trampoline_packet: msgs::TrampolineOnionPacket, final_value_msat: u64, + // If forwarding a trampoline payment inside of a blinded path, this blinding_point will + // be set for the trampoline to decrypt its inner onion. + blinding_point: Option, }, } @@ -581,6 +596,7 @@ where Some(BlindedTailDetails::TrampolineEntry { trampoline_packet, final_value_msat, + blinding_point, }) => { cur_value_msat += final_value_msat; callback( @@ -590,6 +606,7 @@ where declared_incoming_cltv, &recipient_onion, trampoline_packet, + blinding_point, )?, ); }, @@ -996,10 +1013,13 @@ mod fuzzy_onion_utils { pub(crate) failed_within_blinded_path: bool, #[allow(dead_code)] pub(crate) hold_times: Vec, - #[cfg(any(test, feature = "_test_utils"))] pub(crate) onion_error_code: Option, #[cfg(any(test, feature = "_test_utils"))] pub(crate) onion_error_data: Option>, + /// When processing a trampoline forward error that couldn't be decoded at the + /// outer onion level, this contains the error packet with outer layers peeled, + /// ready to be passed through with additional trampoline wrapping. + pub(crate) trampoline_peeled_packet: Option, #[cfg(test)] pub(crate) attribution_failed_channel: Option, } @@ -1008,12 +1028,42 @@ mod fuzzy_onion_utils { secp_ctx: &Secp256k1, logger: &L, htlc_source: &HTLCSource, encrypted_packet: OnionErrorPacket, ) -> DecodedOnionFailure { - let (path, session_priv) = match htlc_source { - HTLCSource::OutboundRoute { ref path, ref session_priv, .. } => (path, session_priv), - _ => unreachable!(), - }; + match htlc_source { + HTLCSource::OutboundRoute { ref path, ref session_priv, .. } => { + process_onion_failure_inner( + secp_ctx, + logger, + &path, + &session_priv, + None, + encrypted_packet, + ) + .0 + }, + HTLCSource::TrampolineForward { outbound_payment, .. } => { + let dispatch = outbound_payment.as_ref() + .expect("processing trampoline onion failure for forward with no outbound payment details"); + + let (mut decoded, peeled_packet) = process_onion_failure_inner( + secp_ctx, + logger, + &dispatch.path, + &dispatch.session_priv, + None, + encrypted_packet, + ); - process_onion_failure_inner(secp_ctx, logger, path, &session_priv, None, encrypted_packet) + // If we couldn't decode the error at the outer onion level, it's a + // trampoline-encrypted error from downstream. Store the peeled packet + // so the caller can pass it through with additional trampoline wrapping. + if decoded.onion_error_code.is_none() { + decoded.trampoline_peeled_packet = Some(peeled_packet); + } + + decoded + }, + _ => unreachable!(), + } } /// Decodes the attribution data that we got back from upstream on a payment we sent. @@ -1075,7 +1125,7 @@ pub(crate) use self::fuzzy_onion_utils::*; fn process_onion_failure_inner( secp_ctx: &Secp256k1, logger: &L, path: &Path, session_priv: &SecretKey, trampoline_session_priv_override: Option, mut encrypted_packet: OnionErrorPacket, -) -> DecodedOnionFailure { +) -> (DecodedOnionFailure, OnionErrorPacket) { // Check that there is at least enough data for an hmac, otherwise none of the checking that we may do makes sense. // Also prevent slice out of bounds further down. if encrypted_packet.data.len() < 32 { @@ -1087,19 +1137,22 @@ fn process_onion_failure_inner( // Signal that we failed permanently. Without a valid hmac, we can't identify the failing node and we can't // apply a penalty. Therefore there is nothing more we can do other than failing the payment. - return DecodedOnionFailure { - network_update: None, - short_channel_id: None, - payment_failed_permanently: true, - failed_within_blinded_path: false, - hold_times: Vec::new(), - #[cfg(any(test, feature = "_test_utils"))] - onion_error_code: None, - #[cfg(any(test, feature = "_test_utils"))] - onion_error_data: None, - #[cfg(test)] - attribution_failed_channel: None, - }; + return ( + DecodedOnionFailure { + network_update: None, + short_channel_id: None, + payment_failed_permanently: true, + failed_within_blinded_path: false, + hold_times: Vec::new(), + onion_error_code: None, + #[cfg(any(test, feature = "_test_utils"))] + onion_error_data: None, + trampoline_peeled_packet: None, + #[cfg(test)] + attribution_failed_channel: None, + }, + encrypted_packet, + ); } // Learnings from the HTLC failure to inform future payment retries and scoring. @@ -1180,16 +1233,41 @@ fn process_onion_failure_inner( let route_hop = match route_hop_option.as_ref() { Some(hop) => hop, None => { - // Got an error from within a blinded route. - _error_code_ret = Some(LocalHTLCFailureReason::InvalidOnionBlinding); - _error_packet_ret = Some(vec![0; 32]); - res = Some(FailureLearnings { - network_update: None, - short_channel_id: None, - payment_failed_permanently: false, - failed_within_blinded_path: true, - }); - break; + // This is a blinded hop. We still need to peel this layer so that + // trampoline pass-through errors can be decoded at a later hop. + crypt_failure_packet(shared_secret.as_ref(), &mut encrypted_packet); + + let um = gen_um_from_shared_secret(shared_secret.as_ref()); + let mut hmac = HmacEngine::::new(&um); + hmac.input(&encrypted_packet.data[32..]); + if &Hmac::from_engine(hmac).to_byte_array() == &encrypted_packet.data[..32] { + // The error was addressed to this blinded hop (trampoline recipient). + let err_packet = msgs::DecodedOnionErrorPacket::read(&mut Cursor::new( + &encrypted_packet.data, + )); + if let Ok(err_packet) = err_packet { + if let Some(error_code_slice) = err_packet.failuremsg.get(0..2) { + _error_code_ret = Some( + u16::from_be_bytes(error_code_slice.try_into().expect("len is 2")) + .into(), + ); + _error_packet_ret = Some(err_packet.failuremsg[2..].to_vec()); + } + } + res = Some(FailureLearnings { + network_update: None, + short_channel_id: None, + payment_failed_permanently: false, + failed_within_blinded_path: true, + }); + break; + } + + // HMAC didn't match — continue peeling to find the right hop. + // If we exhaust all hops without a match, the fallthrough will + // handle it (returning onion_error_code: None for trampoline + // pass-through). + continue; }, }; @@ -1206,37 +1284,60 @@ fn process_onion_failure_inner( match next_hop { Some((_, (Some(hop), _))) => hop, _ => { - // The failing hop is within a multi-hop blinded path. - #[cfg(not(test))] - { - _error_code_ret = Some(LocalHTLCFailureReason::InvalidOnionBlinding); - _error_packet_ret = Some(vec![0; 32]); - } - #[cfg(test)] - { - // Actually parse the onion error data in tests so we can check that blinded hops fail - // back correctly. - crypt_failure_packet(shared_secret.as_ref(), &mut encrypted_packet); - let err_packet = msgs::DecodedOnionErrorPacket::read(&mut Cursor::new( - &encrypted_packet.data, - )) - .unwrap(); - _error_code_ret = Some( - u16::from_be_bytes( - err_packet.failuremsg.get(0..2).unwrap().try_into().unwrap(), - ) - .into(), - ); - _error_packet_ret = Some(err_packet.failuremsg[2..].to_vec()); + // The next hop is within a multi-hop blinded path. + // Crypt and check HMAC to see if the error was addressed + // to this hop. If not, continue peeling so trampoline + // pass-through errors can be decoded at a later hop. + crypt_failure_packet(shared_secret.as_ref(), &mut encrypted_packet); + + let um = gen_um_from_shared_secret(shared_secret.as_ref()); + let mut hmac = HmacEngine::::new(&um); + hmac.input(&encrypted_packet.data[32..]); + + if &Hmac::from_engine(hmac).to_byte_array() == &encrypted_packet.data[..32] { + #[cfg(not(test))] + { + _error_code_ret = Some(LocalHTLCFailureReason::InvalidOnionBlinding); + _error_packet_ret = Some(vec![0; 32]); + } + #[cfg(test)] + { + // Actually parse the onion error data in tests so we + // can check that blinded hops fail back correctly. + if let Ok(err_packet) = msgs::DecodedOnionErrorPacket::read( + &mut Cursor::new(&encrypted_packet.data), + ) { + _error_code_ret = Some( + u16::from_be_bytes( + err_packet + .failuremsg + .get(0..2) + .unwrap() + .try_into() + .unwrap(), + ) + .into(), + ); + _error_packet_ret = Some(err_packet.failuremsg[2..].to_vec()); + } else { + _error_code_ret = + Some(LocalHTLCFailureReason::InvalidOnionBlinding); + _error_packet_ret = Some(vec![0; 32]); + } + } + + res = Some(FailureLearnings { + network_update: None, + short_channel_id: None, + payment_failed_permanently: false, + failed_within_blinded_path: true, + }); + break; } - res = Some(FailureLearnings { - network_update: None, - short_channel_id: None, - payment_failed_permanently: false, - failed_within_blinded_path: true, - }); - break; + // HMAC didn't match — continue peeling to find the right + // hop (trampoline pass-through). + continue; }, } }; @@ -1470,7 +1571,7 @@ fn process_onion_failure_inner( break; } - if let Some(FailureLearnings { + let decoded = if let Some(FailureLearnings { network_update, short_channel_id, payment_failed_permanently, @@ -1483,10 +1584,10 @@ fn process_onion_failure_inner( payment_failed_permanently, failed_within_blinded_path, hold_times: hop_hold_times, - #[cfg(any(test, feature = "_test_utils"))] onion_error_code: _error_code_ret, #[cfg(any(test, feature = "_test_utils"))] onion_error_data: _error_packet_ret, + trampoline_peeled_packet: None, #[cfg(test)] attribution_failed_channel, } @@ -1506,14 +1607,16 @@ fn process_onion_failure_inner( payment_failed_permanently: is_from_final_non_blinded_node, failed_within_blinded_path: false, hold_times: hop_hold_times, - #[cfg(any(test, feature = "_test_utils"))] onion_error_code: None, #[cfg(any(test, feature = "_test_utils"))] onion_error_data: None, + trampoline_peeled_packet: None, #[cfg(test)] attribution_failed_channel, } - } + }; + + (decoded, encrypted_packet) } const BADONION: u16 = 0x8000; @@ -1769,7 +1872,7 @@ impl LocalHTLCFailureReason { /// Returns true if the failure is only sent by the final recipient. Note that this function /// only checks [`LocalHTLCFailureReason`] variants that represent bolt 04 errors directly, /// as it's intended to analyze errors we've received as a sender. - fn is_recipient_failure(&self) -> bool { + pub(super) fn is_recipient_failure(&self) -> bool { self.failure_code() == LocalHTLCFailureReason::IncorrectPaymentDetails.failure_code() || *self == LocalHTLCFailureReason::FinalIncorrectCLTVExpiry || *self == LocalHTLCFailureReason::FinalIncorrectHTLCAmount @@ -2089,6 +2192,10 @@ impl HTLCFailReason { }) } + pub(super) fn from_onion_error_packet(packet: OnionErrorPacket) -> Self { + Self(HTLCFailReasonRepr::LightningError { err: packet, hold_time: None }) + } + /// Encrypted a failure packet using a shared secret. /// /// For phantom nodes or inner Trampoline onions, a secondary_shared_secret can be passed, which @@ -2128,6 +2235,10 @@ impl HTLCFailReason { let mut err = err.clone(); let hold_time = hold_time.unwrap_or(0); + if let Some(secondary_shared_secret) = secondary_shared_secret { + process_failure_packet(&mut err, secondary_shared_secret, hold_time); + crypt_failure_packet(secondary_shared_secret, &mut err); + } process_failure_packet(&mut err, incoming_packet_shared_secret, hold_time); crypt_failure_packet(incoming_packet_shared_secret, &mut err); @@ -2139,6 +2250,23 @@ impl HTLCFailReason { pub(super) fn decode_onion_failure( &self, secp_ctx: &Secp256k1, logger: &L, htlc_source: &HTLCSource, ) -> DecodedOnionFailure { + macro_rules! decoded_onion_failure { + ($short_channel_id:expr, $failure_reason:expr, $data:expr) => { + DecodedOnionFailure { + network_update: None, + payment_failed_permanently: false, + short_channel_id: $short_channel_id, + failed_within_blinded_path: false, + hold_times: Vec::new(), + onion_error_code: Some($failure_reason), + #[cfg(any(test, feature = "_test_utils"))] + onion_error_data: Some($data.clone()), + trampoline_peeled_packet: None, + #[cfg(test)] + attribution_failed_channel: None, + } + }; + } match self.0 { HTLCFailReasonRepr::LightningError { ref err, .. } => { process_onion_failure(secp_ctx, logger, &htlc_source, err.clone()) @@ -2150,22 +2278,19 @@ 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 { - network_update: None, - payment_failed_permanently: false, - short_channel_id: Some(path.hops[0].short_channel_id), - failed_within_blinded_path: false, - hold_times: Vec::new(), - #[cfg(any(test, feature = "_test_utils"))] - onion_error_code: Some(*failure_reason), - #[cfg(any(test, feature = "_test_utils"))] - onion_error_data: Some(data.clone()), - #[cfg(test)] - attribution_failed_channel: None, - } - } else { - unreachable!(); + match htlc_source { + &HTLCSource::OutboundRoute { ref path, .. } => { + decoded_onion_failure!( + (Some(path.hops[0].short_channel_id)), + *failure_reason, + data + ) + }, + &HTLCSource::TrampolineForward { ref outbound_payment, .. } => { + debug_assert!(outbound_payment.is_none()); + decoded_onion_failure!(None, *failure_reason, data) + }, + _ => unreachable!(), } }, } @@ -2423,7 +2548,10 @@ pub(crate) fn decode_next_payment_hop( &hop_data.trampoline_packet.hop_data, hop_data.trampoline_packet.hmac, Some(payment_hash), - (blinding_point, &node_signer), + // When we have a trampoline packet, the current_path_key in our outer onion + // payload plays the role of the update_add_htlc blinding_point for the inner + // onion. + (hop_data.current_path_key, node_signer), ); match decoded_trampoline_hop { Ok(( @@ -2634,6 +2762,49 @@ pub(super) fn compute_trampoline_session_priv(outer_onion_session_priv: &SecretK SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!") } +/// Builds a payment onion for an inter-trampoline forward. +pub(crate) fn create_trampoline_forward_onion( + secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, payment_hash: &PaymentHash, + recipient_onion: &RecipientOnionFields, keysend_preimage: &Option, + trampoline_forward_info: &NextTrampolineHopInfo, prng_seed: [u8; 32], +) -> Result<(msgs::OnionPacket, u64, u32), APIError> { + // Inter-trampoline payments should always be cleartext because we need to know the node id + // that we need to route to. LDK does not currently support the legacy "trampoline to blinded + // path" approach, where we get a blinded path to pay inside of our trampoline onion. + debug_assert!(path.blinded_tail.is_none(), "trampoline should not be blinded"); + + let mut res: Vec = Vec::with_capacity(path.hops.len()); + + let blinded_tail_with_hop_iter: BlindedTailDetails<'_, core::iter::Empty<&BlindedHop>> = + BlindedTailDetails::TrampolineEntry { + trampoline_packet: trampoline_forward_info.onion_packet.clone(), + final_value_msat: 0, + blinding_point: trampoline_forward_info.blinding_point, + }; + let (value_msat, cltv) = build_onion_payloads_callback( + path.hops.iter(), + Some(blinded_tail_with_hop_iter), + recipient_onion, + // Note that we use the cltv expiry height that the next trampoline is expecting instead + // of the current block height. This is because we need to create an onion that terminates + // at the next trampoline with the cltv we've been told to give them. + trampoline_forward_info.cltv_expiry_height, + keysend_preimage, + None, + |action, payload| match action { + PayloadCallbackAction::PushBack => res.push(payload), + PayloadCallbackAction::PushFront => res.insert(0, payload), + }, + )?; + + let onion_keys = construct_onion_keys(&secp_ctx, &path, session_priv); + let onion_packet = + construct_onion_packet(res, onion_keys, prng_seed, payment_hash).map_err(|_| { + APIError::InvalidRoute { err: "Route size too large considering onion data".to_owned() } + })?; + Ok((onion_packet, value_msat, 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( @@ -2689,7 +2860,7 @@ pub(crate) fn create_payment_onion_internal( err: "Route size too large (or empty) considering onion data".to_owned(), })?; - (&trampoline_outer_onion, Some(trampoline_packet)) + (&trampoline_outer_onion, Some((trampoline_packet, None))) } else { (recipient_onion, None) } @@ -3704,7 +3875,7 @@ mod tests { data: >::from_hex(error_packet_hex).unwrap(), attribution_data: None, }; - let decrypted_failure = process_onion_failure_inner( + let (decrypted_failure, _) = process_onion_failure_inner( &secp_ctx, &logger, &build_trampoline_test_path(), diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 7259f60796f..285f2d8a83f 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -11,17 +11,17 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; -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, OptionalBolt11PaymentParams, PaymentCompleteUpdate, - PaymentId, + EventCompletionAction, HTLCPreviousHopData, HTLCSource, OptionalBolt11PaymentParams, + PaymentCompleteUpdate, PaymentId, }; -use crate::ln::msgs::DecodeError; +use crate::ln::msgs::{DecodeError, TrampolineOnionPacket}; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder}; @@ -127,6 +127,9 @@ pub(crate) enum PendingOutboundPayment { // Storing the BOLT 12 invoice here to allow Proof of Payment after // the payment is made. bolt12_invoice: Option, + // Storing forward information for trampoline payments in order to build next hop info + // or build error or claims to the origin. + trampoline_forward_info: Option, custom_tlvs: Vec<(u64, Vec)>, pending_amt_msat: u64, /// Used to track the fee paid. Present iff the payment was serialized on 0.0.103+. @@ -167,6 +170,43 @@ pub(crate) enum PendingOutboundPayment { }, } +#[derive(Clone, Eq, PartialEq)] +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, + /// The amount that the next trampoline is expecting to receive. + pub(crate) amount_msat: u64, + /// The cltv expiry height that the next trampoline is expecting. + pub(crate) cltv_expiry_height: u32, +} + +impl_writeable_tlv_based!(NextTrampolineHopInfo, { + (1, onion_packet, required), + (3, blinding_point, option), + (5, amount_msat, required), + (7, cltv_expiry_height, required), +}); + +#[derive(Clone)] +pub(crate) struct TrampolineForwardInfo { + /// Information necessary to construct the onion packet for the next Trampoline hop. + pub(crate) next_hop_info: NextTrampolineHopInfo, + /// The incoming HTLCs that were forwarded to us, which need to be settled or failed once + /// our outbound payment has been completed. + pub(crate) previous_hop_data: Vec, + /// The forwarding fee charged for this trampoline payment, persisted here so that we don't + /// need to look up the value of all our incoming/outgoing payments to calculate fee. + pub(crate) forwading_fee_msat: u64, +} + +impl_writeable_tlv_based!(TrampolineForwardInfo, { + (1, next_hop_info, required), + (3, previous_hop_data, required_vec), + (5, forwading_fee_msat, required), +}); + #[derive(Clone)] pub(crate) struct RetryableInvoiceRequest { pub(crate) invoice_request: InvoiceRequest, @@ -935,6 +975,7 @@ pub(super) struct SendAlongPathArgs<'a> { pub keysend_preimage: &'a Option, pub invoice_request: Option<&'a InvoiceRequest>, pub bolt12_invoice: Option<&'a PaidBolt12Invoice>, + pub trampoline_forward_info: Option<&'a TrampolineForwardInfo>, pub session_priv_bytes: [u8; 32], pub hold_htlc_at_next_hop: bool, } @@ -1195,7 +1236,7 @@ impl OutboundPayments { hash_map::Entry::Occupied(entry) => match entry.get() { PendingOutboundPayment::InvoiceReceived { .. } => { let (retryable_payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion.clone(), keysend_preimage, None, Some(bolt12_invoice.clone()), &route, + payment_hash, recipient_onion.clone(), keysend_preimage, None, Some(bolt12_invoice.clone()), None, &route, Some(retry_strategy), payment_params, entropy_source, best_block_height, ); *entry.into_mut() = retryable_payment; @@ -1206,8 +1247,8 @@ impl OutboundPayments { invoice_request } else { unreachable!() }; let (retryable_payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), Some(bolt12_invoice.clone()), &route, - Some(retry_strategy), payment_params, entropy_source, best_block_height + payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), Some(bolt12_invoice.clone()), + None, &route, Some(retry_strategy), payment_params, entropy_source, best_block_height ); outbounds.insert(payment_id, retryable_payment); onion_session_privs @@ -1219,8 +1260,8 @@ impl OutboundPayments { core::mem::drop(outbounds); let result = self.pay_route_internal( - &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, Some(&bolt12_invoice), payment_id, - &onion_session_privs, hold_htlcs_at_next_hop, node_signer, + &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, Some(&bolt12_invoice), + None, payment_id, &onion_session_privs, hold_htlcs_at_next_hop, node_signer, best_block_height, &send_payment_along_path ); log_info!( @@ -1603,7 +1644,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); @@ -1611,7 +1652,7 @@ impl OutboundPayments { })?; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, &onion_session_privs, false, node_signer, + keysend_preimage, None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Sending payment with id {} and hash {} returned {:?}", payment_id, payment_hash, res); @@ -1625,6 +1666,120 @@ impl OutboundPayments { 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< + R: Router, + NS: NodeSigner, + ES: EntropySource, + IH, + SP, + L: Logger, + >( + &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, + pending_events: &Mutex)>>, + send_payment_along_path: SP, logger: &WithContext, + ) -> Result<(), RetryableSendFailure> + where + 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, + trampoline_forward_info.next_hop_info.amount_msat, + ); + + 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, + None, + Some(&trampoline_forward_info), + payment_id, + &onion_session_privs, + false, + 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, + pending_events, + &send_payment_along_path, + logger, + ); + } + Ok(()) + } + #[rustfmt::skip] fn find_route_and_send_payment( &self, payment_hash: PaymentHash, payment_id: PaymentId, route_params: RouteParameters, @@ -1678,14 +1833,14 @@ impl OutboundPayments { } } } - let (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice) = { + let (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice, trampoline_forward_info) = { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); match outbounds.entry(payment_id) { hash_map::Entry::Occupied(mut payment) => { match payment.get() { PendingOutboundPayment::Retryable { total_msat, keysend_preimage, payment_secret, payment_metadata, - custom_tlvs, pending_amt_msat, invoice_request, onion_total_msat, .. + custom_tlvs, pending_amt_msat, invoice_request, trampoline_forward_info, onion_total_msat, .. } => { const RETRY_OVERFLOW_PERCENTAGE: u64 = 10; let retry_amt_msat = route.get_total_amount(); @@ -1701,6 +1856,7 @@ impl OutboundPayments { return } + let trampoline_forward_info = trampoline_forward_info.clone(); let recipient_onion = RecipientOnionFields { payment_secret: *payment_secret, payment_metadata: payment_metadata.clone(), @@ -1722,7 +1878,7 @@ impl OutboundPayments { payment.get_mut().increment_attempts(); let bolt12_invoice = payment.get().bolt12_invoice(); - (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned()) + (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned(), trampoline_forward_info) }, PendingOutboundPayment::Legacy { .. } => { log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102"); @@ -1762,8 +1918,9 @@ impl OutboundPayments { } }; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, keysend_preimage, - invoice_request.as_ref(), bolt12_invoice.as_ref(), payment_id, - &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); + invoice_request.as_ref(), bolt12_invoice.as_ref(), trampoline_forward_info.as_ref(), + payment_id, &onion_session_privs, false, node_signer, best_block_height, + &send_payment_along_path); log_info!(logger, "Result retrying payment id {}: {:?}", &payment_id, res); if let Err(e) = res { self.handle_pay_route_err( @@ -1914,14 +2071,14 @@ impl OutboundPayments { RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion_fields.clone(), 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 })?; match self.pay_route_internal(&route, payment_hash, &recipient_onion_fields, - None, None, None, payment_id, &onion_session_privs, false, node_signer, + None, None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path ) { Ok(()) => Ok((payment_hash, payment_id)), @@ -1969,7 +2126,7 @@ 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> { - 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) } #[rustfmt::skip] @@ -1977,15 +2134,15 @@ impl OutboundPayments { &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> { let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); match pending_outbounds.entry(payment_id) { hash_map::Entry::Occupied(_) => Err(PaymentSendFailure::DuplicatePayment), 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_hash, recipient_onion, keysend_preimage, None, bolt12_invoice, trampoline_forward_info, + route, retry_strategy, payment_params, entropy_source, best_block_height ); entry.insert(payment); Ok(onion_session_privs) @@ -1997,7 +2154,8 @@ impl OutboundPayments { fn create_pending_payment( payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option, invoice_request: Option, - bolt12_invoice: Option, route: &Route, retry_strategy: Option, + bolt12_invoice: Option, trampoline_forward_info: Option, + route: &Route, retry_strategy: Option, payment_params: Option, entropy_source: &ES, best_block_height: u32 ) -> (PendingOutboundPayment, Vec<[u8; 32]>) { let mut onion_session_privs = Vec::with_capacity(route.paths.len()); @@ -2018,6 +2176,7 @@ impl OutboundPayments { keysend_preimage, invoice_request, bolt12_invoice, + trampoline_forward_info, custom_tlvs: recipient_onion.custom_tlvs, starting_block_height: best_block_height, total_msat: route.get_total_amount(), @@ -2108,7 +2267,7 @@ impl OutboundPayments { fn pay_route_internal( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields, keysend_preimage: Option, invoice_request: Option<&InvoiceRequest>, bolt12_invoice: Option<&PaidBolt12Invoice>, - payment_id: PaymentId, onion_session_privs: &Vec<[u8; 32]>, + trampoline_forward_info: Option<&TrampolineForwardInfo>, payment_id: PaymentId, onion_session_privs: &Vec<[u8; 32]>, hold_htlcs_at_next_hop: bool, node_signer: &NS, best_block_height: u32, send_payment_along_path: &F ) -> Result<(), PaymentSendFailure> where @@ -2122,6 +2281,9 @@ impl OutboundPayments { { return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError{err: "Payment secret is required for multi-path payments".to_owned()})); } + if trampoline_forward_info.is_some() && keysend_preimage.is_some() { + return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError{err: "Trampoline forwards cannot include keysend preimage".to_owned()})); + } let our_node_id = node_signer.get_node_id(Recipient::Node).unwrap(); // TODO no unwrap let mut path_errs = Vec::with_capacity(route.paths.len()); 'path_check: for path in route.paths.iter() { @@ -2157,7 +2319,7 @@ impl OutboundPayments { let path_res = send_payment_along_path(SendAlongPathArgs { path: &path, payment_hash: &payment_hash, recipient_onion, cur_height, payment_id, keysend_preimage: &keysend_preimage, invoice_request, - bolt12_invoice, hold_htlc_at_next_hop: hold_htlcs_at_next_hop, + bolt12_invoice, trampoline_forward_info, hold_htlc_at_next_hop: hold_htlcs_at_next_hop, session_priv_bytes: *session_priv_bytes }); results.push(path_res); @@ -2224,7 +2386,7 @@ impl OutboundPayments { F: Fn(SendAlongPathArgs) -> Result<(), APIError>, { self.pay_route_internal(route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, &onion_session_privs, + keysend_preimage, None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path) .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e }) } @@ -2318,6 +2480,14 @@ impl OutboundPayments { }, None)); } } + } else if let HTLCSource::TrampolineForward { + outbound_payment: Some(trampoline_dispatch), .. + } = source { + let session_priv_bytes = trampoline_dispatch.session_priv.secret_bytes(); + if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(trampoline_dispatch.payment_id) { + assert!(payment.get().is_fulfilled()); + payment.get_mut().remove(&session_priv_bytes, None); + } } } } @@ -2411,6 +2581,103 @@ impl OutboundPayments { }); } + // Reports a failed HTLC that is part of an outgoing trampoline forward. Returns Some() if + // the incoming HTLC(s) associated with the trampoline should be failed back. + pub(super) fn trampoline_htlc_failed( + &self, source: &HTLCSource, payment_hash: &PaymentHash, onion_error: &HTLCFailReason, + secp_ctx: &Secp256k1, logger: &WithContext, + ) -> Option { + #[cfg(any(test, feature = "_test_utils"))] + let decoded_onion = onion_error.decode_onion_failure(secp_ctx, &logger, &source); + + #[cfg(not(any(test, feature = "_test_utils")))] + let decoded_onion = onion_error.decode_onion_failure(secp_ctx, &logger, &source); + + let (payment_id, path, session_priv) = match source { + HTLCSource::TrampolineForward { outbound_payment, .. } => { + let outbound_payment = outbound_payment.clone().unwrap(); + (outbound_payment.payment_id, outbound_payment.path, outbound_payment.session_priv) + }, + _ => { + debug_assert!(false, "trampoline payment failed with no dispatch information"); + return None; + }, + }; + + let mut session_priv_bytes = [0; 32]; + session_priv_bytes.copy_from_slice(&session_priv[..]); + let mut outbounds = self.pending_outbound_payments.lock().unwrap(); + + let attempts_remaining = + if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(payment_id) { + if !payment.get_mut().remove(&session_priv_bytes, Some(&path)) { + log_trace!( + logger, + "Received duplicative fail for HTLC with payment_hash {}", + &payment_hash + ); + return None; + } + if payment.get().is_fulfilled() { + log_trace!( + logger, + "Received failure of HTLC with payment_hash {} after payment completion", + &payment_hash + ); + return None; + } + let mut is_retryable_now = payment.get().is_auto_retryable_now(); + if let Some(scid) = decoded_onion.short_channel_id { + // TODO: If we decided to blame ourselves (or one of our channels) in + // process_onion_failure we should close that channel as it implies our + // next-hop is needlessly blaming us! + payment.get_mut().insert_previously_failed_scid(scid); + } + if decoded_onion.failed_within_blinded_path { + debug_assert!(decoded_onion.short_channel_id.is_none()); + if let Some(bt) = &path.blinded_tail { + payment.get_mut().insert_previously_failed_blinded_path(&bt); + } else { + debug_assert!(false); + } + } + + if !is_retryable_now || decoded_onion.payment_failed_permanently { + let reason = if decoded_onion.payment_failed_permanently { + PaymentFailureReason::RecipientRejected + } else { + PaymentFailureReason::RetriesExhausted + }; + payment.get_mut().mark_abandoned(reason); + is_retryable_now = false; + } + if payment.get().remaining_parts() == 0 { + if let PendingOutboundPayment::Abandoned { .. } = payment.get() { + payment.remove(); + return Some(decoded_onion); + } + } + is_retryable_now + } else { + log_trace!( + logger, + "Received fail for HTLC with payment_hash {} not found.", + &payment_hash + ); + return Some(decoded_onion); + }; + core::mem::drop(outbounds); + log_trace!(logger, "Failing Trampoline forward HTLC with payment_hash {}", &payment_hash); + + // If we miss abandoning the payment above, we *must* generate an event here or else the + // payment will sit in our outbounds forever. + if attempts_remaining { + return None; + }; + + return Some(decoded_onion); + } + pub(super) fn fail_htlc( &self, source: &HTLCSource, payment_hash: &PaymentHash, onion_error: &HTLCFailReason, path: &Path, session_priv: &SecretKey, payment_id: &PaymentId, @@ -2666,6 +2933,7 @@ impl OutboundPayments { 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! + trampoline_forward_info: None, // only used for retries, and we'll never retry on startup custom_tlvs: Vec::new(), // only used for retries, and we'll never retry on startup pending_amt_msat: path_amt, pending_fee_msat: Some(path_fee), @@ -2709,6 +2977,33 @@ impl OutboundPayments { }, } } + + /// Looks up a trampoline forward by its payment id, marks it as fulfilled, and returns the + /// forwarding fee our node earned. Returns None if the payment is not found or it does not + /// have trampoline forwarding information. + pub(crate) fn claim_trampoline_forward( + &self, payment_id: &PaymentId, session_priv: &SecretKey, from_onchain: bool, + ) -> Option { + let mut outbounds = self.pending_outbound_payments.lock().unwrap(); + if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(*payment_id) { + let fee = match payment.get() { + PendingOutboundPayment::Retryable { trampoline_forward_info, .. } => { + trampoline_forward_info.as_ref().map(|info| info.forwading_fee_msat) + }, + _ => None, + }; + if !payment.get().is_fulfilled() { + payment.get_mut().mark_fulfilled(); + } + if from_onchain { + let session_priv_bytes = session_priv.secret_bytes(); + payment.get_mut().remove(&session_priv_bytes, None); + } + fee + } else { + None + } + } } /// Returns whether a payment with the given [`PaymentHash`] and [`PaymentId`] is, in fact, a @@ -2769,6 +3064,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, } })), (13, invoice_request, option), + (14, trampoline_forward_info, option), (15, bolt12_invoice, option), (not_written, retry_strategy, (static_value, None)), (not_written, attempts, (static_value, PaymentAttempts::new())), @@ -2940,7 +3236,7 @@ mod tests { outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), 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, &pending_events, @@ -2986,7 +3282,7 @@ mod tests { outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), 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, &pending_events, diff --git a/lightning/src/ln/trampoline_forward_tests.rs b/lightning/src/ln/trampoline_forward_tests.rs new file mode 100644 index 00000000000..182a5eb523e --- /dev/null +++ b/lightning/src/ln/trampoline_forward_tests.rs @@ -0,0 +1,193 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Tests for trampoline MPP accumulation and forwarding validation in +//! [`ChannelManager::handle_trampoline_htlc`]. + +use crate::chain::transaction::OutPoint; +use crate::events::HTLCHandlingFailureReason; +use crate::ln::channelmanager::{HTLCPreviousHopData, MppPart}; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs; +use crate::ln::onion_utils::LocalHTLCFailureReason; +use crate::ln::outbound_payment::{NextTrampolineHopInfo, RecipientOnionFields}; +use crate::ln::types::ChannelId; +use crate::types::payment::{PaymentHash, PaymentSecret}; + +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + +fn test_prev_hop_data(htlc_id: u64) -> HTLCPreviousHopData { + HTLCPreviousHopData { + prev_outbound_scid_alias: 0, + user_channel_id: None, + htlc_id, + incoming_packet_shared_secret: [0; 32], + phantom_shared_secret: None, + trampoline_shared_secret: Some([0; 32]), + blinded_failure: None, + channel_id: ChannelId::from_bytes([0; 32]), + outpoint: OutPoint { txid: bitcoin::Txid::all_zeros(), index: 0 }, + counterparty_node_id: None, + cltv_expiry: None, + } +} + +fn test_trampoline_onion_packet() -> msgs::TrampolineOnionPacket { + let secp = Secp256k1::new(); + let test_secret = SecretKey::from_slice(&[42; 32]).unwrap(); + msgs::TrampolineOnionPacket { + version: 0, + public_key: PublicKey::from_secret_key(&secp, &test_secret), + hop_data: vec![0; 650], + hmac: [0; 32], + } +} + +fn test_onion_fields(total_msat: u64) -> RecipientOnionFields { + RecipientOnionFields { + payment_secret: Some(PaymentSecret([0; 32])), + total_mpp_amount_msat: total_msat, + payment_metadata: None, + custom_tlvs: Vec::new(), + } +} + +enum TrampolineMppValidationTestCase { + FeeInsufficient, + CltvInsufficient, + TrampolineAmountExceedsReceived, + TrampolineCLTVExceedsReceived, + MismatchedPaymentSecret, +} + +/// Sends two MPP parts through [`ChannelManager::handle_trampoline_htlc`], testing various MPP +/// validation steps with a base case that succeeds. +fn do_test_trampoline_mpp_validation(test_case: Option) { + let update_add_value: u64 = 500_000; // Actual amount we received in update_add_htlc. + let update_add_cltv: u32 = 500; // Actual CLTV we received in update_add_htlc. + let sender_intended_incoming_value: u64 = 500_000; // Amount we expect for one HTLC, outer onion. + let incoming_mpp_total: u64 = 1_000_000; // Total we expect to receive across MPP parts, outer onion. + let mut next_trampoline_amount: u64 = 750_000; // Total next trampoline expects, inner onion. + let mut next_trampoline_cltv: u32 = 100; // CLTV next trampoline expects, inner onion. + + // By default, set our forwarding fee and CLTV delta to exactly what we're being offered + // for this trampoline forward, so that we can force failures by just adding one. + let mut forwarding_fee_base_msat = incoming_mpp_total - next_trampoline_amount; + let mut cltv_delta = update_add_cltv - next_trampoline_cltv; + let mut mismatch_payment_secret = false; + + let expected = match test_case { + Some(TrampolineMppValidationTestCase::FeeInsufficient) => { + forwarding_fee_base_msat += 1; + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + }, + Some(TrampolineMppValidationTestCase::CltvInsufficient) => { + cltv_delta += 1; + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + }, + Some(TrampolineMppValidationTestCase::TrampolineAmountExceedsReceived) => { + next_trampoline_amount = incoming_mpp_total + 1; + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + }, + Some(TrampolineMppValidationTestCase::TrampolineCLTVExceedsReceived) => { + next_trampoline_cltv = update_add_cltv + 1; + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + }, + Some(TrampolineMppValidationTestCase::MismatchedPaymentSecret) => { + mismatch_payment_secret = true; + LocalHTLCFailureReason::InvalidTrampolineForward + }, + // We can't route to the next trampoline as they're unknown. + None => LocalHTLCFailureReason::TemporaryTrampolineFailure, + }; + + let chanmon_cfgs = create_chanmon_cfgs(1); + let node_cfgs = create_node_cfgs(1, &chanmon_cfgs); + let mut cfg = test_default_channel_config(); + cfg.channel_config.forwarding_fee_base_msat = forwarding_fee_base_msat as u32; + cfg.channel_config.forwarding_fee_proportional_millionths = 0; + cfg.channel_config.cltv_expiry_delta = cltv_delta as u16; + let node_chanmgrs = create_node_chanmgrs(1, &node_cfgs, &[Some(cfg)]); + let nodes = create_network(1, &node_cfgs, &node_chanmgrs); + + let payment_hash = PaymentHash([1; 32]); + + let secp = Secp256k1::new(); + let test_secret = SecretKey::from_slice(&[2; 32]).unwrap(); + let next_trampoline = PublicKey::from_secret_key(&secp, &test_secret); + let next_hop_info = NextTrampolineHopInfo { + onion_packet: test_trampoline_onion_packet(), + blinding_point: None, + amount_msat: next_trampoline_amount, + cltv_expiry_height: next_trampoline_cltv, + }; + + let htlc1 = MppPart::new( + test_prev_hop_data(0), + update_add_value, + sender_intended_incoming_value, + update_add_cltv, + ); + assert!(nodes[0] + .node + .test_handle_trampoline_htlc( + htlc1, + test_onion_fields(incoming_mpp_total), + payment_hash, + next_hop_info.clone(), + next_trampoline, + ) + .is_ok()); + + let htlc2 = MppPart::new( + test_prev_hop_data(1), + update_add_value, + sender_intended_incoming_value, + update_add_cltv, + ); + let onion2 = if mismatch_payment_secret { + RecipientOnionFields { + payment_secret: Some(PaymentSecret([1; 32])), + total_mpp_amount_msat: incoming_mpp_total, + payment_metadata: None, + custom_tlvs: Vec::new(), + } + } else { + test_onion_fields(incoming_mpp_total) + }; + let result = nodes[0].node.test_handle_trampoline_htlc( + htlc2, + onion2, + payment_hash, + next_hop_info, + next_trampoline, + ); + + assert_eq!( + HTLCHandlingFailureReason::from(&result.expect_err("expect trampoline failure").1), + HTLCHandlingFailureReason::Local { reason: expected }, + ); +} + +#[test] +fn test_trampoline_mpp_validation() { + do_test_trampoline_mpp_validation(Some(TrampolineMppValidationTestCase::FeeInsufficient)); + do_test_trampoline_mpp_validation(Some(TrampolineMppValidationTestCase::CltvInsufficient)); + do_test_trampoline_mpp_validation(Some( + TrampolineMppValidationTestCase::TrampolineAmountExceedsReceived, + )); + do_test_trampoline_mpp_validation(Some( + TrampolineMppValidationTestCase::TrampolineCLTVExceedsReceived, + )); + do_test_trampoline_mpp_validation(Some( + TrampolineMppValidationTestCase::MismatchedPaymentSecret, + )); + do_test_trampoline_mpp_validation(None); +} diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index edb048c8c7d..783df40649c 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -1326,7 +1326,7 @@ impl PaymentParameters { found_blinded_tail = true; } } - debug_assert!(found_blinded_tail); + //debug_assert!(found_blinded_tail); } } @@ -2473,7 +2473,7 @@ pub(crate) fn compute_fees(amount_msat: u64, channel_fees: RoutingFees) -> Optio /// Calculate the fees required to route the given amount over a channel with the given fees, /// saturating to [`u64::max_value`]. #[rustfmt::skip] -fn compute_fees_saturating(amount_msat: u64, channel_fees: RoutingFees) -> u64 { +pub(crate) fn compute_fees_saturating(amount_msat: u64, channel_fees: RoutingFees) -> u64 { amount_msat.checked_mul(channel_fees.proportional_millionths as u64) .map(|prop| prop / 1_000_000).unwrap_or(u64::max_value()) .saturating_add(channel_fees.base_msat as u64)