From 75cf8d222f9aa0ddcb3b8b3814963c851a5f1e4f Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Tue, 19 Mar 2024 01:15:06 -0700 Subject: [PATCH 1/4] Introduce TrampolineHop struct To define the hops in a Path that are meant as Trampoline hops, we need to introduce a new hop type much alike `RouteHop`, except these hops would not be defined by channel ID used to route payments. Instead, the preceding Trampoline hop would determine its own optimal path to route the payment onwards. --- lightning/src/routing/router.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index d87b5597c48..d1ac42f3621 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -429,6 +429,31 @@ 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. + pub node_features: NodeFeatures, + /// The fee this hop should use to pay for routing towards the next Trampoline hop, or to the + /// recipient if this is the last Trampoline hop. + /// If this is the last Trampoline hop within [`BlindedTail`], this is the fee paid for the use of + /// the entire blinded path. + pub fee_msat: u64, + /// The CLTV delta added for this hop. + /// If this is the last Trampoline hop within [`BlindedTail`], this is the CLTV delta for the entire + /// blinded path. + pub cltv_expiry_delta: u32, +} + +impl_writeable_tlv_based!(TrampolineHop, { + (0, pubkey, required), + (2, node_features, required), + (4, fee_msat, required), + (6, 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 03762baed6d22da96f50a504e3f4c53b85d87a25 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Tue, 19 Mar 2024 01:32:23 -0700 Subject: [PATCH 2/4] Add trampoline_hops field to BlindedTail Given that we do not intend to allow sending to unblinded recipients via Trampoline hops, we're adding a vector of unblinded `TrampolineHop`s directly to the `BlindedTail` struct. --- lightning/src/ln/blinded_payment_tests.rs | 1 + lightning/src/routing/router.rs | 17 +++++++++++++++-- lightning/src/routing/scoring.rs | 1 + lightning/src/util/ser.rs | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 9708dbd6d88..349538070c4 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1505,6 +1505,7 @@ fn route_blinding_spec_test_vector() { maybe_announced_channel: false, }], blinded_tail: Some(BlindedTail { + trampoline_hops: vec![], hops: blinded_hops, blinding_point: bob_blinding_point, excess_final_cltv_expiry_delta: 0, diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index d1ac42f3621..133964d0f60 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -399,13 +399,13 @@ pub struct RouteHop { /// 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 [`BlindedPaymentPath`], this is the fee paid for use of the entire - /// blinded path + /// blinded path (including any Trampoline hops) /// * otherwise, this is the full value of this [`Path`]'s part of the payment 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 [`BlindedPaymentPath`], this is the CLTV delta for the entire blinded - /// path + /// path (including any Trampoline hops) /// * otherwise, this is the CLTV delta expected at the destination pub cltv_expiry_delta: u32, /// Indicates whether this hop is possibly announced in the public network graph. @@ -460,6 +460,12 @@ impl_writeable_tlv_based!(TrampolineHop, { /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct BlindedTail { + /// The list of unblinded Trampoline hops. When using Trampoline, must contain at least one hop. + /// + /// Note that the first [`TrampolineHop`] node must also be present as the last [`RouteHop`] node, + /// where the [`RouteHop`]'s fee_msat is the fee paid for use of the entire blinded path, including + /// any Trampoline hops. + pub trampoline_hops: Vec, /// The hops of the [`BlindedPaymentPath`] provided by the recipient. pub hops: Vec, /// The blinding point of the [`BlindedPaymentPath`] provided by the recipient. @@ -476,6 +482,7 @@ impl_writeable_tlv_based!(BlindedTail, { (2, blinding_point, required), (4, excess_final_cltv_expiry_delta, required), (6, final_value_msat, required), + (8, trampoline_hops, optional_vec), }); /// A path in a [`Route`] to the payment recipient. Must always be at least length one. @@ -3404,6 +3411,8 @@ where L::Target: Logger { if let Some(blinded_path) = h.candidate.blinded_path() { final_cltv_delta = h.candidate.cltv_expiry_delta(); Some(BlindedTail { + // TODO: fill correctly + trampoline_hops: vec![], hops: blinded_path.blinded_hops().to_vec(), blinding_point: blinded_path.blinding_point(), excess_final_cltv_expiry_delta: 0, @@ -7750,6 +7759,7 @@ mod tests { maybe_announced_channel: true, }], blinded_tail: Some(BlindedTail { + trampoline_hops: vec![], hops: vec![ BlindedHop { blinded_node_id: ln_test_utils::pubkey(44), encrypted_payload: Vec::new() }, BlindedHop { blinded_node_id: ln_test_utils::pubkey(45), encrypted_payload: Vec::new() } @@ -7776,6 +7786,7 @@ mod tests { // (De)serialize a Route with two paths, each containing a blinded tail. route.paths[1].blinded_tail = Some(BlindedTail { + trampoline_hops: vec![], hops: vec![ BlindedHop { blinded_node_id: ln_test_utils::pubkey(48), encrypted_payload: Vec::new() }, BlindedHop { blinded_node_id: ln_test_utils::pubkey(49), encrypted_payload: Vec::new() } @@ -7815,6 +7826,7 @@ mod tests { maybe_announced_channel: false, }], blinded_tail: Some(BlindedTail { + trampoline_hops: vec![], hops: vec![BlindedHop { blinded_node_id: ln_test_utils::pubkey(49), encrypted_payload: Vec::new() }], blinding_point: ln_test_utils::pubkey(48), excess_final_cltv_expiry_delta: 0, @@ -7850,6 +7862,7 @@ mod tests { } ], blinded_tail: Some(BlindedTail { + trampoline_hops: vec![], hops: vec![ BlindedHop { blinded_node_id: ln_test_utils::pubkey(45), encrypted_payload: Vec::new() }, BlindedHop { blinded_node_id: ln_test_utils::pubkey(46), encrypted_payload: Vec::new() } diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index 431f1597c17..2feceb46df2 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -3571,6 +3571,7 @@ mod tests { let mut path = payment_path_for_amount(768); let recipient_hop = path.hops.pop().unwrap(); path.blinded_tail = Some(BlindedTail { + trampoline_hops: vec![], hops: vec![BlindedHop { blinded_node_id: test_utils::pubkey(44), encrypted_payload: Vec::new() }], blinding_point: test_utils::pubkey(42), excess_final_cltv_expiry_delta: recipient_hop.cltv_expiry_delta, diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 0f8dae0eb8d..b0ab1d3b844 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1044,6 +1044,7 @@ 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::TrampolineHop); impl_for_vec_with_element_length_prefix!(crate::ln::msgs::UpdateAddHTLC); impl_writeable_for_vec_with_element_length_prefix!(&crate::ln::msgs::UpdateAddHTLC); From eba3f902861686d6d9cca7f42442b167f61cc7d7 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Sun, 27 Oct 2024 19:33:58 -0700 Subject: [PATCH 3/4] Trampoline payload construction method Trampoline routing relies on serializing an inner onion within the hop data. In this commit, we introduce the method to serialize it based on the augmented path data introduced in the previous commit. --- lightning/src/ln/blinded_payment_tests.rs | 4 +- lightning/src/ln/functional_tests.rs | 12 +- .../src/ln/max_payment_path_len_tests.rs | 3 +- lightning/src/ln/msgs.rs | 3 - lightning/src/ln/onion_payment.rs | 2 +- lightning/src/ln/onion_route_tests.rs | 296 ++++++++++- lightning/src/ln/onion_utils.rs | 468 ++++++++++++++---- 7 files changed, 677 insertions(+), 111 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 349538070c4..b7fab93a3d9 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -365,7 +365,7 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); let cur_height = nodes[0].best_block_info().1; let (mut onion_payloads, ..) = onion_utils::build_onion_payloads( - &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None).unwrap(); + &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); // Remove the receive payload so the blinded forward payload is encoded as a final payload // (i.e. next_hop_hmac == [0; 32]) onion_payloads.pop(); @@ -935,7 +935,7 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { let cur_height = nodes[0].best_block_info().1; let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); let (mut onion_payloads, ..) = onion_utils::build_onion_payloads( - &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None).unwrap(); + &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); let update_add = &mut payment_event_1_2.msgs[0]; onion_payloads.last_mut().map(|p| { diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 48512d17026..7eaad38a55b 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1428,7 +1428,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], - 3460001, &recipient_onion_fields, cur_height, &None, None).unwrap(); + 3460001, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash).unwrap(); let msg = msgs::UpdateAddHTLC { channel_id: chan.2, @@ -1623,7 +1623,7 @@ fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { 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], - 700_000, &recipient_onion_fields, cur_height, &None, None).unwrap(); + 700_000, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash).unwrap(); let msg = msgs::UpdateAddHTLC { channel_id: chan.2, @@ -1803,7 +1803,7 @@ fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { 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( - &route_2.paths[0], recv_value_2, &recipient_onion_fields, cur_height, &None, None).unwrap(); + &route_2.paths[0], recv_value_2, &recipient_onion_fields, cur_height, &None, None, 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 { channel_id: chan.2, @@ -3856,7 +3856,7 @@ fn fail_backward_pending_htlc_upon_channel_failure() { 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( - &route.paths[0], 50_000, &recipient_onion_fields, current_height, &None, None).unwrap(); + &route.paths[0], 50_000, &recipient_onion_fields, current_height, &None, None, 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(); @@ -6852,7 +6852,7 @@ fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { 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( - &route.paths[0], send_amt, &recipient_onion_fields, cur_height, &None, None).unwrap(); + &route.paths[0], send_amt, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &our_payment_hash).unwrap(); let mut msg = msgs::UpdateAddHTLC { @@ -8607,7 +8607,7 @@ fn test_onion_value_mpp_set_calculation() { 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, - &recipient_onion_fields, height + 1, &None, None).unwrap(); + &recipient_onion_fields, height + 1, &None, None, None).unwrap(); // Edit amt_to_forward to simulate the sender having set // the final amount and the routing node taking less fee if let msgs::OutboundOnionPayload::Receive { diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index 9db12491b49..98c3a062bc4 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -281,7 +281,8 @@ fn blinded_path_with_custom_tlv() { let reserved_packet_bytes_without_custom_tlv: usize = onion_utils::build_onion_payloads( &route.paths[0], MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY, &RecipientOnionFields::spontaneous_empty(), - nodes[0].best_block_info().1 + DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, &None, None + nodes[0].best_block_info().1 + DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, &None, + None, None ) .unwrap() .0 diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 659ec65f6cf..b533c62bb17 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -1834,7 +1834,6 @@ mod fuzzy_internal_msgs { } pub(crate) enum OutboundTrampolinePayload<'a> { - #[allow(unused)] Forward { /// The value, in msat, of the payment after this hop's fee is deducted. amt_to_forward: u64, @@ -1854,12 +1853,10 @@ mod fuzzy_internal_msgs { /// If applicable, features of the BOLT12 invoice being paid. invoice_features: Option, }, - #[allow(unused)] BlindedForward { encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, }, - #[allow(unused)] BlindedReceive { sender_intended_htlc_amt_msat: u64, total_msat: u64, diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 254274fd16f..39241acc573 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -537,7 +537,7 @@ mod tests { let path = Path { hops, 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), None + &path, total_amt_msat, &recipient_onion, cur_height + 1, &Some(keysend_preimage), None, None ).unwrap(); assert!(super::onion_utils::construct_onion_packet( diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index c919a9e1a42..27bfd88775c 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -19,11 +19,11 @@ use crate::ln::channel::EXPIRE_PREV_CONFIG_TICKS; use crate::ln::channelmanager::{HTLCForwardInfo, FailureCode, CLTV_FAR_FAR_AWAY, DISABLE_GOSSIP_TICKS, MIN_CLTV_EXPIRY_DELTA, PendingAddHTLCInfo, PendingHTLCInfo, PendingHTLCRouting, PaymentId, RecipientOnionFields}; use crate::ln::onion_utils; use crate::routing::gossip::{NetworkUpdate, RoutingFees}; -use crate::routing::router::{get_route, PaymentParameters, Route, RouteParameters, RouteHint, RouteHintHop}; +use crate::routing::router::{get_route, PaymentParameters, Route, RouteParameters, RouteHint, RouteHintHop, Path, TrampolineHop, BlindedTail, RouteHop}; use crate::types::features::{InitFeatures, Bolt11InvoiceFeatures}; use crate::ln::functional_test_utils::test_default_channel_config; use crate::ln::msgs; -use crate::ln::msgs::{ChannelMessageHandler, ChannelUpdate, OutboundTrampolinePayload}; +use crate::ln::msgs::{ChannelMessageHandler, ChannelUpdate, FinalOnionHopData, OutboundOnionPayload, OutboundTrampolinePayload}; use crate::ln::wire::Encode; use crate::util::ser::{Writeable, Writer, BigSize}; use crate::util::test_utils; @@ -40,9 +40,11 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use crate::io; use crate::prelude::*; -use bitcoin::hex::FromHex; - +use bitcoin::hex::{DisplayHex, FromHex}; +use types::features::{ChannelFeatures, Features, NodeFeatures}; +use crate::blinded_path::BlindedHop; use crate::ln::functional_test_utils::*; +use crate::ln::onion_utils::{construct_trampoline_onion_keys, construct_trampoline_onion_packet}; fn run_onion_failure_test(_name: &str, test_case: u8, nodes: &Vec, route: &Route, payment_hash: &PaymentHash, payment_secret: &PaymentSecret, callback_msg: F1, callback_node: F2, expected_retryable: bool, expected_error_code: Option, expected_channel_update: Option, expected_short_channel_id: Option, expected_htlc_destination: Option) where F1: for <'a> FnMut(&'a mut msgs::UpdateAddHTLC), @@ -364,7 +366,7 @@ fn test_onion_failure() { 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( - &route.paths[0], 40000, &recipient_onion_fields, cur_height, &None, None).unwrap(); + &route.paths[0], 40000, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { new_payloads.push(BogusOnionHopData::new(payload)); @@ -383,7 +385,7 @@ fn test_onion_failure() { 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( - &route.paths[0], 40000, &recipient_onion_fields, cur_height, &None, None).unwrap(); + &route.paths[0], 40000, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { new_payloads.push(BogusOnionHopData::new(payload)); @@ -637,7 +639,7 @@ fn test_onion_failure() { 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( - &route.paths[0], 40000, &recipient_onion_fields, height, &None, None).unwrap(); + &route.paths[0], 40000, &recipient_onion_fields, height, &None, None, None).unwrap(); let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash).unwrap(); msg.cltv_expiry = htlc_cltv; msg.onion_routing_packet = onion_packet; @@ -975,7 +977,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( - &route.paths[0], 40000, &recipient_onion_fields, cur_height, &None, None).unwrap(); + &route.paths[0], 40000, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); match onion_payloads[0] { msgs::OutboundOnionPayload::Forward {..} => {}, @@ -1012,6 +1014,282 @@ fn test_trampoline_onion_payload_serialization() { assert_eq!(carol_payload_hex, "2e020405f5e10004030c35000e2102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"); } +#[test] +fn test_trampoline_onion_payload_assembly_values() { + // Test that we produce Trampoline and outer onion payloads that align with our expectations + // from the Path argument. Additionally, ensure that the fee and HTLC values using the + // `create_payment_onion` method, which hides some of the Trampoline onion inner workings, match + // the values we arrive at by assembling each onion explicitly in this test + let amt_msat = 150_000_000; + let cur_height = 800_000; + + let path = Path { + hops: vec![ + // Bob + RouteHop { + pubkey: PublicKey::from_slice(&>::from_hex("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()).unwrap(), + node_features: NodeFeatures::empty(), + short_channel_id: 0, + channel_features: ChannelFeatures::empty(), + fee_msat: 3_000, + cltv_expiry_delta: 24, + maybe_announced_channel: false, + }, + + // Carol + RouteHop { + pubkey: PublicKey::from_slice(&>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(), + node_features: NodeFeatures::empty(), + short_channel_id: (572330 << 40) + (42 << 16) + 2821, + channel_features: ChannelFeatures::empty(), + fee_msat: 153_000, + cltv_expiry_delta: 0, + maybe_announced_channel: false, + }, + ], + blinded_tail: Some(BlindedTail { + trampoline_hops: vec![ + // Carol's pubkey + TrampolineHop { + pubkey: PublicKey::from_slice(&>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(), + node_features: Features::empty(), + fee_msat: 2_500, + cltv_expiry_delta: 24, + }, + // Dave's pubkey (the intro node needs to be duplicated) + TrampolineHop { + pubkey: PublicKey::from_slice(&>::from_hex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991").unwrap()).unwrap(), + node_features: Features::empty(), + fee_msat: 150_500, + cltv_expiry_delta: 36, + } + ], + hops: vec![ + // Dave's blinded node id + BlindedHop { + blinded_node_id: PublicKey::from_slice(&>::from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be").unwrap()).unwrap(), + encrypted_payload: vec![], + }, + // Eve's blinded node id + BlindedHop { + blinded_node_id: PublicKey::from_slice(&>::from_hex("020e2dbadcc2005e859819ddebbe88a834ae8a6d2b049233c07335f15cd1dc5f22").unwrap()).unwrap(), + encrypted_payload: vec![], + } + ], + blinding_point: PublicKey::from_slice(&>::from_hex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e").unwrap()).unwrap(), + excess_final_cltv_expiry_delta: 0, + final_value_msat: amt_msat + }), + }; + assert_eq!(path.fee_msat(), 156_000); + assert_eq!(path.final_value_msat(), amt_msat); + assert_eq!(path.final_cltv_expiry_delta(), None); + + let payment_secret = PaymentSecret(SecretKey::from_slice(&>::from_hex("7494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da").unwrap()).unwrap().secret_bytes()); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&path.blinded_tail.as_ref().unwrap(), amt_msat, &recipient_onion_fields, cur_height, &None).unwrap(); + assert_eq!(trampoline_payloads.len(), 3); + assert_eq!(outer_total_msat, 150_153_000); + assert_eq!(outer_starting_htlc_offset, 800_060); + + let trampoline_carol_payload = &trampoline_payloads[0]; + let trampoline_dave_payload = &trampoline_payloads[1]; + let trampoline_eve_payload = &trampoline_payloads[2]; + if let OutboundTrampolinePayload::BlindedReceive { sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, .. } = trampoline_eve_payload { + assert_eq!(sender_intended_htlc_amt_msat, &150_000_000); + assert_eq!(total_msat, &150_000_000); + assert_eq!(cltv_expiry_height, &800_000); + } else { + panic!("Eve Trampoline payload must be BlindedReceive"); + } + + if let OutboundTrampolinePayload::BlindedForward { .. } = trampoline_dave_payload {} else { + panic!("Dave Trampoline payload must be BlindedForward"); + } + + if let OutboundTrampolinePayload::Forward { amt_to_forward, outgoing_cltv_value, .. } = trampoline_carol_payload { + assert_eq!(amt_to_forward, &150_150_500); + assert_eq!(outgoing_cltv_value, &800_036); + } else { + panic!("Carol Trampoline payload must be Forward"); + } + + // all dummy values + let secp_ctx = Secp256k1::new(); + let session_priv = SecretKey::from_slice(&>::from_hex("a64feb81abd58e473df290e9e1c07dc3e56114495cadf33191f44ba5448ebe99").unwrap()).unwrap(); + let prng_seed = onion_utils::gen_pad_from_shared_secret(&session_priv.secret_bytes()); + let payment_hash = PaymentHash(session_priv.secret_bytes()); + + let onion_keys = construct_trampoline_onion_keys(&secp_ctx, &path.blinded_tail.as_ref().unwrap(), &session_priv).unwrap(); + let trampoline_packet = construct_trampoline_onion_packet( + trampoline_payloads, + onion_keys, + prng_seed, + &payment_hash, + None, + ).unwrap(); + + let (outer_payloads, total_msat, total_htlc_offset) = onion_utils::build_onion_payloads(&path, outer_total_msat, &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap(); + assert_eq!(outer_payloads.len(), 2); + assert_eq!(total_msat, 150_156_000); + assert_eq!(total_htlc_offset, 800_084); + + let outer_bob_payload = &outer_payloads[0]; + let outer_carol_payload = &outer_payloads[1]; + if let OutboundOnionPayload::TrampolineEntrypoint { amt_to_forward, outgoing_cltv_value, .. } = outer_carol_payload { + assert_eq!(amt_to_forward, &150_153_000); + assert_eq!(outgoing_cltv_value, &800_060); + } else { + panic!("Carol payload must be TrampolineEntrypoint"); + } + if let OutboundOnionPayload::Forward { amt_to_forward, outgoing_cltv_value, .. } = outer_bob_payload { + assert_eq!(amt_to_forward, &150_153_000); + assert_eq!(outgoing_cltv_value, &800_084); + } else { + panic!("Bob payload must be Forward"); + } +} + +#[test] +fn test_trampoline_onion_payload_construction_vectors() { + // As per https://github.com/lightning/bolts/blob/fa0594ac2af3531d734f1d707a146d6e13679451/bolt04/trampoline-to-blinded-path-payment-onion-test.json#L251 + + let trampoline_payload_carol = OutboundTrampolinePayload::Forward { + amt_to_forward: 150_150_500, + outgoing_cltv_value: 800_036, + outgoing_node_id: PublicKey::from_slice(&>::from_hex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991").unwrap()).unwrap(), + }; + let carol_payload = trampoline_payload_carol.encode().to_lower_hex_string(); + assert_eq!(carol_payload, "2e020408f31d6404030c35240e21032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"); + + let trampoline_payload_dave = OutboundTrampolinePayload::BlindedForward { + encrypted_tlvs: &>::from_hex("0ccf3c8a58deaa603f657ee2a5ed9d604eb5c8ca1e5f801989afa8f3ea6d789bbdde2c7e7a1ef9ca8c38d2c54760febad8446d3f273ddb537569ef56613846ccd3aba78a").unwrap(), + intro_node_blinding_point: Some(PublicKey::from_slice(&>::from_hex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e").unwrap()).unwrap()), + }; + let dave_payload = trampoline_payload_dave.encode().to_lower_hex_string(); + assert_eq!(dave_payload, "690a440ccf3c8a58deaa603f657ee2a5ed9d604eb5c8ca1e5f801989afa8f3ea6d789bbdde2c7e7a1ef9ca8c38d2c54760febad8446d3f273ddb537569ef56613846ccd3aba78a0c2102988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e"); + + let trampoline_payload_eve = OutboundTrampolinePayload::BlindedReceive { + sender_intended_htlc_amt_msat: 150_000_000, + total_msat: 150_000_000, + cltv_expiry_height: 800_000, + encrypted_tlvs: &>::from_hex("bcd747394fbd4d99588da075a623316e15a576df5bc785cccc7cd6ec7b398acce6faf520175f9ec920f2ef261cdb83dc28cc3a0eeb970107b3306489bf771ef5b1213bca811d345285405861d08a655b6c237fa247a8b4491beee20c878a60e9816492026d8feb9dafa84585b253978db6a0aa2945df5ef445c61e801fb82f43d5f00716baf9fc9b3de50bc22950a36bda8fc27bfb1242e5860c7e687438d4133e058770361a19b6c271a2a07788d34dccc27e39b9829b061a4d960eac4a2c2b0f4de506c24f9af3868c0aff6dda27281c").unwrap(), + intro_node_blinding_point: None, + keysend_preimage: None, + custom_tlvs: &vec![], + }; + let eve_payload = trampoline_payload_eve.encode().to_lower_hex_string(); + assert_eq!(eve_payload, "e4020408f0d18004030c35000ad1bcd747394fbd4d99588da075a623316e15a576df5bc785cccc7cd6ec7b398acce6faf520175f9ec920f2ef261cdb83dc28cc3a0eeb970107b3306489bf771ef5b1213bca811d345285405861d08a655b6c237fa247a8b4491beee20c878a60e9816492026d8feb9dafa84585b253978db6a0aa2945df5ef445c61e801fb82f43d5f00716baf9fc9b3de50bc22950a36bda8fc27bfb1242e5860c7e687438d4133e058770361a19b6c271a2a07788d34dccc27e39b9829b061a4d960eac4a2c2b0f4de506c24f9af3868c0aff6dda27281c120408f0d180"); + + let trampoline_payloads = vec![trampoline_payload_carol, trampoline_payload_dave, trampoline_payload_eve]; + + let trampoline_session_key = SecretKey::from_slice(&>::from_hex("a64feb81abd58e473df290e9e1c07dc3e56114495cadf33191f44ba5448ebe99").unwrap()).unwrap(); + let associated_data_slice = SecretKey::from_slice(&>::from_hex("e89bc505e84aaca09613833fc58c9069078fb43bfbea0488f34eec9db99b5f82").unwrap()).unwrap(); + let associated_data = PaymentHash(associated_data_slice.secret_bytes()); + + let trampoline_hops = Path { + hops: vec![], + blinded_tail: Some(BlindedTail { + trampoline_hops: vec![ + // Carol's pubkey + TrampolineHop { + pubkey: PublicKey::from_slice(&>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(), + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + // Dave's pubkey (the intro node needs to be duplicated) + TrampolineHop { + pubkey: PublicKey::from_slice(&>::from_hex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991").unwrap()).unwrap(), + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + } + ], + hops: vec![ + // Dave's blinded node id + BlindedHop { + blinded_node_id: PublicKey::from_slice(&>::from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be").unwrap()).unwrap(), + encrypted_payload: vec![], + }, + // Eve's blinded node id + BlindedHop { + blinded_node_id: PublicKey::from_slice(&>::from_hex("020e2dbadcc2005e859819ddebbe88a834ae8a6d2b049233c07335f15cd1dc5f22").unwrap()).unwrap(), + encrypted_payload: vec![], + } + ], + blinding_point: PublicKey::from_slice(&>::from_hex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e").unwrap()).unwrap(), + excess_final_cltv_expiry_delta: 0, + final_value_msat: 0 + }), + }; + + let trampoline_onion_keys = construct_trampoline_onion_keys(&Secp256k1::new(), &trampoline_hops.blinded_tail.unwrap(), &trampoline_session_key).unwrap(); + let trampoline_onion_packet = construct_trampoline_onion_packet(trampoline_payloads, trampoline_onion_keys, [0u8; 32], &associated_data, None).unwrap(); + let trampoline_onion_packet_hex = trampoline_onion_packet.encode().to_lower_hex_string(); + assert_eq!(trampoline_onion_packet_hex, "0002bc59a9abc893d75a8d4f56a6572f9a3507323a8de22abe0496ea8d37da166a8b4bba0e560f1a9deb602bfd98fe9167141d0b61d669df90c0149096d505b85d3d02806e6c12caeb308b878b6bc7f1b15839c038a6443cd3bec3a94c2293165375555f6d7720862b525930f41fddcc02260d197abd93fb58e60835fd97d9dc14e7979c12f59df08517b02e3e4d50e1817de4271df66d522c4e9675df71c635c4176a8381bc22b342ff4e9031cede87f74cc039fca74aa0a3786bc1db2e158a9a520ecb99667ef9a6bbfaf5f0e06f81c27ca48134ba2103229145937c5dc7b8ecc5201d6aeb592e78faa3c05d3a035df77628f0be9b1af3ef7d386dd5cc87b20778f47ebd40dbfcf12b9071c5d7112ab84c3e0c5c14867e684d09a18bc93ac47d73b7343e3403ef6e3b70366835988920e7d772c3719d3596e53c29c4017cb6938421a557ce81b4bb26701c25bf622d4c69f1359dc85857a375c5c74987a4d3152f66987001c68a50c4bf9e0b1dab4ad1a64b0535319bbf6c4fbe4f9c50cb65f5ef887bfb91b0a57c0f86ba3d91cbeea1607fb0c12c6c75d03bbb0d3a3019c40597027f5eebca23083e50ec79d41b1152131853525bf3fc13fb0be62c2e3ce733f59671eee5c4064863fb92ae74be9ca68b9c716f9519fd268478ee27d91d466b0de51404de3226b74217d28250ead9d2c95411e0230570f547d4cc7c1d589791623131aa73965dccc5aa17ec12b442215ce5d346df664d799190df5dd04a13"); + + let outer_payloads = vec![ + // Bob + OutboundOnionPayload::Forward { + short_channel_id: (572330 << 40) + (42 << 16) + 2821, + amt_to_forward: 150153000, + outgoing_cltv_value: 800060, + }, + + // Carol + OutboundOnionPayload::TrampolineEntrypoint { + amt_to_forward: 150153000, + outgoing_cltv_value: 800060, + trampoline_packet: trampoline_onion_packet, + multipath_trampoline_data: Some(FinalOnionHopData{ + payment_secret: PaymentSecret(SecretKey::from_slice(&>::from_hex("7494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da").unwrap()).unwrap().secret_bytes()), + total_msat: 150153000 + }), + } + ]; + + let outer_hops = Path { + hops: vec![ + // Bob + RouteHop { + pubkey: PublicKey::from_slice(&>::from_hex("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()).unwrap(), + node_features: NodeFeatures::empty(), + short_channel_id: 0, + channel_features: ChannelFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + maybe_announced_channel: false, + }, + + // Carol + RouteHop { + pubkey: PublicKey::from_slice(&>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(), + node_features: NodeFeatures::empty(), + short_channel_id: 0, + channel_features: ChannelFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + maybe_announced_channel: false, + }, + ], + blinded_tail: None, + }; + + let bob_payload = outer_payloads[0].encode().to_lower_hex_string(); + assert_eq!(bob_payload, "15020408f3272804030c353c060808bbaa00002a0b05"); + + let carol_payload = outer_payloads[1].encode().to_lower_hex_string(); + assert_eq!(carol_payload, "fd0255020408f3272804030c353c08247494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da08f3272814fd02200002bc59a9abc893d75a8d4f56a6572f9a3507323a8de22abe0496ea8d37da166a8b4bba0e560f1a9deb602bfd98fe9167141d0b61d669df90c0149096d505b85d3d02806e6c12caeb308b878b6bc7f1b15839c038a6443cd3bec3a94c2293165375555f6d7720862b525930f41fddcc02260d197abd93fb58e60835fd97d9dc14e7979c12f59df08517b02e3e4d50e1817de4271df66d522c4e9675df71c635c4176a8381bc22b342ff4e9031cede87f74cc039fca74aa0a3786bc1db2e158a9a520ecb99667ef9a6bbfaf5f0e06f81c27ca48134ba2103229145937c5dc7b8ecc5201d6aeb592e78faa3c05d3a035df77628f0be9b1af3ef7d386dd5cc87b20778f47ebd40dbfcf12b9071c5d7112ab84c3e0c5c14867e684d09a18bc93ac47d73b7343e3403ef6e3b70366835988920e7d772c3719d3596e53c29c4017cb6938421a557ce81b4bb26701c25bf622d4c69f1359dc85857a375c5c74987a4d3152f66987001c68a50c4bf9e0b1dab4ad1a64b0535319bbf6c4fbe4f9c50cb65f5ef887bfb91b0a57c0f86ba3d91cbeea1607fb0c12c6c75d03bbb0d3a3019c40597027f5eebca23083e50ec79d41b1152131853525bf3fc13fb0be62c2e3ce733f59671eee5c4064863fb92ae74be9ca68b9c716f9519fd268478ee27d91d466b0de51404de3226b74217d28250ead9d2c95411e0230570f547d4cc7c1d589791623131aa73965dccc5aa17ec12b442215ce5d346df664d799190df5dd04a13"); + + let outer_session_key = SecretKey::from_slice(&>::from_hex("4f777e8dac16e6dfe333066d9efb014f7a51d11762ff76eca4d3a95ada99ba3e").unwrap()).unwrap(); + let outer_onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &outer_hops, &outer_session_key).unwrap(); + let outer_onion_prng_seed = onion_utils::gen_pad_from_shared_secret(&outer_session_key.secret_bytes()); + let outer_onion_packet = onion_utils::construct_onion_packet(outer_payloads, outer_onion_keys, outer_onion_prng_seed, &associated_data).unwrap(); + let outer_onion_packet_hex = outer_onion_packet.encode().to_lower_hex_string(); + assert_eq!(outer_onion_packet_hex, "00025fd60556c134ae97e4baedba220a644037754ee67c54fd05e93bf40c17cbb73362fb9dee96001ff229945595b6edb59437a6bc143406d3f90f749892a84d8d430c6890437d26d5bfc599d565316ef51347521075bbab87c59c57bcf20af7e63d7192b46cf171e4f73cb11f9f603915389105d91ad630224bea95d735e3988add1e24b5bf28f1d7128db64284d90a839ba340d088c74b1fb1bd21136b1809428ec5399c8649e9bdf92d2dcfc694deae5046fa5b2bdf646847aaad73f5e95275763091c90e71031cae1f9a770fdea559642c9c02f424a2a28163dd0957e3874bd28a97bec67d18c0321b0e68bc804aa8345b17cb626e2348ca06c8312a167c989521056b0f25c55559d446507d6c491d50605cb79fa87929ce64b0a9860926eeaec2c431d926a1cadb9a1186e4061cb01671a122fc1f57602cbef06d6c194ec4b715c2e3dd4120baca3172cd81900b49fef857fb6d6afd24c983b608108b0a5ac0c1c6c52011f23b8778059ffadd1bb7cd06e2525417365f485a7fd1d4a9ba3818ede7cdc9e71afee8532252d08e2531ca52538655b7e8d912f7ec6d37bbcce8d7ec690709dbf9321e92c565b78e7fe2c22edf23e0902153d1ca15a112ad32fb19695ec65ce11ddf670da7915f05ad4b86c154fb908cb567315d1124f303f75fa075ebde8ef7bb12e27737ad9e4924439097338ea6d7a6fc3721b88c9b830a34e8d55f4c582b74a3895cc848fe57f4fe29f115dabeb6b3175be15d94408ed6771109cfaf57067ae658201082eae7605d26b1449af4425ae8e8f58cdda5c6265f1fd7a386fc6cea3074e4f25b909b96175883676f7610a00fdf34df9eb6c7b9a4ae89b839c69fd1f285e38cdceb634d782cc6d81179759bc9fd47d7fd060470d0b048287764c6837963274e708314f017ac7dc26d0554d59bfcfd3136225798f65f0b0fea337c6b256ebbb63a90b994c0ab93fd8b1d6bd4c74aebe535d6110014cd3d525394027dfe8faa98b4e9b2bee7949eb1961f1b026791092f84deea63afab66603dbe9b6365a102a1fef2f6b9744bc1bb091a8da9130d34d4d39f25dbad191649cfb67e10246364b7ce0c6ec072f9690cabb459d9fda0c849e17535de4357e9907270c75953fca3c845bb613926ecf73205219c7057a4b6bb244c184362bb4e2f24279dc4e60b94a5b1ec11c34081a628428ba5646c995b9558821053ba9c84a05afbf00dabd60223723096516d2f5668f3ec7e11612b01eb7a3a0506189a2272b88e89807943adb34291a17f6cb5516ffd6f945a1c42a524b21f096d66f350b1dad4db455741ae3d0e023309fbda5ef55fb0dc74f3297041448b2be76c525141963934c6afc53d263fb7836626df502d7c2ee9e79cbbd87afd84bbb8dfbf45248af3cd61ad5fac827e7683ca4f91dfad507a8eb9c17b2c9ac5ec051fe645a4a6cb37136f6f19b611e0ea8da7960af2d779507e55f57305bc74b7568928c5dd5132990fe54c22117df91c257d8c7b61935a018a28c1c3b17bab8e4294fa699161ec21123c9fc4e71079df31f300c2822e1246561e04765d3aab333eafd026c7431ac7616debb0e022746f4538e1c6348b600c988eeb2d051fc60c468dca260a84c79ab3ab8342dc345a764672848ea234e17332bc124799daf7c5fcb2e2358514a7461357e1c19c802c5ee32deccf1776885dd825bedd5f781d459984370a6b7ae885d4483a76ddb19b30f47ed47cd56aa5a079a89793dbcad461c59f2e002067ac98dd5a534e525c9c46c2af730741bf1f8629357ec0bfc0bc9ecb31af96777e507648ff4260dc3673716e098d9111dfd245f1d7c55a6de340deb8bd7a053e5d62d760f184dc70ca8fa255b9023b9b9aedfb6e419a5b5951ba0f83b603793830ee68d442d7b88ee1bbf6bbd1bcd6f68cc1af"); +} + fn do_test_fail_htlc_backwards_with_reason(failure_code: FailureCode) { let chanmon_cfgs = create_chanmon_cfgs(2); @@ -1235,7 +1513,7 @@ fn test_phantom_invalid_onion_payload() { let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); let (mut onion_payloads, _, _) = onion_utils::build_onion_payloads( &route.paths[0], msgs::MAX_VALUE_MSAT + 1, - &recipient_onion_fields, height + 1, &None, None).unwrap(); + &recipient_onion_fields, height + 1, &None, None, None).unwrap(); // We only want to construct the onion packet for the last hop, not the entire route, so // remove the first hop's payload and its keys. onion_keys.remove(0); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 4140fb17088..ae38be57233 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::offers::invoice_request::InvoiceRequest; use crate::routing::gossip::NetworkUpdate; -use crate::routing::router::{Path, RouteHop, RouteParameters}; +use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters, TrampolineHop}; use crate::sign::NodeSigner; use crate::types::features::{ChannelFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage}; @@ -109,26 +109,212 @@ pub(crate) fn next_hop_pubkey( curr_pubkey.mul_tweak(secp_ctx, &Scalar::from_be_bytes(blinding_factor).unwrap()) } -// can only fail if an intermediary hop has an invalid public key or session_priv is invalid +trait HopInfo { + fn node_pubkey(&self) -> &PublicKey; +} + +trait PathHop { + type HopId; + fn hop_id(&self) -> Self::HopId; + fn fee_msat(&self) -> u64; + fn cltv_expiry_delta(&self) -> u32; +} + +impl HopInfo for RouteHop { + fn node_pubkey(&self) -> &PublicKey { + &self.pubkey + } +} + +impl<'a> PathHop for &'a RouteHop { + type HopId = u64; // scid + + fn hop_id(&self) -> Self::HopId { + self.short_channel_id + } + + fn fee_msat(&self) -> u64 { + self.fee_msat + } + + fn cltv_expiry_delta(&self) -> u32 { + self.cltv_expiry_delta + } +} + +impl HopInfo for TrampolineHop { + fn node_pubkey(&self) -> &PublicKey { + &self.pubkey + } +} + +impl<'a> PathHop for &'a TrampolineHop { + type HopId = PublicKey; + + fn hop_id(&self) -> Self::HopId { + self.pubkey + } + + fn fee_msat(&self) -> u64 { + self.fee_msat + } + + fn cltv_expiry_delta(&self) -> u32 { + self.cltv_expiry_delta + } +} + +trait OnionPayload<'a, 'b> { + type PathHopForId: PathHop + 'b; + type ReceiveType: OnionPayload<'a, 'b>; + fn new_forward( + hop_id: <>::PathHopForId as PathHop>::HopId, + amt_to_forward: u64, outgoing_cltv_value: u32, + ) -> Self; + fn new_receive( + recipient_onion: &'a RecipientOnionFields, keysend_preimage: Option, + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + ) -> Result; + fn new_blinded_forward( + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + ) -> Self; + fn new_blinded_receive( + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + keysend_preimage: Option, invoice_request: Option<&'a InvoiceRequest>, + custom_tlvs: &'a Vec<(u64, Vec)>, + ) -> Self; + fn new_trampoline_entry( + total_msat: u64, amt_to_forward: u64, outgoing_cltv_value: u32, + recipient_onion: &'a RecipientOnionFields, packet: msgs::TrampolineOnionPacket, + ) -> Result; +} +impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { + type PathHopForId = &'b RouteHop; + type ReceiveType = msgs::OutboundOnionPayload<'a>; + fn new_forward(short_channel_id: u64, amt_to_forward: u64, outgoing_cltv_value: u32) -> Self { + Self::Forward { short_channel_id, amt_to_forward, outgoing_cltv_value } + } + fn new_receive( + recipient_onion: &'a RecipientOnionFields, keysend_preimage: Option, + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + ) -> Result { + Ok(Self::Receive { + payment_data: recipient_onion + .payment_secret + .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }), + payment_metadata: recipient_onion.payment_metadata.as_ref(), + keysend_preimage, + custom_tlvs: &recipient_onion.custom_tlvs, + sender_intended_htlc_amt_msat, + cltv_expiry_height, + }) + } + fn new_blinded_forward( + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + ) -> Self { + Self::BlindedForward { encrypted_tlvs, intro_node_blinding_point } + } + fn new_blinded_receive( + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + keysend_preimage: Option, invoice_request: Option<&'a InvoiceRequest>, + custom_tlvs: &'a Vec<(u64, Vec)>, + ) -> Self { + Self::BlindedReceive { + sender_intended_htlc_amt_msat, + total_msat, + cltv_expiry_height, + encrypted_tlvs, + intro_node_blinding_point, + keysend_preimage, + invoice_request, + custom_tlvs, + } + } + + fn new_trampoline_entry( + total_msat: u64, amt_to_forward: u64, outgoing_cltv_value: u32, + recipient_onion: &'a RecipientOnionFields, packet: msgs::TrampolineOnionPacket, + ) -> 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, + }) + } +} +impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { + type PathHopForId = &'b TrampolineHop; + type ReceiveType = msgs::OutboundTrampolinePayload<'a>; + fn new_forward( + outgoing_node_id: PublicKey, amt_to_forward: u64, outgoing_cltv_value: u32, + ) -> Self { + Self::Forward { outgoing_node_id, amt_to_forward, outgoing_cltv_value } + } + fn new_receive( + _recipient_onion: &'a RecipientOnionFields, _keysend_preimage: Option, + _sender_intended_htlc_amt_msat: u64, _total_msat: u64, _cltv_expiry_height: u32, + ) -> Result { + Err(APIError::InvalidRoute { + err: "Unblinded receiving is not supported for Trampoline!".to_string(), + }) + } + fn new_blinded_forward( + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + ) -> Self { + Self::BlindedForward { encrypted_tlvs, intro_node_blinding_point } + } + fn new_blinded_receive( + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + keysend_preimage: Option, _invoice_request: Option<&'a InvoiceRequest>, + custom_tlvs: &'a Vec<(u64, Vec)>, + ) -> Self { + Self::BlindedReceive { + sender_intended_htlc_amt_msat, + total_msat, + cltv_expiry_height, + encrypted_tlvs, + intro_node_blinding_point, + keysend_preimage, + custom_tlvs, + } + } + + fn new_trampoline_entry( + _total_msat: u64, _amt_to_forward: u64, _outgoing_cltv_value: u32, + _recipient_onion: &'a RecipientOnionFields, _packet: msgs::TrampolineOnionPacket, + ) -> Result { + Err(APIError::InvalidRoute { + err: "Trampoline onions cannot contain Trampoline entrypoints!".to_string(), + }) + } +} + #[inline] -pub(super) fn construct_onion_keys_callback( - secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, mut callback: FType, +fn construct_onion_keys_generic_callback( + secp_ctx: &Secp256k1, hops: &[H], blinded_tail: Option<&BlindedTail>, + session_priv: &SecretKey, mut callback: FType, ) -> Result<(), secp256k1::Error> where T: secp256k1::Signing, - FType: FnMut(SharedSecret, [u8; 32], PublicKey, Option<&RouteHop>, usize), + H: HopInfo, + FType: FnMut(SharedSecret, [u8; 32], PublicKey, Option<&H>, usize), { let mut blinded_priv = session_priv.clone(); let mut blinded_pub = PublicKey::from_secret_key(secp_ctx, &blinded_priv); - let unblinded_hops_iter = path.hops.iter().map(|h| (&h.pubkey, Some(h))); - let blinded_pks_iter = path - .blinded_tail - .as_ref() + let unblinded_hops_iter = hops.iter().map(|h| (h.node_pubkey(), Some(h))); + let blinded_pks_iter = blinded_tail .map(|t| t.hops.iter()) .unwrap_or([].iter()) .skip(1) // Skip the intro node because it's included in the unblinded hops .map(|h| (&h.blinded_node_id, None)); + for (idx, (pubkey, route_hop_opt)) in unblinded_hops_iter.chain(blinded_pks_iter).enumerate() { let shared_secret = SharedSecret::new(pubkey, &blinded_priv); @@ -154,9 +340,10 @@ pub(super) fn construct_onion_keys( ) -> Result, secp256k1::Error> { let mut res = Vec::with_capacity(path.hops.len()); - construct_onion_keys_callback( + construct_onion_keys_generic_callback( secp_ctx, - &path, + &path.hops, + path.blinded_tail.as_ref(), session_priv, |shared_secret, _blinding_factor, ephemeral_pubkey, _, _| { let (rho, mu) = gen_rho_mu_from_shared_secret(shared_secret.as_ref()); @@ -176,20 +363,91 @@ pub(super) fn construct_onion_keys( Ok(res) } +// can only fail if an intermediary hop has an invalid public key or session_priv is invalid +pub(super) fn construct_trampoline_onion_keys( + secp_ctx: &Secp256k1, blinded_tail: &BlindedTail, session_priv: &SecretKey, +) -> Result, secp256k1::Error> { + let mut res = Vec::with_capacity(blinded_tail.trampoline_hops.len()); + + construct_onion_keys_generic_callback( + secp_ctx, + &blinded_tail.trampoline_hops, + Some(blinded_tail), + session_priv, + |shared_secret, _blinding_factor, ephemeral_pubkey, _, _| { + let (rho, mu) = gen_rho_mu_from_shared_secret(shared_secret.as_ref()); + + res.push(OnionKeys { + #[cfg(test)] + shared_secret, + #[cfg(test)] + blinding_factor: _blinding_factor, + ephemeral_pubkey, + rho, + mu, + }); + }, + )?; + + Ok(res) +} + +pub(super) fn build_trampoline_onion_payloads<'a>( + blinded_tail: &'a BlindedTail, 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(blinded_tail.trampoline_hops.len() + blinded_tail.hops.len()); + let blinded_tail_with_hop_iter = BlindedTailDetails::DirectEntry { + 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_onion_payloads_callback( + blinded_tail.trampoline_hops.iter(), + Some(blinded_tail_with_hop_iter), + total_msat, + recipient_onion, + starting_htlc_offset, + keysend_preimage, + None, + |action, payload| match action { + PayloadCallbackAction::PushBack => res.push(payload), + PayloadCallbackAction::PushFront => res.insert(0, payload), + }, + )?; + Ok((res, value_msat, 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, starting_htlc_offset: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, + trampoline_packet: Option, ) -> Result<(Vec>, u64, u32), APIError> { let mut res: Vec = Vec::with_capacity( path.hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), ); - let blinded_tail_with_hop_iter = path.blinded_tail.as_ref().map(|bt| BlindedTailHopIter { - hops: bt.hops.iter(), - blinding_point: bt.blinding_point, - final_value_msat: bt.final_value_msat, - excess_final_cltv_expiry_delta: bt.excess_final_cltv_expiry_delta, + + // When Trampoline hops are present, they are presumed to follow the non-Trampoline hops, which + // means that the blinded path needs not be appended to the regular hops, and is only included + // among the Trampoline onion payloads. + let blinded_tail_with_hop_iter = path.blinded_tail.as_ref().map(|bt| { + if let Some(trampoline_packet) = trampoline_packet { + return BlindedTailDetails::TrampolineEntry { + trampoline_packet, + final_value_msat: bt.final_value_msat, + }; + } + BlindedTailDetails::DirectEntry { + hops: bt.hops.iter(), + blinding_point: bt.blinding_point, + final_value_msat: bt.final_value_msat, + excess_final_cltv_expiry_delta: bt.excess_final_cltv_expiry_delta, + } }); let (value_msat, cltv) = build_onion_payloads_callback( @@ -208,110 +466,135 @@ pub(super) fn build_onion_payloads<'a>( Ok((res, value_msat, cltv)) } -struct BlindedTailHopIter<'a, I: Iterator> { - hops: I, - blinding_point: PublicKey, - final_value_msat: u64, - excess_final_cltv_expiry_delta: u32, +enum BlindedTailDetails<'a, I: Iterator> { + DirectEntry { + hops: I, + blinding_point: PublicKey, + final_value_msat: u64, + excess_final_cltv_expiry_delta: u32, + }, + TrampolineEntry { + trampoline_packet: msgs::TrampolineOnionPacket, + final_value_msat: u64, + }, } + enum PayloadCallbackAction { PushBack, PushFront, } -fn build_onion_payloads_callback<'a, H, B, F>( - hops: H, mut blinded_tail: Option>, total_msat: u64, +fn build_onion_payloads_callback<'a, 'b, H, B, F, OP>( + hops: H, mut blinded_tail: Option>, total_msat: u64, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, mut callback: F, ) -> Result<(u64, u32), APIError> where - H: DoubleEndedIterator, + H: DoubleEndedIterator, B: ExactSizeIterator, - F: FnMut(PayloadCallbackAction, msgs::OutboundOnionPayload<'a>), + F: FnMut(PayloadCallbackAction, OP), + OP: OnionPayload<'a, 'b, ReceiveType = OP>, { let mut cur_value_msat = 0u64; let mut cur_cltv = starting_htlc_offset; - let mut last_short_channel_id = 0; + let mut last_hop_id = None; 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 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.saturating_add(starting_htlc_offset) + hop.cltv_expiry_delta().saturating_add(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.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::OutboundOnionPayload::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, - invoice_request, - custom_tlvs: &recipient_onion.custom_tlvs, - }, - ); - } else { - callback( - PayloadCallbackAction::PushBack, - msgs::OutboundOnionPayload::BlindedForward { - encrypted_tlvs: &blinded_hop.encrypted_payload, - intro_node_blinding_point: blinding_point.take(), - }, - ); + match blinded_tail.take() { + Some(BlindedTailDetails::DirectEntry { + blinding_point, + hops, + final_value_msat, + excess_final_cltv_expiry_delta, + .. + }) => { + 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, + OP::new_blinded_receive( + final_value_msat, + total_msat, + cur_cltv + excess_final_cltv_expiry_delta, + &blinded_hop.encrypted_payload, + blinding_point.take(), + *keysend_preimage, + invoice_request, + &recipient_onion.custom_tlvs, + ), + ); + } else { + callback( + PayloadCallbackAction::PushBack, + OP::new_blinded_forward( + &blinded_hop.encrypted_payload, + blinding_point.take(), + ), + ); + } } - } - } else { - callback( - PayloadCallbackAction::PushBack, - msgs::OutboundOnionPayload::Receive { - payment_data: recipient_onion.payment_secret.map(|payment_secret| { - msgs::FinalOnionHopData { payment_secret, total_msat } - }), - payment_metadata: recipient_onion.payment_metadata.as_ref(), - keysend_preimage: *keysend_preimage, - custom_tlvs: &recipient_onion.custom_tlvs, - sender_intended_htlc_amt_msat: value_msat, - cltv_expiry_height: cltv, - }, - ); + }, + Some(BlindedTailDetails::TrampolineEntry { + trampoline_packet, + final_value_msat, + }) => { + cur_value_msat += final_value_msat; + callback( + PayloadCallbackAction::PushBack, + OP::new_trampoline_entry( + total_msat, + final_value_msat + hop.fee_msat(), + cur_cltv, + &recipient_onion, + trampoline_packet, + )?, + ); + }, + None => { + callback( + PayloadCallbackAction::PushBack, + OP::new_receive( + &recipient_onion, + *keysend_preimage, + value_msat, + total_msat, + cltv, + )?, + ); + }, } } else { - let payload = msgs::OutboundOnionPayload::Forward { - short_channel_id: last_short_channel_id, - amt_to_forward: value_msat, - outgoing_cltv_value: cltv, - }; + let payload = OP::new_forward( + last_hop_id.ok_or(APIError::InvalidRoute { + err: "Next hop ID must be known for non-final hops".to_string(), + })?, + value_msat, + cltv, + ); callback(PayloadCallbackAction::PushFront, payload); } - cur_value_msat += hop.fee_msat; + 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 = cur_cltv.saturating_add(hop.cltv_expiry_delta as u32); + cur_cltv = cur_cltv.saturating_add(hop.cltv_expiry_delta() as u32); if cur_cltv >= 500000000 { return Err(APIError::InvalidRoute { err: "Channel CLTV overflowed?".to_owned() }); } - last_short_channel_id = hop.short_channel_id; + last_hop_id = Some(hop.hop_id()); } Ok((cur_value_msat, cur_cltv)) } @@ -344,7 +627,7 @@ pub(crate) fn set_max_path_length( .blinded_route_hints() .iter() .max_by_key(|path| path.inner_blinded_path().serialized_length()) - .map(|largest_path| BlindedTailHopIter { + .map(|largest_path| BlindedTailDetails::DirectEntry { hops: largest_path.blinded_hops().iter(), blinding_point: largest_path.blinding_point(), final_value_msat: final_value_msat_with_overpay_buffer, @@ -371,7 +654,7 @@ pub(crate) fn set_max_path_length( best_block_height, &keysend_preimage, invoice_request, - |_, payload| { + |_, payload: msgs::OutboundOnionPayload| { num_reserved_bytes = num_reserved_bytes .saturating_add(payload.serialized_length()) .saturating_add(PAYLOAD_HMAC_LEN); @@ -900,8 +1183,14 @@ where } }; - construct_onion_keys_callback(secp_ctx, &path, session_priv, callback) - .expect("Route that we sent via spontaneously grew invalid keys in the middle of it?"); + construct_onion_keys_generic_callback( + secp_ctx, + &path.hops, + path.blinded_tail.as_ref(), + session_priv, + callback, + ) + .expect("Route we used spontaneously grew invalid keys in the middle of it?"); if let Some(FailureLearnings { network_update, @@ -1197,6 +1486,7 @@ pub fn create_payment_onion( cur_block_height, keysend_preimage, invoice_request, + None )?; let onion_packet = construct_onion_packet(onion_payloads, onion_keys, prng_seed, payment_hash) .map_err(|_| APIError::InvalidRoute { From 7b56d51223a9aaccce56ae4e77053bb61e853bc1 Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Fri, 7 Feb 2025 12:19:13 -0800 Subject: [PATCH 4/4] Construct Trampoline component in create_payment_onion Now that we can generate the inner Trampoline onions, we need to update the `create_payment_onion` utility method to consider the newly available path data and incorporate the encoded Trampoline onion in the outer hop data. This also comes with a behavioral change for blinded tails: when Trampoline are present, we assume that they precede the blinded tail, which must therefore be serialized as part of the Trampoline onion. --- lightning/src/ln/blinded_payment_tests.rs | 94 ++++++++++++++++++- lightning/src/ln/msgs.rs | 1 - lightning/src/ln/onion_route_tests.rs | 15 ++++ lightning/src/ln/onion_utils.rs | 104 +++++++++++++++++++--- 4 files changed, 198 insertions(+), 16 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index b7fab93a3d9..314797729e7 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -8,6 +8,7 @@ // licenses. use bitcoin::hashes::hex::FromHex; +use bitcoin::hex::DisplayHex; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, schnorr}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; @@ -30,12 +31,14 @@ use crate::ln::outbound_payment::{Retry, IDEMPOTENCY_TIMEOUT_TICKS}; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::offers::nonce::Nonce; use crate::prelude::*; -use crate::routing::router::{BlindedTail, Path, Payee, PaymentParameters, RouteHop, RouteParameters}; +use crate::routing::router::{BlindedTail, Path, Payee, PaymentParameters, RouteHop, RouteParameters, TrampolineHop}; use crate::sign::{NodeSigner, Recipient}; use crate::util::config::UserConfig; -use crate::util::ser::WithoutLength; +use crate::util::ser::{WithoutLength, Writeable}; use crate::util::test_utils; use lightning_invoice::RawBolt11Invoice; +use types::features::Features; +use crate::blinded_path::BlindedHop; pub fn blinded_payment_path( payment_secret: PaymentSecret, intro_node_min_htlc: u64, intro_node_max_htlc: u64, @@ -362,15 +365,17 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { ForwardCheckFail::ForwardPayloadEncodedAsReceive => { let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); - let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); + let mut onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap(); let cur_height = nodes[0].best_block_info().1; let (mut onion_payloads, ..) = onion_utils::build_onion_payloads( &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); // Remove the receive payload so the blinded forward payload is encoded as a final payload // (i.e. next_hop_hmac == [0; 32]) onion_payloads.pop(); + onion_keys.pop(); if $target_node_idx + 1 < nodes.len() { onion_payloads.pop(); + onion_keys.pop(); } $update_add.onion_routing_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash).unwrap(); }, @@ -1655,3 +1660,86 @@ fn route_blinding_spec_test_vector() { _ => panic!("Unexpected error") } } + +#[test] +fn test_combined_trampoline_onion_creation_vectors() { + // As per https://github.com/lightning/bolts/blob/fa0594ac2af3531d734f1d707a146d6e13679451/bolt04/trampoline-to-blinded-path-payment-onion-test.json#L251 + + let mut secp_ctx = Secp256k1::new(); + let session_priv = secret_from_hex("a64feb81abd58e473df290e9e1c07dc3e56114495cadf33191f44ba5448ebe99"); + + let path = Path { + hops: vec![ + // Bob + RouteHop { + pubkey: pubkey_from_hex("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"), + node_features: NodeFeatures::empty(), + short_channel_id: 0, + channel_features: ChannelFeatures::empty(), + fee_msat: 3_000, + cltv_expiry_delta: 0, + maybe_announced_channel: false, + }, + + // Carol + RouteHop { + pubkey: pubkey_from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"), + node_features: NodeFeatures::empty(), + short_channel_id: (572330 << 40) + (42 << 16) + 2821, + channel_features: ChannelFeatures::empty(), + fee_msat: 153_000, + cltv_expiry_delta: 0, + maybe_announced_channel: false, + }, + ], + blinded_tail: Some(BlindedTail { + trampoline_hops: vec![ + // Carol's pubkey + TrampolineHop { + pubkey: pubkey_from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"), + node_features: Features::empty(), + fee_msat: 2_500, + cltv_expiry_delta: 24, + }, + // Dave's pubkey (the intro node needs to be duplicated) + TrampolineHop { + pubkey: pubkey_from_hex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"), + node_features: Features::empty(), + fee_msat: 150_500, // incorporate both base and proportional fee + cltv_expiry_delta: 36, + } + ], + hops: vec![ + // Dave's blinded node id + BlindedHop { + blinded_node_id: pubkey_from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be"), + encrypted_payload: bytes_from_hex("0ccf3c8a58deaa603f657ee2a5ed9d604eb5c8ca1e5f801989afa8f3ea6d789bbdde2c7e7a1ef9ca8c38d2c54760febad8446d3f273ddb537569ef56613846ccd3aba78a"), + }, + // Eve's blinded node id + BlindedHop { + blinded_node_id: pubkey_from_hex("020e2dbadcc2005e859819ddebbe88a834ae8a6d2b049233c07335f15cd1dc5f22"), + encrypted_payload: bytes_from_hex("bcd747394fbd4d99588da075a623316e15a576df5bc785cccc7cd6ec7b398acce6faf520175f9ec920f2ef261cdb83dc28cc3a0eeb970107b3306489bf771ef5b1213bca811d345285405861d08a655b6c237fa247a8b4491beee20c878a60e9816492026d8feb9dafa84585b253978db6a0aa2945df5ef445c61e801fb82f43d5f00716baf9fc9b3de50bc22950a36bda8fc27bfb1242e5860c7e687438d4133e058770361a19b6c271a2a07788d34dccc27e39b9829b061a4d960eac4a2c2b0f4de506c24f9af3868c0aff6dda27281c"), + } + ], + blinding_point: pubkey_from_hex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e"), + excess_final_cltv_expiry_delta: 0, + final_value_msat: 150_000_000 + }), + }; + + let associated_data_slice = secret_from_hex("e89bc505e84aaca09613833fc58c9069078fb43bfbea0488f34eec9db99b5f82"); + let associated_data = PaymentHash(associated_data_slice.secret_bytes()); + let payment_secret = PaymentSecret(secret_from_hex("7494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da").secret_bytes()); + let outer_session_key = secret_from_hex("4f777e8dac16e6dfe333066d9efb014f7a51d11762ff76eca4d3a95ada99ba3e"); + let outer_onion_prng_seed = onion_utils::gen_pad_from_shared_secret(&outer_session_key.secret_bytes()); + + let amt_msat = 150_000_000; + let cur_height = 800_000; + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let (bob_onion, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion_internal(&secp_ctx, &path, &session_priv, amt_msat, &recipient_onion_fields, cur_height, &associated_data, &None, None, [0; 32], Some(outer_session_key), Some(outer_onion_prng_seed)).unwrap(); + + let outer_onion_packet_hex = bob_onion.encode().to_lower_hex_string(); + assert_eq!(outer_onion_packet_hex, "00025fd60556c134ae97e4baedba220a644037754ee67c54fd05e93bf40c17cbb73362fb9dee96001ff229945595b6edb59437a6bc143406d3f90f749892a84d8d430c6890437d26d5bfc599d565316ef51347521075bbab87c59c57bcf20af7e63d7192b46cf171e4f73cb11f9f603915389105d91ad630224bea95d735e3988add1e24b5bf28f1d7128db64284d90a839ba340d088c74b1fb1bd21136b1809428ec5399c8649e9bdf92d2dcfc694deae5046fa5b2bdf646847aaad73f5e95275763091c90e71031cae1f9a770fdea559642c9c02f424a2a28163dd0957e3874bd28a97bec67d18c0321b0e68bc804aa8345b17cb626e2348ca06c8312a167c989521056b0f25c55559d446507d6c491d50605cb79fa87929ce64b0a9860926eeaec2c431d926a1cadb9a1186e4061cb01671a122fc1f57602cbef06d6c194ec4b715c2e3dd4120baca3172cd81900b49fef857fb6d6afd24c983b608108b0a5ac0c1c6c52011f23b8778059ffadd1bb7cd06e2525417365f485a7fd1d4a9ba3818ede7cdc9e71afee8532252d08e2531ca52538655b7e8d912f7ec6d37bbcce8d7ec690709dbf9321e92c565b78e7fe2c22edf23e0902153d1ca15a112ad32fb19695ec65ce11ddf670da7915f05ad4b86c154fb908cb567315d1124f303f75fa075ebde8ef7bb12e27737ad9e4924439097338ea6d7a6fc3721b88c9b830a34e8d55f4c582b74a3895cc848fe57f4fe29f115dabeb6b3175be15d94408ed6771109cfaf57067ae658201082eae7605d26b1449af4425ae8e8f58cdda5c6265f1fd7a386fc6cea3074e4f25b909b96175883676f7610a00fdf34df9eb6c7b9a4ae89b839c69fd1f285e38cdceb634d782cc6d81179759bc9fd47d7fd060470d0b048287764c6837963274e708314f017ac7dc26d0554d59bfcfd3136225798f65f0b0fea337c6b256ebbb63a90b994c0ab93fd8b1d6bd4c74aebe535d6110014cd3d525394027dfe8faa98b4e9b2bee7949eb1961f1b026791092f84deea63afab66603dbe9b6365a102a1fef2f6b9744bc1bb091a8da9130d34d4d39f25dbad191649cfb67e10246364b7ce0c6ec072f9690cabb459d9fda0c849e17535de4357e9907270c75953fca3c845bb613926ecf73205219c7057a4b6bb244c184362bb4e2f24279dc4e60b94a5b1ec11c34081a628428ba5646c995b9558821053ba9c84a05afbf00dabd60223723096516d2f5668f3ec7e11612b01eb7a3a0506189a2272b88e89807943adb34291a17f6cb5516ffd6f945a1c42a524b21f096d66f350b1dad4db455741ae3d0e023309fbda5ef55fb0dc74f3297041448b2be76c525141963934c6afc53d263fb7836626df502d7c2ee9e79cbbd87afd84bbb8dfbf45248af3cd61ad5fac827e7683ca4f91dfad507a8eb9c17b2c9ac5ec051fe645a4a6cb37136f6f19b611e0ea8da7960af2d779507e55f57305bc74b7568928c5dd5132990fe54c22117df91c257d8c7b61935a018a28c1c3b17bab8e4294fa699161ec21123c9fc4e71079df31f300c2822e1246561e04765d3aab333eafd026c7431ac7616debb0e022746f4538e1c6348b600c988eeb2d051fc60c468dca260a84c79ab3ab8342dc345a764672848ea234e17332bc124799daf7c5fcb2e2358514a7461357e1c19c802c5ee32deccf1776885dd825bedd5f781d459984370a6b7ae885d4483a76ddb19b30f47ed47cd56aa5a079a89793dbcad461c59f2e002067ac98dd5a534e525c9c46c2af730741bf1f8629357ec0bfc0bc9ecb31af96777e507648ff4260dc3673716e098d9111dfd245f1d7c55a6de340deb8bd7a053e5d62d760f184dc70ca8fa255b9023b9b9aedfb6e419a5b5951ba0f83b603793830ee68d442d7b88ee1bbf6bbd1bcd6f68cc1af"); + assert_eq!(htlc_msat, 150_156_000); + assert_eq!(htlc_cltv, 800_060); +} diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index b533c62bb17..09865c8837e 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -1802,7 +1802,6 @@ mod fuzzy_internal_msgs { amt_to_forward: u64, outgoing_cltv_value: u32, }, - #[allow(unused)] TrampolineEntrypoint { amt_to_forward: u64, outgoing_cltv_value: u32, diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index 27bfd88775c..cee8bd1d301 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -1148,6 +1148,21 @@ fn test_trampoline_onion_payload_assembly_values() { } else { panic!("Bob payload must be Forward"); } + + let (_, total_msat_combined, total_htlc_offset_combined) = onion_utils::create_payment_onion( + &Secp256k1::new(), + &path, + &session_priv, + amt_msat, + &recipient_onion_fields, + cur_height, + &payment_hash, + &None, + None, + prng_seed, + ).unwrap(); + assert_eq!(total_msat_combined, total_msat); + assert_eq!(total_htlc_offset_combined, total_htlc_offset); } #[test] diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index ae38be57233..6e0a162406c 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -340,10 +340,16 @@ pub(super) fn construct_onion_keys( ) -> Result, secp256k1::Error> { let mut res = Vec::with_capacity(path.hops.len()); + let blinded_tail = path.blinded_tail.as_ref().and_then(|t| { + if !t.trampoline_hops.is_empty() { + return None; + } + Some(t) + }); construct_onion_keys_generic_callback( secp_ctx, &path.hops, - path.blinded_tail.as_ref(), + blinded_tail, session_priv, |shared_secret, _blinding_factor, ephemeral_pubkey, _, _| { let (rho, mu) = gen_rho_mu_from_shared_secret(shared_secret.as_ref()); @@ -698,6 +704,8 @@ pub(super) fn construct_onion_packet( let mut chacha = ChaCha20::new(&prng_seed, &[0; 8]); chacha.process(&[0; ONION_DATA_LEN], &mut packet_data); + debug_assert_eq!(payloads.len(), onion_keys.len(), "Payloads and keys must have equal lengths"); + let packet = FixedSizeOnionPacket(packet_data); construct_onion_packet_with_init_noise::<_, _>( payloads, @@ -707,7 +715,6 @@ pub(super) fn construct_onion_packet( ) } -#[allow(unused)] pub(super) fn construct_trampoline_onion_packet( payloads: Vec, onion_keys: Vec, prng_seed: [u8; 32], associated_data: &PaymentHash, length: Option, @@ -1476,22 +1483,95 @@ pub fn create_payment_onion( keysend_preimage: &Option, invoice_request: Option<&InvoiceRequest>, prng_seed: [u8; 32], ) -> Result<(msgs::OnionPacket, u64, u32), APIError> { - 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( - &path, + create_payment_onion_internal( + secp_ctx, + path, + session_priv, total_msat, recipient_onion, cur_block_height, + payment_hash, keysend_preimage, invoice_request, - None + prng_seed, + None, + None, + ) +} + +/// 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( + secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, total_msat: u64, + 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]>, +) -> Result<(msgs::OnionPacket, u64, u32), APIError> { + let mut outer_total_msat = total_msat; + let mut outer_starting_htlc_offset = cur_block_height; + let mut outer_session_priv_override = None; + let mut trampoline_packet_option = None; + + if let Some(blinded_tail) = &path.blinded_tail { + if !blinded_tail.trampoline_hops.is_empty() { + let trampoline_payloads; + (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = + build_trampoline_onion_payloads( + &blinded_tail, + total_msat, + recipient_onion, + cur_block_height, + keysend_preimage, + )?; + + let onion_keys = + construct_trampoline_onion_keys(&secp_ctx, &blinded_tail, &session_priv).map_err( + |_| APIError::InvalidRoute { + err: "Pubkey along hop was maliciously selected".to_owned(), + }, + )?; + let trampoline_packet = construct_trampoline_onion_packet( + trampoline_payloads, + onion_keys, + prng_seed, + payment_hash, + // TODO: specify a fixed size for privacy in future spec upgrade + None, + ) + .map_err(|_| APIError::InvalidRoute { + err: "Route size too large considering onion data".to_owned(), + })?; + + trampoline_packet_option = Some(trampoline_packet); + + outer_session_priv_override = Some(secondary_session_priv.unwrap_or_else(|| { + let session_priv_hash = Sha256::hash(&session_priv.secret_bytes()).to_byte_array(); + SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!") + })); + } + } + + let (onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( + &path, + outer_total_msat, + recipient_onion, + outer_starting_htlc_offset, + keysend_preimage, + invoice_request, + trampoline_packet_option, )?; - 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(), - })?; + + let outer_session_priv = outer_session_priv_override.as_ref().unwrap_or(session_priv); + let onion_keys = construct_onion_keys(&secp_ctx, &path, outer_session_priv).map_err(|_| { + APIError::InvalidRoute { err: "Pubkey along hop was maliciously selected".to_owned() } + })?; + let outer_onion_prng_seed = secondary_prng_seed.unwrap_or(prng_seed); + let onion_packet = + construct_onion_packet(onion_payloads, onion_keys, outer_onion_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)) }