From 3d61431026d1e6a7b3bf313896a8b683343a5358 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Thu, 31 Oct 2024 09:46:22 -0700 Subject: [PATCH 1/5] Keep track of Trampoline support in blinded tails --- lightning/src/ln/blinded_payment_tests.rs | 3 ++- lightning/src/routing/router.rs | 8 ++++++++ lightning/src/routing/scoring.rs | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 79b24e06db3..74707205caf 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1443,7 +1443,8 @@ fn route_blinding_spec_test_vector() { hops: blinded_hops, blinding_point: bob_blinding_point, excess_final_cltv_expiry_delta: 0, - final_value_msat: amt_msat + final_value_msat: amt_msat, + final_hop_supports_trampoline: false }), }; let cur_height = 747_000; diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 4c01bdcc4c0..dc343576336 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -404,6 +404,8 @@ pub struct BlindedTail { pub excess_final_cltv_expiry_delta: u32, /// The total amount paid on this [`Path`], excluding the fees. pub final_value_msat: u64, + /// Used for determining the type of Trampoline path to construct + pub final_hop_supports_trampoline: bool } impl_writeable_tlv_based!(BlindedTail, { @@ -411,6 +413,7 @@ impl_writeable_tlv_based!(BlindedTail, { (2, blinding_point, required), (4, excess_final_cltv_expiry_delta, required), (6, final_value_msat, required), + (8, final_hop_supports_trampoline, required), }); /// A path in a [`Route`] to the payment recipient. Must always be at least length one. @@ -3342,6 +3345,7 @@ where L::Target: Logger { blinding_point: blinded_path.blinding_point(), excess_final_cltv_expiry_delta: 0, final_value_msat: h.fee_msat, + final_hop_supports_trampoline: false }) } else { None } }); @@ -7691,6 +7695,7 @@ mod tests { blinding_point: ln_test_utils::pubkey(43), excess_final_cltv_expiry_delta: 40, final_value_msat: 100, + final_hop_supports_trampoline: false, })}, Path { hops: vec![RouteHop { pubkey: ln_test_utils::pubkey(51), @@ -7717,6 +7722,7 @@ mod tests { blinding_point: ln_test_utils::pubkey(47), excess_final_cltv_expiry_delta: 41, final_value_msat: 101, + final_hop_supports_trampoline: false, }); let encoded_route = route.encode(); let decoded_route: Route = Readable::read(&mut Cursor::new(&encoded_route[..])).unwrap(); @@ -7753,6 +7759,7 @@ mod tests { blinding_point: ln_test_utils::pubkey(48), excess_final_cltv_expiry_delta: 0, final_value_msat: 200, + final_hop_supports_trampoline: false, }), }; inflight_htlcs.process_path(&path, ln_test_utils::pubkey(44)); @@ -7791,6 +7798,7 @@ mod tests { blinding_point: ln_test_utils::pubkey(44), excess_final_cltv_expiry_delta: 0, final_value_msat: 200, + final_hop_supports_trampoline: false, }), }], route_params: None}; diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index a3d02a5a484..c949f867824 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -3390,6 +3390,7 @@ mod tests { blinding_point: test_utils::pubkey(42), excess_final_cltv_expiry_delta: recipient_hop.cltv_expiry_delta, final_value_msat: recipient_hop.fee_msat, + final_hop_supports_trampoline: false }); // Check the liquidity before and after scoring payment failures to ensure the blinded path's From f1a1a0c9c315e98ec0797468d6abe964d8af5710 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Tue, 19 Mar 2024 01:15:06 -0700 Subject: [PATCH 2/5] Create TrampolineHop struct --- lightning/src/routing/router.rs | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index dc343576336..420a72a0a08 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -389,6 +389,41 @@ impl_writeable_tlv_based!(RouteHop, { (10, cltv_expiry_delta, required), }); +/// A Trampoline hop in a route, and additional metadata about it. "Hop" is defined as a node. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct TrampolineHop { + /// The node_id of the node at this hop. + pub pubkey: PublicKey, + /// The node_announcement features of the node at this hop. For the last hop, these may be + /// amended to match the features present in the invoice this node generated. + pub node_features: NodeFeatures, + /// The channel_announcement features of the channel that should be used from the previous hop + /// to reach this node. + pub channel_features: ChannelFeatures, + /// The fee taken on this hop (for paying for the use of the *next* channel in the path). + /// If this is the last hop in [`Path::hops`]: + /// * if we're sending to a [`BlindedPath`], this is the fee paid for use of the entire blinded path + /// * otherwise, this is the full value of this [`Path`]'s part of the payment + /// + /// [`BlindedPath`]: crate::blinded_path::BlindedPath + pub fee_msat: u64, + /// The CLTV delta added for this hop. + /// If this is the last hop in [`Path::hops`]: + /// * if we're sending to a [`BlindedPath`], this is the CLTV delta for the entire blinded path + /// * otherwise, this is the CLTV delta expected at the destination + /// + /// [`BlindedPath`]: crate::blinded_path::BlindedPath + pub cltv_expiry_delta: u32, +} + +impl_writeable_tlv_based!(TrampolineHop, { + (0, pubkey, required), + (2, node_features, required), + (6, channel_features, required), + (8, fee_msat, required), + (10, cltv_expiry_delta, required), +}); + /// The blinded portion of a [`Path`], if we're routing to a recipient who provided blinded paths in /// their [`Bolt12Invoice`]. /// From de9f85a2e6a150b701f98714ad303bd5298abd6e Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Tue, 19 Mar 2024 01:32:23 -0700 Subject: [PATCH 3/5] Add trampoline_hops field to Path --- fuzz/src/chanmon_consistency.rs | 2 ++ lightning-background-processor/src/lib.rs | 2 +- lightning/src/events/mod.rs | 8 +++--- lightning/src/ln/blinded_payment_tests.rs | 1 + lightning/src/ln/channel.rs | 3 ++- lightning/src/ln/channelmanager.rs | 4 +-- lightning/src/ln/functional_tests.rs | 4 +-- lightning/src/ln/onion_payment.rs | 3 ++- lightning/src/ln/onion_utils.rs | 2 +- lightning/src/ln/outbound_payment.rs | 3 ++- lightning/src/ln/payment_tests.rs | 30 +++++++++++------------ lightning/src/routing/router.rs | 18 +++++++++----- lightning/src/routing/scoring.rs | 6 ++--- 13 files changed, 49 insertions(+), 37 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index f897ba6e092..7c245e0aa80 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -566,6 +566,7 @@ fn send_payment( maybe_announced_channel: true, }], blinded_tail: None, + trampoline_hops: vec![], }], route_params: None, }, @@ -646,6 +647,7 @@ fn send_hop_payment( }, ], blinded_tail: None, + trampoline_hops: vec![], }], route_params: None, }, diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index af7c7ffb003..c6e7878c7e6 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -2540,7 +2540,7 @@ mod tests { fee_msat: 0, cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA as u32, maybe_announced_channel: true, - }], blinded_tail: None }; + }], trampoline_hops: vec![], blinded_tail: None }; $nodes[0].scorer.write_lock().expect(TestResult::PaymentFailure { path: path.clone(), short_channel_id: scored_scid }); $nodes[0].node.push_pending_event(Event::PaymentPathFailed { diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index c450c1d91cd..237158dafc4 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1894,7 +1894,7 @@ impl MaybeReadable for Event { payment_hash, payment_failed_permanently, failure, - path: Path { hops: path.unwrap(), blinded_tail }, + path: Path { hops: path.unwrap(), trampoline_hops: vec![], blinded_tail }, short_channel_id, #[cfg(test)] error_code, @@ -2034,7 +2034,7 @@ impl MaybeReadable for Event { Ok(Some(Event::PaymentPathSuccessful { payment_id: payment_id.0.unwrap(), payment_hash, - path: Path { hops: path, blinded_tail }, + path: Path { hops: path, trampoline_hops: vec![], blinded_tail }, })) }; f() @@ -2114,7 +2114,7 @@ impl MaybeReadable for Event { Ok(Some(Event::ProbeSuccessful { payment_id: payment_id.0.unwrap(), payment_hash: payment_hash.0.unwrap(), - path: Path { hops: path, blinded_tail }, + path: Path { hops: path, trampoline_hops: vec![], blinded_tail }, })) }; f() @@ -2131,7 +2131,7 @@ impl MaybeReadable for Event { Ok(Some(Event::ProbeFailed { payment_id: payment_id.0.unwrap(), payment_hash: payment_hash.0.unwrap(), - path: Path { hops: path, blinded_tail }, + path: Path { hops: path, trampoline_hops: vec![], blinded_tail }, short_channel_id, })) }; diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 74707205caf..4a96d19e34e 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1439,6 +1439,7 @@ fn route_blinding_spec_test_vector() { cltv_expiry_delta: 42, maybe_announced_channel: false, }], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: blinded_hops, blinding_point: bob_blinding_point, diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 42458e4769f..051742c78f9 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -9835,7 +9835,7 @@ mod tests { cltv_expiry: 200000000, state: OutboundHTLCState::Committed, source: HTLCSource::OutboundRoute { - path: Path { hops: Vec::new(), blinded_tail: None }, + path: Path { hops: Vec::new(), trampoline_hops: vec![], blinded_tail: None }, session_priv: SecretKey::from_slice(&>::from_hex("0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap()[..]).unwrap(), first_hop_htlc_msat: 548, payment_id: PaymentId([42; 32]), @@ -10211,6 +10211,7 @@ mod tests { node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: false, }], + trampoline_hops: vec![], blinded_tail: None }, session_priv: test_utils::privkey(42), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 3914384ca82..d213ea57d4b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -639,7 +639,7 @@ impl HTLCSource { pub fn dummy() -> Self { assert!(cfg!(not(feature = "grind_signatures"))); HTLCSource::OutboundRoute { - path: Path { hops: Vec::new(), blinded_tail: None }, + path: Path { hops: Vec::new(), trampoline_hops: Vec::new(), blinded_tail: None }, session_priv: SecretKey::from_slice(&[1; 32]).unwrap(), first_hop_htlc_msat: 0, payment_id: PaymentId([2; 32]), @@ -11667,7 +11667,7 @@ impl Readable for HTLCSource { // instead. payment_id = Some(PaymentId(*session_priv.0.unwrap().as_ref())); } - let path = Path { hops: path_hops, blinded_tail }; + let path = Path { hops: path_hops, trampoline_hops: vec![], blinded_tail }; if path.hops.len() == 0 { return Err(DecodeError::InvalidValue); } diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 42a57b114cb..e94f78f7eb7 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1093,7 +1093,7 @@ fn fake_network_test() { hops[1].fee_msat = chan_4.1.contents.fee_base_msat as u64 + chan_4.1.contents.fee_proportional_millionths as u64 * hops[2].fee_msat as u64 / 1000000; hops[0].fee_msat = chan_3.0.contents.fee_base_msat as u64 + chan_3.0.contents.fee_proportional_millionths as u64 * hops[1].fee_msat as u64 / 1000000; let payment_preimage_1 = send_along_route(&nodes[1], - Route { paths: vec![Path { hops, blinded_tail: None }], route_params: None }, + Route { paths: vec![Path { hops, trampoline_hops: vec![], blinded_tail: None }], route_params: None }, &vec!(&nodes[2], &nodes[3], &nodes[1])[..], 1000000).0; let mut hops = Vec::with_capacity(3); @@ -1127,7 +1127,7 @@ fn fake_network_test() { hops[1].fee_msat = chan_2.1.contents.fee_base_msat as u64 + chan_2.1.contents.fee_proportional_millionths as u64 * hops[2].fee_msat as u64 / 1000000; hops[0].fee_msat = chan_3.1.contents.fee_base_msat as u64 + chan_3.1.contents.fee_proportional_millionths as u64 * hops[1].fee_msat as u64 / 1000000; let payment_hash_2 = send_along_route(&nodes[1], - Route { paths: vec![Path { hops, blinded_tail: None }], route_params: None }, + Route { paths: vec![Path { hops, trampoline_hops: vec![], blinded_tail: None }], route_params: None }, &vec!(&nodes[3], &nodes[2], &nodes[1])[..], 1000000).1; // Claim the rebalances... diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 528b5e7d886..dc7504ca946 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -538,7 +538,7 @@ mod tests { // Ensure the onion will not fit all the payloads by adding a large custom TLV. recipient_onion.custom_tlvs.push((13377331, vec![0; 1156])); - let path = Path { hops, blinded_tail: None, }; + let path = Path { hops, trampoline_hops: vec![], blinded_tail: None, }; let onion_keys = super::onion_utils::construct_onion_keys(&secp_ctx, &path, &session_priv).unwrap(); let (onion_payloads, ..) = super::onion_utils::build_onion_payloads( &path, total_amt_msat, &recipient_onion, cur_height + 1, &Some(keysend_preimage) @@ -564,6 +564,7 @@ mod tests { let path = Path { hops: hops, + trampoline_hops: vec![], blinded_tail: None, }; diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 208096c128e..947ca608815 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1321,7 +1321,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops. }, - ], blinded_tail: None }], + ], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }; diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 9c8998b8970..9a7a5398473 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -2418,7 +2418,7 @@ mod tests { fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, - }], blinded_tail: None }], + }], trampoline_hops: vec![], blinded_tail: None }], route_params: Some(route_params.clone()), }; router.expect_find_route(route_params.clone(), Ok(route.clone())); @@ -2771,6 +2771,7 @@ mod tests { maybe_announced_channel: true, } ], + trampoline_hops: vec![], blinded_tail: None, } ], diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 7839b49be77..05f71153266 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -2462,7 +2462,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 2, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2471,7 +2471,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 2, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(route_params.clone()), }; @@ -2493,7 +2493,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 4, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2502,7 +2502,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 4, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(retry_1_params.clone()), }; @@ -2524,7 +2524,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 4, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(retry_2_params.clone()), }; @@ -2669,7 +2669,7 @@ fn auto_retry_zero_attempts_send_error() { fee_msat: amt_msat, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(route_params.clone()), }; @@ -2767,7 +2767,7 @@ fn retry_multi_path_single_failed_payment() { fee_msat: 10_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2776,7 +2776,7 @@ fn retry_multi_path_single_failed_payment() { fee_msat: 100_000_001, // Our default max-HTLC-value is 10% of the channel value, which this is one more than cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(route_params.clone()), }; @@ -2858,7 +2858,7 @@ fn immediate_retry_on_failure() { fee_msat: 100_000_001, // Our default max-HTLC-value is 10% of the channel value, which this is one more than cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(route_params.clone()), }; @@ -2952,7 +2952,7 @@ fn no_extra_retries_on_back_to_back_fail() { fee_msat: 100_000_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2969,7 +2969,7 @@ fn no_extra_retries_on_back_to_back_fail() { fee_msat: 100_000_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None } + }], trampoline_hops: vec![], blinded_tail: None } ], route_params: Some(route_params.clone()), }; @@ -3157,7 +3157,7 @@ fn test_simple_partial_retry() { fee_msat: 100_000_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -3174,7 +3174,7 @@ fn test_simple_partial_retry() { fee_msat: 100_000_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None } + }], trampoline_hops: vec![], blinded_tail: None } ], route_params: Some(route_params.clone()), }; @@ -3328,7 +3328,7 @@ fn test_threaded_payment_retries() { fee_msat: amt_msat / 1000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[2].node.get_our_node_id(), node_features: nodes[2].node.node_features(), @@ -3345,7 +3345,7 @@ fn test_threaded_payment_retries() { fee_msat: amt_msat - amt_msat / 1000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None } + }], trampoline_hops: vec![], blinded_tail: None } ], route_params: Some(route_params.clone()), }; diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 420a72a0a08..c54691d0ee5 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -457,6 +457,9 @@ impl_writeable_tlv_based!(BlindedTail, { pub struct Path { /// The list of unblinded hops in this [`Path`]. Must be at least length one. pub hops: Vec, + /// The list of unblinded Trampoline hops. If present, must be at least one. + /// The public key of the first Trampoline hop must match the public key of the last regular hop. + pub trampoline_hops: Vec, /// The blinded path at which this path terminates, if we're sending to one, and its metadata. pub blinded_tail: Option, } @@ -589,7 +592,7 @@ impl Readable for Route { if hops.is_empty() { return Err(DecodeError::InvalidValue); } min_final_cltv_expiry_delta = cmp::min(min_final_cltv_expiry_delta, hops.last().unwrap().cltv_expiry_delta); - paths.push(Path { hops, blinded_tail: None }); + paths.push(Path { hops, trampoline_hops: vec![], blinded_tail: None }); } _init_and_read_len_prefixed_tlv_fields!(reader, { (1, payment_params, (option: ReadableArgs, min_final_cltv_expiry_delta)), @@ -3390,7 +3393,7 @@ where L::Target: Logger { core::mem::replace(&mut hop.cltv_expiry_delta, prev_cltv_expiry_delta) }); - paths.push(Path { hops, blinded_tail }); + paths.push(Path { hops, trampoline_hops: vec![], blinded_tail }); } // Make sure we would never create a route with more paths than we allow. debug_assert!(paths.len() <= payment_params.max_path_count.into()); @@ -7114,7 +7117,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0, maybe_announced_channel: true, }, - ], blinded_tail: None }], + ], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }; @@ -7136,7 +7139,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true, }, - ], blinded_tail: None }, Path { hops: vec![ + ], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![ RouteHop { pubkey: PublicKey::from_slice(&>::from_hex("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), @@ -7147,7 +7150,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true, }, - ], blinded_tail: None }], + ], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }; @@ -7722,6 +7725,7 @@ mod tests { cltv_expiry_delta: 0, maybe_announced_channel: true, }], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: vec![ BlindedHop { blinded_node_id: ln_test_utils::pubkey(44), encrypted_payload: Vec::new() }, @@ -7740,7 +7744,7 @@ mod tests { fee_msat: 100, cltv_expiry_delta: 0, maybe_announced_channel: true, - }], blinded_tail: None }], + }], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }; let encoded_route = route.encode(); @@ -7789,6 +7793,7 @@ mod tests { cltv_expiry_delta: 0, maybe_announced_channel: false, }], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: vec![BlindedHop { blinded_node_id: ln_test_utils::pubkey(49), encrypted_payload: Vec::new() }], blinding_point: ln_test_utils::pubkey(48), @@ -7825,6 +7830,7 @@ mod tests { maybe_announced_channel: false, } ], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: vec![ BlindedHop { blinded_node_id: ln_test_utils::pubkey(45), encrypted_payload: Vec::new() }, diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index c949f867824..9a5147eabf7 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -2131,7 +2131,7 @@ mod tests { path_hop(source_pubkey(), 41, 1), path_hop(target_pubkey(), 42, 2), path_hop(recipient_pubkey(), 43, amount_msat), - ], blinded_tail: None, + ], trampoline_hops: vec![], blinded_tail: None, } } @@ -2607,7 +2607,7 @@ mod tests { }); assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 128); - scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 43, Duration::ZERO); + scorer.payment_path_failed(&Path { hops: path, trampoline_hops: vec![], blinded_tail: None }, 43, Duration::ZERO); let channel = network_graph.read_only().channel(42).unwrap().to_owned(); let (info, _) = channel.as_directed_from(&node_a).unwrap(); @@ -3304,7 +3304,7 @@ mod tests { path_hop(source_pubkey(), 42, 1), path_hop(sender_pubkey(), 41, 0), ]; - scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 42, Duration::from_secs(10 * (16 + 60 * 60))); + scorer.payment_path_failed(&Path { hops: path, trampoline_hops: vec![], blinded_tail: None }, 42, Duration::from_secs(10 * (16 + 60 * 60))); } #[test] From 5583b98c7b3d644be3f6174c43e9ad5500d614dc Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Sun, 27 Oct 2024 19:33:58 -0700 Subject: [PATCH 4/5] Trampoline payload construction method --- lightning/src/ln/onion_utils.rs | 118 +++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 947ca608815..85747194f1a 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -14,7 +14,7 @@ use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::channelmanager::{HTLCSource, RecipientOnionFields}; use crate::ln::msgs; use crate::routing::gossip::NetworkUpdate; -use crate::routing::router::{Path, RouteHop, RouteParameters}; +use crate::routing::router::{Path, RouteHop, RouteParameters, TrampolineHop}; use crate::sign::NodeSigner; use crate::types::features::{ChannelFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage}; @@ -175,6 +175,122 @@ pub(super) fn construct_onion_keys( Ok(res) } +fn build_trampoline_onion_payloads<'a>( + path: &'a Path, total_msat: u64, recipient_onion: &'a RecipientOnionFields, + starting_htlc_offset: u32, keysend_preimage: &Option, +) -> Result<(Vec>, u64, u32), APIError> { + let mut res: Vec = Vec::with_capacity( + path.trampoline_hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), + ); + let blinded_tail = path.blinded_tail.as_ref().ok_or(APIError::InvalidRoute { + err: "Routes using Trampoline must terminate blindly.".to_string(), + })?; + let blinded_tail_with_hop_iter = BlindedTailHopIter { + hops: blinded_tail.hops.iter(), + blinding_point: blinded_tail.blinding_point, + final_value_msat: blinded_tail.final_value_msat, + excess_final_cltv_expiry_delta: blinded_tail.excess_final_cltv_expiry_delta, + }; + + let (value_msat, cltv) = build_trampoline_onion_payloads_callback( + path.trampoline_hops.iter(), + blinded_tail_with_hop_iter, + total_msat, + recipient_onion, + starting_htlc_offset, + keysend_preimage, + |action, payload| match action { + PayloadCallbackAction::PushBack => res.push(payload), + PayloadCallbackAction::PushFront => res.insert(0, payload), + }, + )?; + Ok((res, value_msat, cltv)) +} + +fn build_trampoline_onion_payloads_callback<'a, H, B, F>( + hops: H, mut blinded_tail: BlindedTailHopIter<'a, B>, total_msat: u64, + recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, + keysend_preimage: &Option, mut callback: F, +) -> Result<(u64, u32), APIError> +where + H: DoubleEndedIterator, + B: ExactSizeIterator, + F: FnMut(PayloadCallbackAction, msgs::OutboundTrampolinePayload<'a>), +{ + let mut cur_value_msat = 0u64; + let mut cur_cltv = starting_htlc_offset; + let mut last_node_id = None; + + // appeasing the borrow checker + let mut blinded_tail_option = Some(blinded_tail); + + for (idx, hop) in hops.rev().enumerate() { + // First hop gets special values so that it can check, on receipt, that everything is + // exactly as it should be (and the next hop isn't trying to probe to find out if we're + // the intended recipient). + let value_msat = if cur_value_msat == 0 { hop.fee_msat } else { cur_value_msat }; + let cltv = if cur_cltv == starting_htlc_offset { + hop.cltv_expiry_delta + starting_htlc_offset + } else { + cur_cltv + }; + if idx == 0 { + if let Some(BlindedTailHopIter { + blinding_point, + hops, + final_value_msat, + excess_final_cltv_expiry_delta, + }) = blinded_tail_option.take() + { + let mut blinding_point = Some(blinding_point); + let hops_len = hops.len(); + for (i, blinded_hop) in hops.enumerate() { + if i == hops_len - 1 { + cur_value_msat += final_value_msat; + callback( + PayloadCallbackAction::PushBack, + msgs::OutboundTrampolinePayload::BlindedReceive { + sender_intended_htlc_amt_msat: final_value_msat, + total_msat, + cltv_expiry_height: cur_cltv + excess_final_cltv_expiry_delta, + encrypted_tlvs: &blinded_hop.encrypted_payload, + intro_node_blinding_point: blinding_point.take(), + keysend_preimage: *keysend_preimage, + custom_tlvs: &recipient_onion.custom_tlvs, + }, + ); + } else { + callback( + PayloadCallbackAction::PushBack, + msgs::OutboundTrampolinePayload::BlindedForward { + encrypted_tlvs: &blinded_hop.encrypted_payload, + intro_node_blinding_point: blinding_point.take(), + }, + ); + } + } + } + } else { + let payload = msgs::OutboundTrampolinePayload::Forward { + outgoing_node_id: last_node_id.unwrap(), + amt_to_forward: value_msat, + outgoing_cltv_value: cltv, + }; + callback(PayloadCallbackAction::PushFront, payload); + } + cur_value_msat += hop.fee_msat; + if cur_value_msat >= 21000000 * 100000000 * 1000 { + return Err(APIError::InvalidRoute { err: "Channel fees overflowed?".to_owned() }); + } + cur_cltv += hop.cltv_expiry_delta as u32; + if cur_cltv >= 500000000 { + return Err(APIError::InvalidRoute { err: "Channel CLTV overflowed?".to_owned() }); + } + last_node_id = Some(hop.pubkey); + } + Ok((cur_value_msat, cur_cltv)) +} + /// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. pub(super) fn build_onion_payloads<'a>( path: &'a Path, total_msat: u64, recipient_onion: &'a RecipientOnionFields, From 7cad33e3ab47bd96b4b09e27bfcef4dc37402fa1 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Wed, 6 Nov 2024 11:05:20 -0800 Subject: [PATCH 5/5] Return Trampoline payloads in `build_onion_payloads` --- lightning/src/ln/functional_tests.rs | 12 ++++++------ lightning/src/ln/onion_route_tests.rs | 10 +++++----- lightning/src/ln/onion_utils.rs | 21 ++++++++++++++++----- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index e94f78f7eb7..4c8471b7da4 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1437,7 +1437,7 @@ fn test_fee_spike_violation_fails_htlc() { let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv).unwrap(); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], + let (onion_payloads, _, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], 3460001, &recipient_onion_fields, cur_height, &None).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash).unwrap(); let msg = msgs::UpdateAddHTLC { @@ -1636,7 +1636,7 @@ fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { let cur_height = nodes[1].node.best_block.read().unwrap().height + 1; let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv).unwrap(); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], + let (onion_payloads, _, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(&route.paths[0], 700_000, &recipient_onion_fields, cur_height, &None).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash).unwrap(); let msg = msgs::UpdateAddHTLC { @@ -1816,7 +1816,7 @@ fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { let cur_height = nodes[0].node.best_block.read().unwrap().height + 1; let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route_2.paths[0], &session_priv).unwrap(); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + let (onion_payloads, _, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( &route_2.paths[0], recv_value_2, &recipient_onion_fields, cur_height, &None).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &our_payment_hash_1).unwrap(); let msg = msgs::UpdateAddHTLC { @@ -3555,7 +3555,7 @@ fn fail_backward_pending_htlc_upon_channel_failure() { let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); let current_height = nodes[1].node.best_block.read().unwrap().height + 1; let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (onion_payloads, _amount_msat, cltv_expiry) = onion_utils::build_onion_payloads( + let (onion_payloads, __trampoline_payloads, amount_msat, cltv_expiry) = onion_utils::build_onion_payloads( &route.paths[0], 50_000, &recipient_onion_fields, current_height, &None).unwrap(); let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv).unwrap(); let onion_routing_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash).unwrap(); @@ -6547,7 +6547,7 @@ fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { let cur_height = nodes[0].node.best_block.read().unwrap().height + 1; let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::signing_only(), &route.paths[0], &session_priv).unwrap(); let recipient_onion_fields = RecipientOnionFields::secret_only(our_payment_secret); - let (onion_payloads, _htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + let (onion_payloads, _trampoline_payloads, _htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( &route.paths[0], send_amt, &recipient_onion_fields, cur_height, &None).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &our_payment_hash).unwrap(); @@ -8290,7 +8290,7 @@ fn test_onion_value_mpp_set_calculation() { let session_priv = SecretKey::from_slice(&session_priv).unwrap(); let mut onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); let recipient_onion_fields = RecipientOnionFields::secret_only(our_payment_secret); - let (mut onion_payloads, _, _) = onion_utils::build_onion_payloads(&route.paths[0], 100_000, + let (mut onion_payloads, _, _, _) = onion_utils::build_onion_payloads(&route.paths[0], 100_000, &recipient_onion_fields, height + 1, &None).unwrap(); // Edit amt_to_forward to simulate the sender having set // the final amount and the routing node taking less fee diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index c0253166d99..ba2727c9292 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -355,7 +355,7 @@ fn test_onion_failure() { let cur_height = nodes[0].best_block_info().1 + 1; let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); - let (mut onion_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads( + let (mut onion_payloads, _trampoline_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads( &route.paths[0], 40000, &recipient_onion_fields, cur_height, &None).unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { @@ -374,7 +374,7 @@ fn test_onion_failure() { let cur_height = nodes[0].best_block_info().1 + 1; let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); - let (mut onion_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads( + let (mut onion_payloads, _trampoline_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads( &route.paths[0], 40000, &recipient_onion_fields, cur_height, &None).unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { @@ -626,7 +626,7 @@ fn test_onion_failure() { route.paths[0].hops[1].cltv_expiry_delta += CLTV_FAR_FAR_AWAY + route.paths[0].hops[0].cltv_expiry_delta + 1; let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); - let (onion_payloads, _, htlc_cltv) = onion_utils::build_onion_payloads( + let (onion_payloads, _, _, htlc_cltv) = onion_utils::build_onion_payloads( &route.paths[0], 40000, &recipient_onion_fields, height, &None).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash).unwrap(); msg.cltv_expiry = htlc_cltv; @@ -963,7 +963,7 @@ fn test_always_create_tlv_format_onion_payloads() { let cur_height = nodes[0].best_block_info().1 + 1; let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); - let (onion_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads( + let (onion_payloads, _trampoline_payloads, _htlc_msat, _htlc_cltv) = onion_utils::build_onion_payloads( &route.paths[0], 40000, &recipient_onion_fields, cur_height, &None).unwrap(); match onion_payloads[0] { @@ -1219,7 +1219,7 @@ fn test_phantom_invalid_onion_payload() { let session_priv = SecretKey::from_slice(&session_priv).unwrap(); let mut onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (mut onion_payloads, _, _) = onion_utils::build_onion_payloads( + let (mut onion_payloads, _, _, _) = onion_utils::build_onion_payloads( &route.paths[0], msgs::MAX_VALUE_MSAT + 1, &recipient_onion_fields, height + 1, &None).unwrap(); // We only want to construct the onion packet for the last hop, not the entire route, so diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 85747194f1a..48ced4f9b2e 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -15,7 +15,7 @@ use crate::ln::channelmanager::{HTLCSource, RecipientOnionFields}; use crate::ln::msgs; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{Path, RouteHop, RouteParameters, TrampolineHop}; -use crate::sign::NodeSigner; +use crate::sign::{EntropySource, NodeSigner}; use crate::types::features::{ChannelFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::errors::{self, APIError}; @@ -33,7 +33,7 @@ use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; use crate::io::{Cursor, Read}; use core::ops::Deref; - +use crate::blinded_path::payment::BlindedPaymentPath; #[allow(unused_imports)] use crate::prelude::*; @@ -185,6 +185,11 @@ fn build_trampoline_onion_payloads<'a>( let blinded_tail = path.blinded_tail.as_ref().ok_or(APIError::InvalidRoute { err: "Routes using Trampoline must terminate blindly.".to_string(), })?; + + if !blinded_tail.final_hop_supports_trampoline { + debug_assert!(false); + } + let blinded_tail_with_hop_iter = BlindedTailHopIter { hops: blinded_tail.hops.iter(), blinding_point: blinded_tail.blinding_point, @@ -295,7 +300,7 @@ where pub(super) fn build_onion_payloads<'a>( path: &'a Path, total_msat: u64, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, keysend_preimage: &Option, -) -> Result<(Vec>, u64, u32), APIError> { +) -> Result<(Vec>, Vec>, u64, u32), APIError> { let mut res: Vec = Vec::with_capacity( path.hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), ); @@ -306,6 +311,12 @@ pub(super) fn build_onion_payloads<'a>( excess_final_cltv_expiry_delta: bt.excess_final_cltv_expiry_delta, }); + let trampoline_payloads = if path.trampoline_hops.len() > 0 { + build_trampoline_onion_payloads(path, total_msat, recipient_onion, starting_htlc_offset, keysend_preimage)?.0 + } else { + vec![] + }; + let (value_msat, cltv) = build_onion_payloads_callback( path.hops.iter(), blinded_tail_with_hop_iter, @@ -318,7 +329,7 @@ pub(super) fn build_onion_payloads<'a>( PayloadCallbackAction::PushFront => res.insert(0, payload), }, )?; - Ok((res, value_msat, cltv)) + Ok((res, trampoline_payloads, value_msat, cltv)) } struct BlindedTailHopIter<'a, I: Iterator> { @@ -1278,7 +1289,7 @@ pub fn create_payment_onion( let onion_keys = construct_onion_keys(&secp_ctx, &path, &session_priv).map_err(|_| { APIError::InvalidRoute { err: "Pubkey along hop was maliciously selected".to_owned() } })?; - let (onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( + let (onion_payloads, _, htlc_msat, htlc_cltv) = build_onion_payloads( &path, total_msat, recipient_onion,