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