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