diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 7c205ff8c48..e4678a2322e 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -297,6 +297,26 @@ pub struct ForwardTlvs { pub next_blinding_override: Option, } +/// Data to construct a [`BlindedHop`] for forwarding a Trampoline payment. +#[cfg(trampoline)] +#[derive(Clone, Debug)] +pub struct TrampolineForwardTlvs { + /// The node id to which the trampoline node must find a route. + pub next_trampoline: PublicKey, + /// Payment parameters for relaying over [`Self::next_trampoline`]. + pub payment_relay: PaymentRelay, + /// Payment constraints for relaying over [`Self::next_trampoline`]. + pub payment_constraints: PaymentConstraints, + /// Supported and required features when relaying a payment onion containing this object's + /// corresponding [`BlindedHop::encrypted_payload`]. + /// + /// [`BlindedHop::encrypted_payload`]: crate::blinded_path::BlindedHop::encrypted_payload + pub features: BlindedHopFeatures, + /// Set if this [`BlindedPaymentPath`] is concatenated to another, to indicate the + /// [`BlindedPaymentPath::blinding_point`] of the appended blinded path. + pub next_blinding_override: Option, +} + /// Data to construct a [`BlindedHop`] for receiving a payment. This payload is custom to LDK and /// may not be valid if received by another lightning implementation. /// @@ -348,6 +368,17 @@ pub(crate) enum BlindedPaymentTlvs { Receive(ReceiveTlvs), } +/// Data to construct a [`BlindedHop`] for sending a Trampoline payment over. +/// +/// [`BlindedHop`]: crate::blinded_path::BlindedHop +#[cfg(trampoline)] +pub(crate) enum BlindedTrampolineTlvs { + /// This blinded payment data is for a forwarding node. + Forward(TrampolineForwardTlvs), + /// This blinded payment data is for the receiving node. + Receive(ReceiveTlvs), +} + // Used to include forward and receive TLVs in the same iterator for encoding. enum BlindedPaymentTlvsRef<'a> { Forward(&'a ForwardTlvs), @@ -560,6 +591,47 @@ impl Readable for BlindedPaymentTlvs { } } +#[cfg(trampoline)] +impl Readable for BlindedTrampolineTlvs { + fn read(r: &mut R) -> Result { + _init_and_read_tlv_stream!(r, { + (8, next_blinding_override, option), + (10, payment_relay, option), + (12, payment_constraints, required), + (14, next_trampoline, option), + (14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))), + (65536, payment_secret, option), + (65537, payment_context, option), + (65539, authentication, option), + }); + + if let Some(next_trampoline) = next_trampoline { + if payment_secret.is_some() { + return Err(DecodeError::InvalidValue); + } + Ok(BlindedTrampolineTlvs::Forward(TrampolineForwardTlvs { + next_trampoline, + payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?, + payment_constraints: payment_constraints.0.unwrap(), + next_blinding_override, + features: features.unwrap_or_else(BlindedHopFeatures::empty), + })) + } else { + if payment_relay.is_some() || features.is_some() { + return Err(DecodeError::InvalidValue); + } + Ok(BlindedTrampolineTlvs::Receive(ReceiveTlvs { + tlvs: UnauthenticatedReceiveTlvs { + payment_secret: payment_secret.ok_or(DecodeError::InvalidValue)?, + payment_constraints: payment_constraints.0.unwrap(), + payment_context: payment_context.ok_or(DecodeError::InvalidValue)?, + }, + authentication: authentication.ok_or(DecodeError::InvalidValue)?, + })) + } + } +} + /// Represents the padding round off size (in bytes) that /// is used to pad payment bilnded path's [`BlindedHop`] pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30; diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 24f0985b107..89b83ede3eb 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1814,3 +1814,147 @@ fn test_combined_trampoline_onion_creation_vectors() { assert_eq!(htlc_msat, 150_156_000); assert_eq!(htlc_cltv, 800_060); } + +#[test] +#[cfg(trampoline)] +fn test_trampoline_inbound_payment_decoding() { + let secp_ctx = Secp256k1::new(); + let session_priv = secret_from_hex("0303030303030303030303030303030303030303030303030303030303030303"); + + let bob_secret = secret_from_hex("4242424242424242424242424242424242424242424242424242424242424242"); + let bob_node_id = PublicKey::from_secret_key(&secp_ctx, &bob_secret); + let _bob_unblinded_tlvs = bytes_from_hex("011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456"); + let carol_secret = secret_from_hex("4343434343434343434343434343434343434343434343434343434343434343"); + let carol_node_id = PublicKey::from_secret_key(&secp_ctx, &carol_secret); + let _carol_unblinded_tlvs = bytes_from_hex("020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00"); + let dave_secret = secret_from_hex("4444444444444444444444444444444444444444444444444444444444444444"); + let dave_node_id = PublicKey::from_secret_key(&secp_ctx, &dave_secret); + let _dave_unblinded_tlvs = bytes_from_hex("01230000000000000000000000000000000000000000000000000000000000000000000000020800000000000002310a060090000000fa0c06000b699105dc0e00"); + let eve_secret = secret_from_hex("4545454545454545454545454545454545454545454545454545454545454545"); + let _eve_node_id = PublicKey::from_secret_key(&secp_ctx, &eve_secret); + let _eve_unblinded_tlvs = bytes_from_hex("011a00000000000000000000000000000000000000000000000000000604deadbeef0c06000b690105dc0e0f020000000000000000000000000000fdffff0206c1"); + + let path = Path { + hops: vec![ + // Bob + RouteHop { + pubkey: bob_node_id, + 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: carol_node_id, + node_features: NodeFeatures::empty(), + short_channel_id: (572330 << 40) + (42 << 16) + 2821, + channel_features: ChannelFeatures::empty(), + fee_msat: 150_153_000, + cltv_expiry_delta: 0, + maybe_announced_channel: false, + }, + ], + blinded_tail: Some(BlindedTail { + trampoline_hops: vec![ + // Carol's pubkey + TrampolineHop { + pubkey: carol_node_id, + node_features: Features::empty(), + fee_msat: 2_500, + cltv_expiry_delta: 24, + }, + // Dave's pubkey (the intro node needs to be duplicated) + TrampolineHop { + pubkey: dave_node_id, + 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 payment_secret = PaymentSecret(secret_from_hex("7494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da").secret_bytes()); + + let amt_msat = 150_000_001; + let cur_height = 800_001; + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, amt_msat, &recipient_onion_fields, cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); + + struct TestEcdhSigner { + node_secret: SecretKey, + } + impl NodeSigner for TestEcdhSigner { + fn ecdh( + &self, _recipient: Recipient, other_key: &PublicKey, tweak: Option<&Scalar>, + ) -> Result { + let mut node_secret = self.node_secret.clone(); + if let Some(tweak) = tweak { + node_secret = self.node_secret.mul_tweak(tweak).map_err(|_| ())?; + } + Ok(SharedSecret::new(other_key, &node_secret)) + } + fn get_inbound_payment_key(&self) -> ExpandedKey { unreachable!() } + fn get_node_id(&self, _recipient: Recipient) -> Result { unreachable!() } + fn sign_invoice( + &self, _invoice: &RawBolt11Invoice, _recipient: Recipient, + ) -> Result { unreachable!() } + fn sign_bolt12_invoice( + &self, _invoice: &UnsignedBolt12Invoice, + ) -> Result { unreachable!() } + fn sign_gossip_message(&self, _msg: UnsignedGossipMessage) -> Result { unreachable!() } + } + let logger = test_utils::TestLogger::with_id("".to_owned()); + + let bob_update_add = update_add_msg(111_000, 747_501, None, bob_onion); + let bob_node_signer = TestEcdhSigner { node_secret: bob_secret }; + + let (bob_peeled_onion, next_packet_details_opt) = onion_payment::decode_incoming_update_add_htlc_onion( + &bob_update_add, &bob_node_signer, &logger, &secp_ctx + ).unwrap_or_else(|_| panic!()); + + let (carol_packet_bytes, carol_hmac) = if let onion_utils::Hop::Forward { + next_hop_data: msgs::InboundOnionForwardPayload {..}, next_hop_hmac, new_packet_bytes, .. + } = bob_peeled_onion { + (new_packet_bytes, next_hop_hmac) + } else { panic!() }; + + let carol_packet_details = next_packet_details_opt.unwrap(); + let carol_onion = msgs::OnionPacket { + version: 0, + public_key: carol_packet_details.next_packet_pubkey, + hop_data: carol_packet_bytes, + hmac: carol_hmac, + }; + let carol_update_add = update_add_msg(carol_packet_details.outgoing_amt_msat, carol_packet_details.outgoing_cltv_value, None, carol_onion); + + let carol_node_signer = TestEcdhSigner { node_secret: carol_secret }; + let (carol_peeled_onion, _) = onion_payment::decode_incoming_update_add_htlc_onion( + &carol_update_add, &carol_node_signer, &logger, &secp_ctx + ).unwrap_or_else(|_| panic!()); + + let _carol_trampoline_update_add = if let onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, .. } = carol_peeled_onion { + assert_eq!(next_trampoline_hop_data.next_trampoline, dave_node_id); + } else { + panic!(); + }; +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 99374c7bb54..2caa79d908f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -54,10 +54,8 @@ use crate::ln::channel_state::ChannelDetails; use crate::types::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; #[cfg(any(feature = "_test_utils", test))] use crate::types::features::Bolt11InvoiceFeatures; -#[cfg(trampoline)] -use crate::routing::gossip::NodeId; use crate::routing::router::{BlindedTail, InFlightHtlcs, Path, Payee, PaymentParameters, RouteParameters, RouteParametersConfig, Router, FixedRouter, Route}; -use crate::ln::onion_payment::{check_incoming_htlc_cltv, create_recv_pending_htlc_info, create_fwd_pending_htlc_info, decode_incoming_update_add_htlc_onion, InboundHTLCErr, NextPacketDetails}; +use crate::ln::onion_payment::{check_incoming_htlc_cltv, create_recv_pending_htlc_info, create_fwd_pending_htlc_info, decode_incoming_update_add_htlc_onion, HopConnector, InboundHTLCErr, NextPacketDetails}; use crate::ln::msgs; use crate::ln::onion_utils; use crate::ln::onion_utils::{HTLCFailReason, INVALID_ONION_BLINDING}; @@ -181,7 +179,7 @@ pub enum PendingHTLCRouting { /// do with the HTLC. onion_packet: msgs::TrampolineOnionPacket, /// The node ID of the Trampoline node which we need to route this HTLC to. - node_id: NodeId, + node_id: PublicKey, /// Set if this HTLC is being forwarded within a blinded path. blinded: Option, /// The absolute CLTV of the inbound HTLC @@ -4303,11 +4301,15 @@ where // we don't allow forwards outbound over them. return Err(("Refusing to forward to a private channel based on our config.", 0x4000 | 10)); } - if chan.context.get_channel_type().supports_scid_privacy() && next_packet.outgoing_scid != chan.context.outbound_scid_alias() { - // `option_scid_alias` (referred to in LDK as `scid_privacy`) means - // "refuse to forward unless the SCID alias was used", so we pretend - // we don't have the channel here. - return Err(("Refusing to forward over real channel SCID as our counterparty requested.", 0x4000 | 10)); + if let HopConnector::ShortChannelId(outgoing_scid) = next_packet.outgoing_connector { + if chan.context.get_channel_type().supports_scid_privacy() && outgoing_scid != chan.context.outbound_scid_alias() { + // `option_scid_alias` (referred to in LDK as `scid_privacy`) means + // "refuse to forward unless the SCID alias was used", so we pretend + // we don't have the channel here. + return Err(("Refusing to forward over real channel SCID as our counterparty requested.", 0x4000 | 10)); + } + } else { + return Err(("Cannot forward by Node ID without SCID.", 0x4000 | 10)); } // Note that we could technically not return an error yet here and just hope @@ -4359,7 +4361,13 @@ where fn can_forward_htlc( &self, msg: &msgs::UpdateAddHTLC, next_packet_details: &NextPacketDetails ) -> Result<(), (&'static str, u16)> { - match self.do_funded_channel_callback(next_packet_details.outgoing_scid, |chan: &mut FundedChannel| { + let outgoing_scid = match next_packet_details.outgoing_connector { + HopConnector::ShortChannelId(scid) => scid, + HopConnector::Trampoline(_) => { + return Err(("Cannot forward by Node ID without SCID.", 0x4000 | 10)); + } + }; + match self.do_funded_channel_callback(outgoing_scid, |chan: &mut FundedChannel| { self.can_forward_htlc_to_outgoing_channel(chan, msg, next_packet_details) }) { Some(Ok(())) => {}, @@ -4368,8 +4376,8 @@ where // If we couldn't find the channel info for the scid, it may be a phantom or // intercept forward. if (self.default_configuration.accept_intercept_htlcs && - fake_scid::is_valid_intercept(&self.fake_scid_rand_bytes, next_packet_details.outgoing_scid, &self.chain_hash)) || - fake_scid::is_valid_phantom(&self.fake_scid_rand_bytes, next_packet_details.outgoing_scid, &self.chain_hash) + fake_scid::is_valid_intercept(&self.fake_scid_rand_bytes, outgoing_scid, &self.chain_hash)) || + fake_scid::is_valid_phantom(&self.fake_scid_rand_bytes, outgoing_scid, &self.chain_hash) {} else { return Err(("Don't have available channel for forwarding as requested.", 0x4000 | 10)); } @@ -4466,15 +4474,27 @@ where } match decoded_hop { onion_utils::Hop::Receive { .. } | onion_utils::Hop::BlindedReceive { .. } => { - let inbound_onion_payload = match decoded_hop { - onion_utils::Hop::Receive { hop_data, .. } => msgs::InboundOnionPayload::Receive(hop_data), - onion_utils::Hop::BlindedReceive { hop_data, .. } => msgs::InboundOnionPayload::BlindedReceive(hop_data), - _ => unreachable!() - }; - // OUR PAYMENT! let current_height: u32 = self.best_block.read().unwrap().height; - match create_recv_pending_htlc_info(inbound_onion_payload, shared_secret, msg.payment_hash, + match create_recv_pending_htlc_info(decoded_hop, shared_secret, msg.payment_hash, + msg.amount_msat, msg.cltv_expiry, None, allow_underpay, msg.skimmed_fee_msat, + current_height) + { + Ok(info) => { + // Note that we could obviously respond immediately with an update_fulfill_htlc + // message, however that would leak that we are the recipient of this payment, so + // instead we stay symmetric with the forwarding case, only responding (after a + // delay) once they've sent us a commitment_signed! + PendingHTLCStatus::Forward(info) + }, + Err(InboundHTLCErr { err_code, err_data, msg }) => return_err!(msg, err_code, &err_data) + } + }, + #[cfg(trampoline)] + onion_utils::Hop::TrampolineReceive { .. } | onion_utils::Hop::TrampolineBlindedReceive { .. } => { + // OUR PAYMENT! + let current_height: u32 = self.best_block.read().unwrap().height; + match create_recv_pending_htlc_info(decoded_hop, shared_secret, msg.payment_hash, msg.amount_msat, msg.cltv_expiry, None, allow_underpay, msg.skimmed_fee_msat, current_height) { @@ -4482,22 +4502,21 @@ where // Note that we could obviously respond immediately with an update_fulfill_htlc // message, however that would leak that we are the recipient of this payment, so // instead we stay symmetric with the forwarding case, only responding (after a - // delay) once they've send us a commitment_signed! + // delay) once they've sent us a commitment_signed! PendingHTLCStatus::Forward(info) }, Err(InboundHTLCErr { err_code, err_data, msg }) => return_err!(msg, err_code, &err_data) } }, - onion_utils::Hop::Forward { next_hop_data, next_hop_hmac, new_packet_bytes, .. } => { - match create_fwd_pending_htlc_info(msg, msgs::InboundOnionPayload::Forward(next_hop_data), next_hop_hmac, - new_packet_bytes, shared_secret, next_packet_pubkey_opt) { + onion_utils::Hop::Forward { .. } | onion_utils::Hop::BlindedForward { .. } => { + match create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt) { Ok(info) => PendingHTLCStatus::Forward(info), Err(InboundHTLCErr { err_code, err_data, msg }) => return_err!(msg, err_code, &err_data) } }, - onion_utils::Hop::BlindedForward { next_hop_data, next_hop_hmac, new_packet_bytes, .. } => { - match create_fwd_pending_htlc_info(msg, msgs::InboundOnionPayload::BlindedForward(next_hop_data), next_hop_hmac, - new_packet_bytes, shared_secret, next_packet_pubkey_opt) { + #[cfg(trampoline)] + onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => { + match create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt) { Ok(info) => PendingHTLCStatus::Forward(info), Err(InboundHTLCErr { err_code, err_data, msg }) => return_err!(msg, err_code, &err_data) } @@ -5734,7 +5753,12 @@ where }; let is_intro_node_blinded_forward = next_hop.is_intro_node_blinded_forward(); - let outgoing_scid_opt = next_packet_details_opt.as_ref().map(|d| d.outgoing_scid); + let outgoing_scid_opt = next_packet_details_opt.as_ref().and_then(|d| { + match d.outgoing_connector { + HopConnector::ShortChannelId(scid) => { Some(scid) } + HopConnector::Trampoline(_) => { None } + } + }); let shared_secret = next_hop.shared_secret().secret_bytes(); // Process the HTLC on the incoming channel. @@ -5907,19 +5931,14 @@ where // of the onion. failed_payment!(err_msg, err_code, sha256_of_onion.to_vec(), None); }, - Err(onion_utils::OnionDecodeErr::Relay { err_msg, err_code, shared_secret }) => { + Err(onion_utils::OnionDecodeErr::Relay { err_msg, err_code, shared_secret, .. }) => { let phantom_shared_secret = shared_secret.secret_bytes(); failed_payment!(err_msg, err_code, Vec::new(), Some(phantom_shared_secret)); }, }; - let (inbound_onion_payload, shared_secret) = match next_hop { - onion_utils::Hop::Receive { hop_data, shared_secret } => (msgs::InboundOnionPayload::Receive(hop_data), shared_secret), - onion_utils::Hop::BlindedReceive { hop_data, shared_secret } => (msgs::InboundOnionPayload::BlindedReceive(hop_data), shared_secret), - _ => panic!() - }; - let phantom_shared_secret = shared_secret.secret_bytes(); + let phantom_shared_secret = next_hop.shared_secret().secret_bytes(); let current_height: u32 = self.best_block.read().unwrap().height; - match create_recv_pending_htlc_info(inbound_onion_payload, + match create_recv_pending_htlc_info(next_hop, incoming_shared_secret, payment_hash, outgoing_amt_msat, outgoing_cltv_value, Some(phantom_shared_secret), false, None, current_height) @@ -14847,6 +14866,7 @@ where mod tests { use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use bitcoin::secp256k1::ecdh::SharedSecret; use core::sync::atomic::Ordering; use crate::events::{Event, HTLCDestination, ClosureReason}; use crate::ln::types::ChannelId; @@ -14854,6 +14874,7 @@ mod tests { use crate::ln::channelmanager::{create_recv_pending_htlc_info, inbound_payment, ChannelConfigOverrides, HTLCForwardInfo, InterceptId, PaymentId, RecipientOnionFields}; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, AcceptChannel, ErrorAction, MessageSendEvent}; + use crate::ln::onion_utils; use crate::ln::outbound_payment::Retry; use crate::prelude::*; use crate::routing::router::{PaymentParameters, RouteParameters, find_route}; @@ -15070,8 +15091,8 @@ mod tests { let nodes = create_network(2, &node_cfgs, &node_chanmgrs); create_announced_chan_between_nodes(&nodes, 0, 1); - - // Since we do not send peer storage, we manually simulate receiving a dummy + + // Since we do not send peer storage, we manually simulate receiving a dummy // `PeerStorage` from the channel partner. nodes[0].node.handle_peer_storage(nodes[1].node.get_our_node_id(), msgs::PeerStorage{data: vec![0; 100]}); @@ -15944,17 +15965,20 @@ mod tests { let node = create_network(1, &node_cfg, &node_chanmgr); let sender_intended_amt_msat = 100; let extra_fee_msat = 10; - let hop_data = msgs::InboundOnionPayload::Receive(msgs::InboundOnionReceivePayload { - sender_intended_htlc_amt_msat: 100, - cltv_expiry_height: 42, - payment_metadata: None, - keysend_preimage: None, - payment_data: Some(msgs::FinalOnionHopData { - payment_secret: PaymentSecret([0; 32]), - total_msat: sender_intended_amt_msat, - }), - custom_tlvs: Vec::new(), - }); + let hop_data = onion_utils::Hop::Receive { + hop_data: msgs::InboundOnionReceivePayload { + sender_intended_htlc_amt_msat: 100, + cltv_expiry_height: 42, + payment_metadata: None, + keysend_preimage: None, + payment_data: Some(msgs::FinalOnionHopData { + payment_secret: PaymentSecret([0; 32]), + total_msat: sender_intended_amt_msat, + }), + custom_tlvs: Vec::new(), + }, + shared_secret: SharedSecret::from_bytes([0; 32]), + }; // Check that if the amount we received + the penultimate hop extra fee is less than the sender // intended amount, we fail the payment. let current_height: u32 = node[0].node.best_block.read().unwrap().height; @@ -15967,17 +15991,20 @@ mod tests { } else { panic!(); } // If amt_received + extra_fee is equal to the sender intended amount, we're fine. - let hop_data = msgs::InboundOnionPayload::Receive(msgs::InboundOnionReceivePayload { // This is the same payload as above, InboundOnionPayload doesn't implement Clone - sender_intended_htlc_amt_msat: 100, - cltv_expiry_height: 42, - payment_metadata: None, - keysend_preimage: None, - payment_data: Some(msgs::FinalOnionHopData { - payment_secret: PaymentSecret([0; 32]), - total_msat: sender_intended_amt_msat, - }), - custom_tlvs: Vec::new(), - }); + let hop_data = onion_utils::Hop::Receive { + hop_data: msgs::InboundOnionReceivePayload { // This is the same payload as above, InboundOnionPayload doesn't implement Clone + sender_intended_htlc_amt_msat: 100, + cltv_expiry_height: 42, + payment_metadata: None, + keysend_preimage: None, + payment_data: Some(msgs::FinalOnionHopData { + payment_secret: PaymentSecret([0; 32]), + total_msat: sender_intended_amt_msat, + }), + custom_tlvs: Vec::new(), + }, + shared_secret: SharedSecret::from_bytes([0; 32]), + }; let current_height: u32 = node[0].node.best_block.read().unwrap().height; assert!(create_recv_pending_htlc_info(hop_data, [0; 32], PaymentHash([0; 32]), sender_intended_amt_msat - extra_fee_msat, 42, None, true, Some(extra_fee_msat), @@ -15992,17 +16019,20 @@ mod tests { let node = create_network(1, &node_cfg, &node_chanmgr); let current_height: u32 = node[0].node.best_block.read().unwrap().height; - let result = create_recv_pending_htlc_info(msgs::InboundOnionPayload::Receive(msgs::InboundOnionReceivePayload { - sender_intended_htlc_amt_msat: 100, - cltv_expiry_height: TEST_FINAL_CLTV, - payment_metadata: None, - keysend_preimage: None, - payment_data: Some(msgs::FinalOnionHopData { - payment_secret: PaymentSecret([0; 32]), - total_msat: 100, - }), - custom_tlvs: Vec::new(), - }), [0; 32], PaymentHash([0; 32]), 100, TEST_FINAL_CLTV + 1, None, true, None, current_height); + let result = create_recv_pending_htlc_info(onion_utils::Hop::Receive { + hop_data: msgs::InboundOnionReceivePayload { + sender_intended_htlc_amt_msat: 100, + cltv_expiry_height: TEST_FINAL_CLTV, + payment_metadata: None, + keysend_preimage: None, + payment_data: Some(msgs::FinalOnionHopData { + payment_secret: PaymentSecret([0; 32]), + total_msat: 100, + }), + custom_tlvs: Vec::new(), + }, + shared_secret: SharedSecret::from_bytes([0; 32]), + }, [0; 32], PaymentHash([0; 32]), 100, TEST_FINAL_CLTV + 1, None, true, None, current_height); // Should not return an error as this condition: // https://github.com/lightning/bolts/blob/4dcc377209509b13cf89a4b91fde7d478f5b46d8/04-onion-routing.md?plain=1#L334 diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 81505a92cc7..64181ffa929 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -32,6 +32,8 @@ use bitcoin::script::ScriptBuf; use bitcoin::hash_types::Txid; use crate::blinded_path::payment::{BlindedPaymentTlvs, ForwardTlvs, ReceiveTlvs, UnauthenticatedReceiveTlvs}; +#[cfg(trampoline)] +use crate::blinded_path::payment::{BlindedTrampolineTlvs, TrampolineForwardTlvs}; use crate::ln::channelmanager::Verification; use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret}; @@ -728,7 +730,7 @@ pub struct UpdateFulfillHTLC { /// A [`peer_storage`] message that can be sent to or received from a peer. /// /// This message is used to distribute backup data to peers. -/// If data is lost or corrupted, users can retrieve it through [`PeerStorageRetrieval`] +/// If data is lost or corrupted, users can retrieve it through [`PeerStorageRetrieval`] /// to recover critical information, such as channel states, for fund recovery. /// /// [`peer_storage`] is used to send our own encrypted backup data to a peer. @@ -743,7 +745,7 @@ pub struct PeerStorage { /// A [`peer_storage_retrieval`] message that can be sent to or received from a peer. /// /// This message is sent to peers for whom we store backup data. -/// If we receive this message, it indicates that the peer had stored our backup data. +/// If we receive this message, it indicates that the peer had stored our backup data. /// This data can be used for fund recovery in case of data loss. /// /// [`peer_storage_retrieval`] is used to send the most recent backup of the peer. @@ -2073,6 +2075,18 @@ mod fuzzy_internal_msgs { pub outgoing_cltv_value: u32, } + #[cfg(trampoline)] + #[cfg_attr(trampoline, allow(unused))] + pub struct InboundTrampolineEntrypointPayload { + pub amt_to_forward: u64, + pub outgoing_cltv_value: u32, + pub multipath_trampoline_data: FinalOnionHopData, + pub trampoline_packet: TrampolineOnionPacket, + /// The blinding point this hop needs to decrypt its Trampoline onion. + /// This is used for Trampoline hops that are not the blinded path intro hop. + pub current_path_key: Option + } + pub struct InboundOnionReceivePayload { pub payment_data: Option, pub payment_metadata: Option>, @@ -2104,11 +2118,40 @@ mod fuzzy_internal_msgs { pub enum InboundOnionPayload { Forward(InboundOnionForwardPayload), + #[cfg(trampoline)] + #[cfg_attr(trampoline, allow(unused))] + TrampolineEntrypoint(InboundTrampolineEntrypointPayload), Receive(InboundOnionReceivePayload), BlindedForward(InboundOnionBlindedForwardPayload), BlindedReceive(InboundOnionBlindedReceivePayload), } + #[cfg(trampoline)] + pub struct InboundTrampolineForwardPayload { + pub next_trampoline: PublicKey, + /// The value, in msat, of the payment after this hop's fee is deducted. + pub amt_to_forward: u64, + pub outgoing_cltv_value: u32, + } + + #[cfg(trampoline)] + pub struct InboundTrampolineBlindedForwardPayload { + pub next_trampoline: PublicKey, + pub payment_relay: PaymentRelay, + pub payment_constraints: PaymentConstraints, + pub features: BlindedHopFeatures, + pub intro_node_blinding_point: Option, + pub next_blinding_override: Option, + } + + #[cfg(trampoline)] + pub enum InboundTrampolinePayload { + Forward(InboundTrampolineForwardPayload), + BlindedForward(InboundTrampolineBlindedForwardPayload), + Receive(InboundOnionReceivePayload), + BlindedReceive(InboundOnionBlindedReceivePayload), + } + pub(crate) enum OutboundOnionPayload<'a> { Forward { short_channel_id: u64, @@ -2122,6 +2165,18 @@ mod fuzzy_internal_msgs { multipath_trampoline_data: Option, trampoline_packet: TrampolineOnionPacket, }, + /// This is used for Trampoline hops that are not the blinded path intro hop. + /// We would only ever construct this variant when we are a Trampoline node forwarding a + /// payment along a blinded path. + #[allow(unused)] + BlindedTrampolineEntrypoint { + amt_to_forward: u64, + outgoing_cltv_value: u32, + multipath_trampoline_data: Option, + trampoline_packet: TrampolineOnionPacket, + /// The blinding point this hop needs to use for its Trampoline onion. + current_path_key: PublicKey + }, Receive { payment_data: Option, payment_metadata: Option<&'a Vec>, @@ -3056,6 +3111,18 @@ impl<'a> Writeable for OutboundOnionPayload<'a> { (20, trampoline_packet, required) }); }, + Self::BlindedTrampolineEntrypoint { + amt_to_forward, outgoing_cltv_value, current_path_key, + ref multipath_trampoline_data, ref trampoline_packet + } => { + _encode_varint_length_prefixed_tlv!(w, { + (2, HighZeroBytesDroppedBigSize(*amt_to_forward), required), + (4, HighZeroBytesDroppedBigSize(*outgoing_cltv_value), required), + (8, multipath_trampoline_data, option), + (12, current_path_key, required), + (20, trampoline_packet, required) + }); + }, Self::Receive { ref payment_data, ref payment_metadata, ref keysend_preimage, sender_intended_htlc_amt_msat, cltv_expiry_height, ref custom_tlvs, @@ -3170,11 +3237,36 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh let mut payment_metadata: Option>> = None; let mut total_msat = None; let mut keysend_preimage: Option = None; + #[cfg(trampoline)] + let mut trampoline_onion_packet: Option = None; let mut invoice_request: Option = None; let mut custom_tlvs = Vec::new(); let tlv_len = BigSize::read(r)?; let mut rd = FixedLengthReader::new(r, tlv_len.0); + + #[cfg(trampoline)] + decode_tlv_stream_with_custom_tlv_decode!(&mut rd, { + (2, amt, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), + (4, cltv_value, (option, encoding: (u32, HighZeroBytesDroppedBigSize))), + (6, short_id, option), + (8, payment_data, option), + (10, encrypted_tlvs_opt, option), + (12, intro_node_blinding_point, option), + (16, payment_metadata, option), + (18, total_msat, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), + (20, trampoline_onion_packet, option), + (77_777, invoice_request, option), + // See https://github.com/lightning/blips/blob/master/blip-0003.md + (5482373484, keysend_preimage, option) + }, |msg_type: u64, msg_reader: &mut FixedLengthReader<_>| -> Result { + if msg_type < 1 << 16 { return Ok(false) } + let mut value = Vec::new(); + msg_reader.read_to_limit(&mut value, u64::MAX)?; + custom_tlvs.push((msg_type, value)); + Ok(true) + }); + #[cfg(not(trampoline))] decode_tlv_stream_with_custom_tlv_decode!(&mut rd, { (2, amt, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), (4, cltv_value, (option, encoding: (u32, HighZeroBytesDroppedBigSize))), @@ -3200,6 +3292,20 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh return Err(DecodeError::InvalidValue) } + #[cfg(trampoline)] + if let Some(trampoline_onion_packet) = trampoline_onion_packet { + if payment_metadata.is_some() || encrypted_tlvs_opt.is_some() || + total_msat.is_some() + { return Err(DecodeError::InvalidValue) } + return Ok(Self::TrampolineEntrypoint(InboundTrampolineEntrypointPayload { + amt_to_forward: amt.ok_or(DecodeError::InvalidValue)?, + outgoing_cltv_value: cltv_value.ok_or(DecodeError::InvalidValue)?, + multipath_trampoline_data: payment_data.ok_or(DecodeError::InvalidValue)?, + trampoline_packet: trampoline_onion_packet, + current_path_key: intro_node_blinding_point, + })) + } + if let Some(blinding_point) = intro_node_blinding_point.or(update_add_blinding_point) { if short_id.is_some() || payment_data.is_some() || payment_metadata.is_some() { return Err(DecodeError::InvalidValue) @@ -3283,6 +3389,134 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh } } +#[cfg(trampoline)] +impl ReadableArgs<(Option, NS)> for InboundTrampolinePayload where NS::Target: NodeSigner { + fn read(r: &mut R, args: (Option, NS)) -> Result { + let (update_add_blinding_point, node_signer) = args; + + let mut amt = None; + let mut cltv_value = None; + let mut payment_data: Option = None; + let mut encrypted_tlvs_opt: Option>> = None; + let mut intro_node_blinding_point = None; + let mut next_trampoline: Option = None; + let mut payment_metadata: Option>> = None; + let mut total_msat = None; + let mut keysend_preimage: Option = None; + let mut invoice_request: Option = None; + let mut custom_tlvs = Vec::new(); + + let tlv_len = BigSize::read(r)?; + let mut rd = FixedLengthReader::new(r, tlv_len.0); + decode_tlv_stream_with_custom_tlv_decode!(&mut rd, { + (2, amt, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), + (4, cltv_value, (option, encoding: (u32, HighZeroBytesDroppedBigSize))), + (8, payment_data, option), + (10, encrypted_tlvs_opt, option), + (12, intro_node_blinding_point, option), + (14, next_trampoline, option), + (16, payment_metadata, option), + (18, total_msat, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), + (77_777, invoice_request, option), + // See https://github.com/lightning/blips/blob/master/blip-0003.md + (5482373484, keysend_preimage, option) + }, |msg_type: u64, msg_reader: &mut FixedLengthReader<_>| -> Result { + if msg_type < 1 << 16 { return Ok(false) } + let mut value = Vec::new(); + msg_reader.read_to_limit(&mut value, u64::MAX)?; + custom_tlvs.push((msg_type, value)); + Ok(true) + }); + + if amt.unwrap_or(0) > MAX_VALUE_MSAT { return Err(DecodeError::InvalidValue) } + if intro_node_blinding_point.is_some() && update_add_blinding_point.is_some() { + return Err(DecodeError::InvalidValue) + } + + if let Some(blinding_point) = intro_node_blinding_point.or(update_add_blinding_point) { + if next_trampoline.is_some() || payment_data.is_some() || payment_metadata.is_some() { + return Err(DecodeError::InvalidValue) + } + let enc_tlvs = encrypted_tlvs_opt.ok_or(DecodeError::InvalidValue)?.0; + let enc_tlvs_ss = node_signer.ecdh(Recipient::Node, &blinding_point, None) + .map_err(|_| DecodeError::InvalidValue)?; + let rho = onion_utils::gen_rho_from_shared_secret(&enc_tlvs_ss.secret_bytes()); + let mut s = Cursor::new(&enc_tlvs); + let mut reader = FixedLengthReader::new(&mut s, enc_tlvs.len() as u64); + match ChaChaPolyReadAdapter::read(&mut reader, rho)? { + ChaChaPolyReadAdapter { readable: BlindedTrampolineTlvs::Forward(TrampolineForwardTlvs { + next_trampoline, payment_relay, payment_constraints, features, next_blinding_override + })} => { + if amt.is_some() || cltv_value.is_some() || total_msat.is_some() || + keysend_preimage.is_some() || invoice_request.is_some() + { + return Err(DecodeError::InvalidValue) + } + Ok(Self::BlindedForward(InboundTrampolineBlindedForwardPayload { + next_trampoline, + payment_relay, + payment_constraints, + features, + intro_node_blinding_point, + next_blinding_override, + })) + }, + ChaChaPolyReadAdapter { readable: BlindedTrampolineTlvs::Receive(receive_tlvs) } => { + let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs; + let expanded_key = node_signer.get_inbound_payment_key(); + if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() { + return Err(DecodeError::InvalidValue); + } + + let UnauthenticatedReceiveTlvs { + payment_secret, payment_constraints, payment_context, + } = tlvs; + if total_msat.unwrap_or(0) > MAX_VALUE_MSAT { return Err(DecodeError::InvalidValue) } + Ok(Self::BlindedReceive(InboundOnionBlindedReceivePayload { + sender_intended_htlc_amt_msat: amt.ok_or(DecodeError::InvalidValue)?, + total_msat: total_msat.ok_or(DecodeError::InvalidValue)?, + cltv_expiry_height: cltv_value.ok_or(DecodeError::InvalidValue)?, + payment_secret, + payment_constraints, + payment_context, + intro_node_blinding_point, + keysend_preimage, + invoice_request, + custom_tlvs, + })) + }, + } + } else if let Some(next_trampoline) = next_trampoline { + if payment_data.is_some() || payment_metadata.is_some() || encrypted_tlvs_opt.is_some() || + total_msat.is_some() || invoice_request.is_some() + { return Err(DecodeError::InvalidValue) } + Ok(Self::Forward(InboundTrampolineForwardPayload { + next_trampoline, + amt_to_forward: amt.ok_or(DecodeError::InvalidValue)?, + outgoing_cltv_value: cltv_value.ok_or(DecodeError::InvalidValue)?, + })) + } else { + if encrypted_tlvs_opt.is_some() || total_msat.is_some() || invoice_request.is_some() { + return Err(DecodeError::InvalidValue) + } + if let Some(data) = &payment_data { + if data.total_msat > MAX_VALUE_MSAT { + return Err(DecodeError::InvalidValue); + } + } + Ok(Self::Receive(InboundOnionReceivePayload { + payment_data, + payment_metadata: payment_metadata.map(|w| w.0), + keysend_preimage, + sender_intended_htlc_amt_msat: amt.ok_or(DecodeError::InvalidValue)?, + cltv_expiry_height: cltv_value.ok_or(DecodeError::InvalidValue)?, + custom_tlvs, + })) + } + } +} + + impl Writeable for Ping { fn write(&self, w: &mut W) -> Result<(), io::Error> { self.ponglen.write(w)?; diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index cd2ef491846..c3594a9b0d1 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -7,6 +7,9 @@ use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; +#[cfg(trampoline)] +use bitcoin::secp256k1::ecdh::SharedSecret; + use crate::blinded_path; use crate::blinded_path::payment::{PaymentConstraints, PaymentRelay}; use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS}; @@ -15,7 +18,7 @@ use crate::ln::channelmanager::{BlindedFailure, BlindedForward, CLTV_FAR_FAR_AWA use crate::types::features::BlindedHopFeatures; use crate::ln::msgs; use crate::ln::onion_utils; -use crate::ln::onion_utils::{Hop, HTLCFailReason, INVALID_ONION_BLINDING}; +use crate::ln::onion_utils::{HTLCFailReason, INVALID_ONION_BLINDING, ONION_DATA_LEN}; use crate::sign::{NodeSigner, Recipient}; use crate::util::logger::Logger; @@ -60,29 +63,41 @@ fn check_blinded_forward( Ok((amt_to_forward, outgoing_cltv_value)) } +enum RoutingInfo { + Direct { + short_channel_id: u64, + new_packet_bytes: [u8; ONION_DATA_LEN], + next_hop_hmac: [u8; 32] + }, + #[cfg(trampoline)] + Trampoline { + next_trampoline: PublicKey, + // Trampoline onions are currently variable length + new_packet_bytes: Vec, + next_hop_hmac: [u8; 32], + shared_secret: SharedSecret, + current_path_key: Option + } +} + pub(super) fn create_fwd_pending_htlc_info( - msg: &msgs::UpdateAddHTLC, hop_data: msgs::InboundOnionPayload, hop_hmac: [u8; 32], - new_packet_bytes: [u8; onion_utils::ONION_DATA_LEN], shared_secret: [u8; 32], + msg: &msgs::UpdateAddHTLC, hop_data: onion_utils::Hop, shared_secret: [u8; 32], next_packet_pubkey_opt: Option> ) -> Result { debug_assert!(next_packet_pubkey_opt.is_some()); - let outgoing_packet = msgs::OnionPacket { - version: 0, - public_key: next_packet_pubkey_opt.unwrap_or(Err(secp256k1::Error::InvalidPublicKey)), - hop_data: new_packet_bytes, - hmac: hop_hmac, - }; let ( - short_channel_id, amt_to_forward, outgoing_cltv_value, intro_node_blinding_point, + routing_info, amt_to_forward, outgoing_cltv_value, intro_node_blinding_point, next_blinding_override ) = match hop_data { - msgs::InboundOnionPayload::Forward(msgs::InboundOnionForwardPayload { short_channel_id, amt_to_forward, outgoing_cltv_value }) => - (short_channel_id, amt_to_forward, outgoing_cltv_value, None, None), - msgs::InboundOnionPayload::BlindedForward(msgs::InboundOnionBlindedForwardPayload { + onion_utils::Hop::Forward { next_hop_data: msgs::InboundOnionForwardPayload { + short_channel_id, amt_to_forward, outgoing_cltv_value + }, new_packet_bytes, next_hop_hmac, .. } => + (RoutingInfo::Direct { short_channel_id, new_packet_bytes, next_hop_hmac }, amt_to_forward, outgoing_cltv_value, None, None), + onion_utils::Hop::BlindedForward { next_hop_data: msgs::InboundOnionBlindedForwardPayload { short_channel_id, payment_relay, payment_constraints, intro_node_blinding_point, features, next_blinding_override, - }) => { + }, new_packet_bytes, next_hop_hmac, .. } => { let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward( msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features ).map_err(|()| { @@ -94,31 +109,124 @@ pub(super) fn create_fwd_pending_htlc_info( err_data: vec![0; 32], } })?; - (short_channel_id, amt_to_forward, outgoing_cltv_value, intro_node_blinding_point, - next_blinding_override) + (RoutingInfo::Direct { short_channel_id, new_packet_bytes, next_hop_hmac }, amt_to_forward, outgoing_cltv_value, intro_node_blinding_point, + next_blinding_override) }, - msgs::InboundOnionPayload::Receive { .. } | msgs::InboundOnionPayload::BlindedReceive { .. } => + onion_utils::Hop::Receive { .. } | onion_utils::Hop::BlindedReceive { .. } => return Err(InboundHTLCErr { msg: "Final Node OnionHopData provided for us as an intermediary node", err_code: 0x4000 | 22, err_data: Vec::new(), }), + #[cfg(trampoline)] + onion_utils::Hop::TrampolineReceive { .. } | onion_utils::Hop::TrampolineBlindedReceive { .. } => + return Err(InboundHTLCErr { + msg: "Final Node OnionHopData provided for us as an intermediary node", + err_code: 0x4000 | 22, + err_data: Vec::new(), + }), + #[cfg(trampoline)] + onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + ( + RoutingInfo::Trampoline { + next_trampoline: next_trampoline_hop_data.next_trampoline, + new_packet_bytes: new_trampoline_packet_bytes, + next_hop_hmac: next_trampoline_hop_hmac, + shared_secret: trampoline_shared_secret, + current_path_key: None + }, + next_trampoline_hop_data.amt_to_forward, + next_trampoline_hop_data.outgoing_cltv_value, + None, + None + ) + }, + #[cfg(trampoline)] + onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward( + msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features + ).map_err(|()| { + // We should be returning malformed here if `msg.blinding_point` is set, but this is + // unreachable right now since we checked it in `decode_update_add_htlc_onion`. + InboundHTLCErr { + msg: "Underflow calculating outbound amount or cltv value for blinded forward", + err_code: INVALID_ONION_BLINDING, + err_data: vec![0; 32], + } + })?; + ( + RoutingInfo::Trampoline { + next_trampoline: next_trampoline_hop_data.next_trampoline, + new_packet_bytes: new_trampoline_packet_bytes, + next_hop_hmac: next_trampoline_hop_hmac, + shared_secret: trampoline_shared_secret, + current_path_key: outer_hop_data.current_path_key + }, + amt_to_forward, + outgoing_cltv_value, + next_trampoline_hop_data.intro_node_blinding_point, + next_trampoline_hop_data.next_blinding_override + ) + }, }; - Ok(PendingHTLCInfo { - routing: PendingHTLCRouting::Forward { - onion_packet: outgoing_packet, - short_channel_id, - incoming_cltv_expiry: Some(msg.cltv_expiry), - blinded: intro_node_blinding_point.or(msg.blinding_point) - .map(|bp| BlindedForward { - inbound_blinding_point: bp, - next_blinding_override, - failure: intro_node_blinding_point - .map(|_| BlindedFailure::FromIntroductionNode) - .unwrap_or(BlindedFailure::FromBlindedNode), + let routing = match routing_info { + RoutingInfo::Direct { short_channel_id, new_packet_bytes, next_hop_hmac } => { + let outgoing_packet = msgs::OnionPacket { + version: 0, + public_key: next_packet_pubkey_opt.unwrap_or(Err(secp256k1::Error::InvalidPublicKey)), + hop_data: new_packet_bytes, + hmac: next_hop_hmac, + }; + PendingHTLCRouting::Forward { + onion_packet: outgoing_packet, + short_channel_id, + incoming_cltv_expiry: Some(msg.cltv_expiry), + blinded: intro_node_blinding_point.or(msg.blinding_point) + .map(|bp| BlindedForward { + inbound_blinding_point: bp, + next_blinding_override, + failure: intro_node_blinding_point + .map(|_| BlindedFailure::FromIntroductionNode) + .unwrap_or(BlindedFailure::FromBlindedNode), + }), + } + } + #[cfg(trampoline)] + RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key } => { + let next_trampoline_packet_pubkey = match next_packet_pubkey_opt { + Some(Ok(pubkey)) => pubkey, + _ => return Err(InboundHTLCErr { + msg: "Missing next Trampoline hop pubkey from intermediate Trampoline forwarding data", + err_code: 0x4000 | 22, + err_data: Vec::new(), }), - }, + }; + let outgoing_packet = msgs::TrampolineOnionPacket { + version: 0, + public_key: next_trampoline_packet_pubkey, + hop_data: new_packet_bytes, + hmac: next_hop_hmac, + }; + PendingHTLCRouting::TrampolineForward { + incoming_shared_secret: shared_secret.secret_bytes(), + onion_packet: outgoing_packet, + node_id: next_trampoline, + incoming_cltv_expiry: msg.cltv_expiry, + blinded: intro_node_blinding_point.or(current_path_key) + .map(|bp| BlindedForward { + inbound_blinding_point: bp, + next_blinding_override, + failure: intro_node_blinding_point + .map(|_| BlindedFailure::FromIntroductionNode) + .unwrap_or(BlindedFailure::FromBlindedNode), + }) + } + } + }; + + Ok(PendingHTLCInfo { + routing, payment_hash: msg.payment_hash, incoming_shared_secret: shared_secret, incoming_amt_msat: Some(msg.amount_msat), @@ -129,7 +237,7 @@ pub(super) fn create_fwd_pending_htlc_info( } pub(super) fn create_recv_pending_htlc_info( - hop_data: msgs::InboundOnionPayload, shared_secret: [u8; 32], payment_hash: PaymentHash, + hop_data: onion_utils::Hop, shared_secret: [u8; 32], payment_hash: PaymentHash, amt_msat: u64, cltv_expiry: u32, phantom_shared_secret: Option<[u8; 32]>, allow_underpay: bool, counterparty_skimmed_fee_msat: Option, current_height: u32 ) -> Result { @@ -138,17 +246,17 @@ pub(super) fn create_recv_pending_htlc_info( payment_metadata, payment_context, requires_blinded_error, has_recipient_created_payment_secret, invoice_request ) = match hop_data { - msgs::InboundOnionPayload::Receive(msgs::InboundOnionReceivePayload { + onion_utils::Hop::Receive { hop_data: msgs::InboundOnionReceivePayload { payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, cltv_expiry_height, payment_metadata, .. - }) => + }, .. } => (payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None), - msgs::InboundOnionPayload::BlindedReceive(msgs::InboundOnionBlindedReceivePayload { + onion_utils::Hop::BlindedReceive { hop_data: msgs::InboundOnionBlindedReceivePayload { sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, payment_secret, intro_node_blinding_point, payment_constraints, payment_context, keysend_preimage, custom_tlvs, invoice_request - }) => { + }, .. } => { check_blinded_payment_constraints( sender_intended_htlc_amt_msat, cltv_expiry, &payment_constraints ) @@ -164,20 +272,30 @@ pub(super) fn create_recv_pending_htlc_info( sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context), intro_node_blinding_point.is_none(), true, invoice_request) } - msgs::InboundOnionPayload::Forward { .. } => { + #[cfg(trampoline)] + onion_utils::Hop::TrampolineReceive { .. } | onion_utils::Hop::TrampolineBlindedReceive { .. } => todo!(), + onion_utils::Hop::Forward { .. } => { return Err(InboundHTLCErr { err_code: 0x4000|22, err_data: Vec::new(), msg: "Got non final data with an HMAC of 0", }) }, - msgs::InboundOnionPayload::BlindedForward { .. } => { + onion_utils::Hop::BlindedForward { .. } => { return Err(InboundHTLCErr { err_code: INVALID_ONION_BLINDING, err_data: vec![0; 32], msg: "Got blinded non final data with an HMAC of 0", }) - } + }, + #[cfg(trampoline)] + onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => { + return Err(InboundHTLCErr { + err_code: 0x4000|22, + err_data: Vec::new(), + msg: "Got Trampoline non final data with an HMAC of 0", + }) + }, }; // final_incorrect_cltv_expiry if onion_cltv_expiry > cltv_expiry { @@ -295,16 +413,10 @@ where InboundHTLCErr { msg, err_code, err_data } })?; Ok(match hop { - onion_utils::Hop::Forward { shared_secret, next_hop_hmac, new_packet_bytes, .. } | - onion_utils::Hop::BlindedForward { shared_secret, next_hop_hmac, new_packet_bytes, .. } => { - let inbound_onion_payload = match hop { - onion_utils::Hop::Forward { next_hop_data, .. } => msgs::InboundOnionPayload::Forward(next_hop_data), - onion_utils::Hop::BlindedForward { next_hop_data, .. } => msgs::InboundOnionPayload::BlindedForward(next_hop_data), - _ => unreachable!() - }; - + onion_utils::Hop::Forward { shared_secret, .. } | + onion_utils::Hop::BlindedForward { shared_secret, .. } => { let NextPacketDetails { - next_packet_pubkey, outgoing_amt_msat: _, outgoing_scid: _, outgoing_cltv_value + next_packet_pubkey, outgoing_amt_msat: _, outgoing_connector: _, outgoing_cltv_value } = match next_packet_details_opt { Some(next_packet_details) => next_packet_details, // Forward should always include the next hop details @@ -327,29 +439,29 @@ where // TODO: If this is potentially a phantom payment we should decode the phantom payment // onion here and check it. - create_fwd_pending_htlc_info( - msg, inbound_onion_payload, next_hop_hmac, new_packet_bytes, shared_secret.secret_bytes(), - Some(next_packet_pubkey), - )? - }, - onion_utils::Hop::Receive { hop_data, shared_secret } => { - create_recv_pending_htlc_info( - msgs::InboundOnionPayload::Receive(hop_data), shared_secret.secret_bytes(), msg.payment_hash, msg.amount_msat, msg.cltv_expiry, - None, allow_skimmed_fees, msg.skimmed_fee_msat, cur_height, - )? + create_fwd_pending_htlc_info(msg, hop, shared_secret.secret_bytes(), Some(next_packet_pubkey))? }, - onion_utils::Hop::BlindedReceive { hop_data, shared_secret } => { + _ => { + let shared_secret = hop.shared_secret().secret_bytes(); create_recv_pending_htlc_info( - msgs::InboundOnionPayload::BlindedReceive(hop_data), shared_secret.secret_bytes(), msg.payment_hash, msg.amount_msat, msg.cltv_expiry, + hop, shared_secret, msg.payment_hash, msg.amount_msat, msg.cltv_expiry, None, allow_skimmed_fees, msg.skimmed_fee_msat, cur_height, )? } }) } +pub(super) enum HopConnector { + // scid-based routing + ShortChannelId(u64), + // Trampoline-based routing + #[allow(unused)] + Trampoline(PublicKey), +} + pub(super) struct NextPacketDetails { pub(super) next_packet_pubkey: Result, - pub(super) outgoing_scid: u64, + pub(super) outgoing_connector: HopConnector, pub(super) outgoing_amt_msat: u64, pub(super) outgoing_cltv_value: u32, } @@ -394,7 +506,7 @@ where return_malformed_err!("Unknown onion packet version", 0x8000 | 0x4000 | 4); } - let encode_relay_error = |message: &str, err_code: u16, shared_secret: [u8; 32], data: &[u8]| { + let encode_relay_error = |message: &str, err_code: u16, shared_secret: [u8; 32], trampoline_shared_secret: Option<[u8; 32]>, data: &[u8]| { if msg.blinding_point.is_some() { return_malformed_err!(message, INVALID_ONION_BLINDING) } @@ -404,7 +516,7 @@ where channel_id: msg.channel_id, htlc_id: msg.htlc_id, reason: HTLCFailReason::reason(err_code, data.to_vec()) - .get_encrypted_failure_packet(&shared_secret, &None), + .get_encrypted_failure_packet(&shared_secret, &trampoline_shared_secret), })); }; @@ -416,37 +528,48 @@ where Err(onion_utils::OnionDecodeErr::Malformed { err_msg, err_code }) => { return_malformed_err!(err_msg, err_code); }, - Err(onion_utils::OnionDecodeErr::Relay { err_msg, err_code, shared_secret }) => { - return encode_relay_error(err_msg, err_code, shared_secret.secret_bytes(), &[0; 0]); + Err(onion_utils::OnionDecodeErr::Relay { err_msg, err_code, shared_secret, trampoline_shared_secret }) => { + return encode_relay_error(err_msg, err_code, shared_secret.secret_bytes(), trampoline_shared_secret.map(|tss| tss.secret_bytes()), &[0; 0]); }, }; let next_packet_details = match next_hop { - Hop::Forward { next_hop_data: msgs::InboundOnionForwardPayload { short_channel_id, amt_to_forward, outgoing_cltv_value }, shared_secret, .. } => { + onion_utils::Hop::Forward { next_hop_data: msgs::InboundOnionForwardPayload { short_channel_id, amt_to_forward, outgoing_cltv_value }, shared_secret, .. } => { let next_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, msg.onion_routing_packet.public_key.unwrap(), &shared_secret.secret_bytes()); Some(NextPacketDetails { - next_packet_pubkey, outgoing_scid: short_channel_id, + next_packet_pubkey, outgoing_connector: HopConnector::ShortChannelId(short_channel_id), outgoing_amt_msat: amt_to_forward, outgoing_cltv_value }) } - Hop::BlindedForward { next_hop_data: msgs::InboundOnionBlindedForwardPayload { short_channel_id, ref payment_relay, ref payment_constraints, ref features, .. }, shared_secret, .. } => { + onion_utils::Hop::BlindedForward { next_hop_data: msgs::InboundOnionBlindedForwardPayload { short_channel_id, ref payment_relay, ref payment_constraints, ref features, .. }, shared_secret, .. } => { let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward( msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features ) { Ok((amt, cltv)) => (amt, cltv), Err(()) => { return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded forward", - INVALID_ONION_BLINDING, shared_secret.secret_bytes(), &[0; 32]); + INVALID_ONION_BLINDING, shared_secret.secret_bytes(), None, &[0; 32]); } }; let next_packet_pubkey = onion_utils::next_hop_pubkey(&secp_ctx, msg.onion_routing_packet.public_key.unwrap(), &shared_secret.secret_bytes()); Some(NextPacketDetails { - next_packet_pubkey, outgoing_scid: short_channel_id, outgoing_amt_msat: amt_to_forward, + next_packet_pubkey, outgoing_connector: HopConnector::ShortChannelId(short_channel_id), outgoing_amt_msat: amt_to_forward, outgoing_cltv_value }) } + #[cfg(trampoline)] + onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { + let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, + incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); + Some(NextPacketDetails { + next_packet_pubkey: next_trampoline_packet_pubkey, + outgoing_connector: HopConnector::Trampoline(next_trampoline), + outgoing_amt_msat: amt_to_forward, + outgoing_cltv_value, + }) + } _ => None }; diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 85f4c24b8c5..bc4121cfd3d 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1355,19 +1355,25 @@ impl HTLCFailReason { Self(HTLCFailReasonRepr::LightningError { err: msg.reason.clone() }) } + /// Encrypted a failure packet using a shared secret. + /// + /// For phantom nodes or inner Trampoline onions, a secondary_shared_secret can be passed, which + /// will be used to encrypt the failure packet before applying the outer encryption step using + /// incoming_packet_shared_secret. pub(super) fn get_encrypted_failure_packet( - &self, incoming_packet_shared_secret: &[u8; 32], phantom_shared_secret: &Option<[u8; 32]>, + &self, incoming_packet_shared_secret: &[u8; 32], secondary_shared_secret: &Option<[u8; 32]>, ) -> msgs::OnionErrorPacket { match self.0 { HTLCFailReasonRepr::Reason { ref failure_code, ref data } => { - if let Some(phantom_ss) = phantom_shared_secret { - let phantom_packet = - build_failure_packet(phantom_ss, *failure_code, &data[..]).encode(); - let encrypted_phantom_packet = - encrypt_failure_packet(phantom_ss, &phantom_packet); + if let Some(secondary_shared_secret) = secondary_shared_secret { + let inner_packet = + build_failure_packet(secondary_shared_secret, *failure_code, &data[..]) + .encode(); + let encrypted_inner_packet = + encrypt_failure_packet(secondary_shared_secret, &inner_packet); encrypt_failure_packet( incoming_packet_shared_secret, - &encrypted_phantom_packet.data[..], + &encrypted_inner_packet.data[..], ) } else { let packet = build_failure_packet( @@ -1452,6 +1458,32 @@ pub(crate) enum Hop { /// Bytes of the onion packet we're forwarding. new_packet_bytes: [u8; ONION_DATA_LEN], }, + /// This onion was received via Trampoline, and needs to be forwarded to a subsequent Trampoline + /// node. + #[cfg(trampoline)] + TrampolineForward { + #[allow(unused)] + outer_hop_data: msgs::InboundTrampolineEntrypointPayload, + outer_shared_secret: SharedSecret, + incoming_trampoline_public_key: PublicKey, + trampoline_shared_secret: SharedSecret, + next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload, + next_trampoline_hop_hmac: [u8; 32], + new_trampoline_packet_bytes: Vec, + }, + /// This onion was received via Trampoline, and needs to be forwarded to a subsequent Trampoline + /// node. + #[allow(unused)] + #[cfg(trampoline)] + TrampolineBlindedForward { + outer_hop_data: msgs::InboundTrampolineEntrypointPayload, + outer_shared_secret: SharedSecret, + incoming_trampoline_public_key: PublicKey, + trampoline_shared_secret: SharedSecret, + next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload, + next_trampoline_hop_hmac: [u8; 32], + new_trampoline_packet_bytes: Vec, + }, /// This onion payload needs to be forwarded to a next-hop. BlindedForward { /// Onion payload data used in forwarding the payment. @@ -1479,6 +1511,26 @@ pub(crate) enum Hop { /// Shared secret that was used to decrypt hop_data. shared_secret: SharedSecret, }, + /// This onion payload was for us, not for forwarding to a next-hop, and it was sent to us via + /// Trampoline. Contains information for verifying the incoming payment. + #[allow(unused)] + #[cfg(trampoline)] + TrampolineReceive { + outer_hop_data: msgs::InboundTrampolineEntrypointPayload, + outer_shared_secret: SharedSecret, + trampoline_hop_data: msgs::InboundOnionReceivePayload, + trampoline_shared_secret: SharedSecret, + }, + /// This onion payload was for us, not for forwarding to a next-hop, and it was sent to us via + /// Trampoline. Contains information for verifying the incoming payment. + #[allow(unused)] + #[cfg(trampoline)] + TrampolineBlindedReceive { + outer_hop_data: msgs::InboundTrampolineEntrypointPayload, + outer_shared_secret: SharedSecret, + trampoline_hop_data: msgs::InboundOnionBlindedReceivePayload, + trampoline_shared_secret: SharedSecret, + }, } impl Hop { @@ -1499,8 +1551,16 @@ impl Hop { match self { Hop::Forward { shared_secret, .. } => shared_secret, Hop::BlindedForward { shared_secret, .. } => shared_secret, + #[cfg(trampoline)] + Hop::TrampolineForward { outer_shared_secret, .. } => outer_shared_secret, + #[cfg(trampoline)] + Hop::TrampolineBlindedForward { outer_shared_secret, .. } => outer_shared_secret, Hop::Receive { shared_secret, .. } => shared_secret, Hop::BlindedReceive { shared_secret, .. } => shared_secret, + #[cfg(trampoline)] + Hop::TrampolineReceive { outer_shared_secret, .. } => outer_shared_secret, + #[cfg(trampoline)] + Hop::TrampolineBlindedReceive { outer_shared_secret, .. } => outer_shared_secret, } } } @@ -1511,7 +1571,15 @@ pub(crate) enum OnionDecodeErr { /// The HMAC of the onion packet did not match the hop data. Malformed { err_msg: &'static str, err_code: u16 }, /// We failed to decode the onion payload. - Relay { err_msg: &'static str, err_code: u16, shared_secret: SharedSecret }, + /// + /// If the payload we failed to decode belonged to a Trampoline onion, following the successful + /// decoding of the outer onion, the trampoline_shared_secret field should be set. + Relay { + err_msg: &'static str, + err_code: u16, + shared_secret: SharedSecret, + trampoline_shared_secret: Option, + }, } pub(crate) fn decode_next_payment_hop( @@ -1535,7 +1603,7 @@ where hop_data, hmac_bytes, Some(payment_hash), - (blinding_point, node_signer), + (blinding_point, &(*node_signer)), ); match decoded_hop { Ok((next_hop_data, Some((next_hop_hmac, FixedSizeOnionPacket(new_packet_bytes))))) => { @@ -1566,6 +1634,7 @@ where err_msg: "Final Node OnionHopData provided for us as an intermediary node", err_code: 0x4000 | 22, shared_secret, + trampoline_shared_secret: None, }) }, } @@ -1577,6 +1646,97 @@ where msgs::InboundOnionPayload::BlindedReceive(hop_data) => { Ok(Hop::BlindedReceive { shared_secret, hop_data }) }, + #[cfg(trampoline)] + msgs::InboundOnionPayload::TrampolineEntrypoint(hop_data) => { + let incoming_trampoline_public_key = hop_data.trampoline_packet.public_key; + let trampoline_blinded_node_id_tweak = hop_data.current_path_key.map(|bp| { + let blinded_tlvs_ss = + node_signer.ecdh(recipient, &bp, None).unwrap().secret_bytes(); + let mut hmac = HmacEngine::::new(b"blinded_node_id"); + hmac.input(blinded_tlvs_ss.as_ref()); + Scalar::from_be_bytes(Hmac::from_engine(hmac).to_byte_array()).unwrap() + }); + let trampoline_shared_secret = node_signer + .ecdh( + recipient, + &incoming_trampoline_public_key, + trampoline_blinded_node_id_tweak.as_ref(), + ) + .unwrap() + .secret_bytes(); + let decoded_trampoline_hop: Result< + (msgs::InboundTrampolinePayload, Option<([u8; 32], Vec)>), + _, + > = decode_next_hop( + trampoline_shared_secret, + &hop_data.trampoline_packet.hop_data, + hop_data.trampoline_packet.hmac, + Some(payment_hash), + (blinding_point, node_signer), + ); + match decoded_trampoline_hop { + Ok(( + msgs::InboundTrampolinePayload::Forward(trampoline_hop_data), + Some((next_trampoline_hop_hmac, new_trampoline_packet_bytes)), + )) => Ok(Hop::TrampolineForward { + outer_hop_data: hop_data, + outer_shared_secret: shared_secret, + incoming_trampoline_public_key, + trampoline_shared_secret: SharedSecret::from_bytes( + trampoline_shared_secret, + ), + next_trampoline_hop_data: trampoline_hop_data, + next_trampoline_hop_hmac, + new_trampoline_packet_bytes, + }), + Ok(( + msgs::InboundTrampolinePayload::BlindedForward(trampoline_hop_data), + Some((next_trampoline_hop_hmac, new_trampoline_packet_bytes)), + )) => Ok(Hop::TrampolineBlindedForward { + outer_hop_data: hop_data, + outer_shared_secret: shared_secret, + incoming_trampoline_public_key, + trampoline_shared_secret: SharedSecret::from_bytes( + trampoline_shared_secret, + ), + next_trampoline_hop_data: trampoline_hop_data, + next_trampoline_hop_hmac, + new_trampoline_packet_bytes, + }), + Ok((msgs::InboundTrampolinePayload::Receive(trampoline_hop_data), None)) => { + Ok(Hop::TrampolineReceive { + outer_hop_data: hop_data, + outer_shared_secret: shared_secret, + trampoline_hop_data, + trampoline_shared_secret: SharedSecret::from_bytes( + trampoline_shared_secret, + ), + }) + }, + Ok(( + msgs::InboundTrampolinePayload::BlindedReceive(trampoline_hop_data), + None, + )) => Ok(Hop::TrampolineBlindedReceive { + outer_hop_data: hop_data, + outer_shared_secret: shared_secret, + trampoline_hop_data, + trampoline_shared_secret: SharedSecret::from_bytes( + trampoline_shared_secret, + ), + }), + Ok((_, None)) => Err(OnionDecodeErr::Malformed { + err_msg: "Non-final Trampoline onion data provided to us as last hop", + // todo: find more suitable error code + err_code: 0x4000 | 22, + }), + Ok((_, Some(_))) => Err(OnionDecodeErr::Malformed { + err_msg: "Final Trampoline onion data provided to us as intermediate hop", + // todo: find more suitable error code + err_code: 0x4000 | 22, + }), + Err(e) => Err(e), + } + }, _ => { if blinding_point.is_some() { return Err(OnionDecodeErr::Malformed { @@ -1588,6 +1748,7 @@ where err_msg: "Intermediate Node OnionHopData provided for us as a final node", err_code: 0x4000 | 22, shared_secret, + trampoline_shared_secret: None, }) }, }, @@ -1736,6 +1897,7 @@ fn decode_next_hop, N: NextPacketBytes>( err_msg: "Unable to decode our hop data", err_code: error_code, shared_secret: SharedSecret::from_bytes(shared_secret), + trampoline_shared_secret: None, }); }, Ok(msg) => { @@ -1745,6 +1907,7 @@ fn decode_next_hop, N: NextPacketBytes>( err_msg: "Unable to decode our hop data", err_code: 0x4000 | 22, shared_secret: SharedSecret::from_bytes(shared_secret), + trampoline_shared_secret: None, }); } if hmac == [0; 32] {