diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 36e7cea8a22..6e956782eb7 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -396,7 +396,7 @@ fn send_payment(source: &ChanMan, dest: &ChanMan, dest_chan_id: u64, amt: u64, p fee_msat: amt, cltv_expiry_delta: 200, maybe_announced_channel: true, - }], blinded_tail: None }], + }], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_id)) { check_payment_err(err, amt > max_value_sendable || amt < min_value_sendable); @@ -439,7 +439,7 @@ fn send_hop_payment(source: &ChanMan, middle: &ChanMan, middle_chan_id: u64, des fee_msat: amt, cltv_expiry_delta: 200, maybe_announced_channel: true, - }], blinded_tail: None }], + }], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_id)) { let sent_amt = amt + first_hop_fee; diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index 3736bd603e5..090e9fbad3b 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -1776,7 +1776,7 @@ mod tests { fee_msat: 0, cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA as u32, maybe_announced_channel: true, - }], blinded_tail: None }; + }], trampoline_hops: vec![], blinded_tail: None }; $nodes[0].scorer.write_lock().expect(TestResult::PaymentFailure { path: path.clone(), short_channel_id: scored_scid }); $nodes[0].node.push_pending_event(Event::PaymentPathFailed { diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index f6e7f716487..71f0be56f7c 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1402,7 +1402,7 @@ impl MaybeReadable for Event { payment_hash, payment_failed_permanently, failure, - path: Path { hops: path.unwrap(), blinded_tail }, + path: Path { hops: path.unwrap(), trampoline_hops: vec![], blinded_tail }, short_channel_id, #[cfg(test)] error_code, @@ -1530,7 +1530,7 @@ impl MaybeReadable for Event { Ok(Some(Event::PaymentPathSuccessful { payment_id: payment_id.0.unwrap(), payment_hash, - path: Path { hops: path, blinded_tail }, + path: Path { hops: path, trampoline_hops: vec![], blinded_tail }, })) }; f() @@ -1595,7 +1595,7 @@ impl MaybeReadable for Event { Ok(Some(Event::ProbeSuccessful { payment_id: payment_id.0.unwrap(), payment_hash: payment_hash.0.unwrap(), - path: Path { hops: path, blinded_tail }, + path: Path { hops: path, trampoline_hops: vec![], blinded_tail }, })) }; f() @@ -1612,7 +1612,7 @@ impl MaybeReadable for Event { Ok(Some(Event::ProbeFailed { payment_id: payment_id.0.unwrap(), payment_hash: payment_hash.0.unwrap(), - path: Path { hops: path, blinded_tail }, + path: Path { hops: path, trampoline_hops: vec![], blinded_tail }, short_channel_id, })) }; diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 58d35a1a95a..42aa1458d1c 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -9531,7 +9531,7 @@ mod tests { cltv_expiry: 200000000, state: OutboundHTLCState::Committed, source: HTLCSource::OutboundRoute { - path: Path { hops: Vec::new(), blinded_tail: None }, + path: Path { hops: Vec::new(), trampoline_hops: vec![], blinded_tail: None }, session_priv: SecretKey::from_slice(&>::from_hex("0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap()[..]).unwrap(), first_hop_htlc_msat: 548, payment_id: PaymentId([42; 32]), @@ -9905,6 +9905,7 @@ mod tests { node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: false, }], + trampoline_hops: vec![], blinded_tail: None }, session_priv: test_utils::privkey(42), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index abbab5ff0a7..cdee52fb6fe 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -506,7 +506,7 @@ impl HTLCSource { #[cfg(test)] pub fn dummy() -> Self { HTLCSource::OutboundRoute { - path: Path { hops: Vec::new(), blinded_tail: None }, + path: Path { hops: Vec::new(), trampoline_hops: Vec::new(), blinded_tail: None }, session_priv: SecretKey::from_slice(&[1; 32]).unwrap(), first_hop_htlc_msat: 0, payment_id: PaymentId([2; 32]), @@ -10881,7 +10881,7 @@ impl Readable for HTLCSource { // instead. payment_id = Some(PaymentId(*session_priv.0.unwrap().as_ref())); } - let path = Path { hops: path_hops, blinded_tail }; + let path = Path { hops: path_hops, trampoline_hops: vec![], blinded_tail }; if path.hops.len() == 0 { return Err(DecodeError::InvalidValue); } diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 8dd3f1fc912..3b9292865d9 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1058,7 +1058,7 @@ fn fake_network_test() { hops[1].fee_msat = chan_4.1.contents.fee_base_msat as u64 + chan_4.1.contents.fee_proportional_millionths as u64 * hops[2].fee_msat as u64 / 1000000; hops[0].fee_msat = chan_3.0.contents.fee_base_msat as u64 + chan_3.0.contents.fee_proportional_millionths as u64 * hops[1].fee_msat as u64 / 1000000; let payment_preimage_1 = send_along_route(&nodes[1], - Route { paths: vec![Path { hops, blinded_tail: None }], route_params: None }, + Route { paths: vec![Path { hops, trampoline_hops: vec![], blinded_tail: None }], route_params: None }, &vec!(&nodes[2], &nodes[3], &nodes[1])[..], 1000000).0; let mut hops = Vec::with_capacity(3); @@ -1092,7 +1092,7 @@ fn fake_network_test() { hops[1].fee_msat = chan_2.1.contents.fee_base_msat as u64 + chan_2.1.contents.fee_proportional_millionths as u64 * hops[2].fee_msat as u64 / 1000000; hops[0].fee_msat = chan_3.1.contents.fee_base_msat as u64 + chan_3.1.contents.fee_proportional_millionths as u64 * hops[1].fee_msat as u64 / 1000000; let payment_hash_2 = send_along_route(&nodes[1], - Route { paths: vec![Path { hops, blinded_tail: None }], route_params: None }, + Route { paths: vec![Path { hops, trampoline_hops: vec![], blinded_tail: None }], route_params: None }, &vec!(&nodes[3], &nodes[2], &nodes[1])[..], 1000000).1; // Claim the rebalances... diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index cbdb29bfc7c..612766991bd 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -41,6 +41,7 @@ use crate::sign::{NodeSigner, Recipient}; #[allow(unused_imports)] use crate::prelude::*; +use core::cmp; use core::fmt; use core::fmt::Debug; use core::ops::Deref; @@ -55,7 +56,7 @@ use crate::io_extras::read_to_end; use crate::events::{EventsProvider, MessageSendEventsProvider}; use crate::crypto::streams::ChaChaPolyReadAdapter; use crate::util::logger; -use crate::util::ser::{LengthReadable, LengthReadableArgs, Readable, ReadableArgs, Writeable, Writer, WithoutLength, FixedLengthReader, HighZeroBytesDroppedBigSize, Hostname, TransactionU16LenLimited, BigSize}; +use crate::util::ser::{BigSize, FixedLengthReader, HighZeroBytesDroppedBigSize, Hostname, LengthRead, LengthReadable, LengthReadableArgs, Readable, ReadableArgs, TransactionU16LenLimited, WithoutLength, Writeable, Writer}; use crate::util::base32; use crate::routing::gossip::{NodeAlias, NodeId}; @@ -1767,6 +1768,21 @@ mod fuzzy_internal_msgs { outgoing_cltv_value: u32, /// The node id to which the trampoline node must find a route outgoing_node_id: PublicKey, + }, + #[allow(unused)] + BlindedForward { + encrypted_tlvs: Vec, + intro_node_blinding_point: Option, + }, + #[allow(unused)] + BlindedReceive { + sender_intended_htlc_amt_msat: u64, + total_msat: u64, + cltv_expiry_height: u32, + encrypted_tlvs: Vec, + intro_node_blinding_point: Option, // Set if the introduction node of the blinded path is the final node + keysend_preimage: Option, + custom_tlvs: Vec<(u64, Vec)>, } } @@ -1856,6 +1872,34 @@ impl Writeable for TrampolineOnionPacket { } } +impl LengthReadable for TrampolineOnionPacket { + fn read(r: &mut R) -> Result { + const READ_BUFFER_SIZE: usize = 4096; + + let version = Readable::read(r)?; + let public_key = Readable::read(r)?; + + let mut hop_data = Vec::new(); + let hop_data_len = r.total_bytes().saturating_sub(66) as usize; // 1 (version) + 33 (pubkey) + 32 (HMAC) = 66 + let mut read_idx = 0; + while read_idx < hop_data_len { + let mut read_buffer = [0; READ_BUFFER_SIZE]; + let read_amt = cmp::min(hop_data_len - read_idx, READ_BUFFER_SIZE); + r.read_exact(&mut read_buffer[..read_amt])?; + hop_data.extend_from_slice(&read_buffer[..read_amt]); + read_idx += read_amt; + } + + let hmac = Readable::read(r)?; + Ok(TrampolineOnionPacket { + version, + public_key, + hop_data, + hmac, + }) + } +} + impl Debug for TrampolineOnionPacket { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_fmt(format_args!("TrampolineOnionPacket version {} with hmac {:?}", self.version, &self.hmac[..])) @@ -2644,7 +2688,31 @@ impl Writeable for OutboundTrampolinePayload { (4, HighZeroBytesDroppedBigSize(*outgoing_cltv_value), required), (14, outgoing_node_id, required) }); - } + }, + Self::BlindedForward { encrypted_tlvs, intro_node_blinding_point } => { + _encode_varint_length_prefixed_tlv!(w, { + (10, *encrypted_tlvs, required_vec), + (12, intro_node_blinding_point, option) + }); + }, + Self::BlindedReceive { + sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, encrypted_tlvs, + intro_node_blinding_point, keysend_preimage, ref custom_tlvs, + } => { + // We need to update [`ln::outbound_payment::RecipientOnionFields::with_custom_tlvs`] + // to reject any reserved types in the experimental range if new ones are ever + // standardized. + let keysend_tlv = keysend_preimage.map(|preimage| (5482373484, preimage.encode())); + let mut custom_tlvs: Vec<&(u64, Vec)> = custom_tlvs.iter().chain(keysend_tlv.iter()).collect(); + custom_tlvs.sort_unstable_by_key(|(typ, _)| *typ); + _encode_varint_length_prefixed_tlv!(w, { + (2, HighZeroBytesDroppedBigSize(*sender_intended_htlc_amt_msat), required), + (4, HighZeroBytesDroppedBigSize(*cltv_expiry_height), required), + (10, *encrypted_tlvs, required_vec), + (12, intro_node_blinding_point, option), + (18, HighZeroBytesDroppedBigSize(*total_msat), required) + }, custom_tlvs.iter()); + }, } Ok(()) } @@ -3180,7 +3248,7 @@ mod tests { use crate::ln::msgs::{self, FinalOnionHopData, OnionErrorPacket, CommonOpenChannelFields, CommonAcceptChannelFields, TrampolineOnionPacket}; use crate::ln::msgs::SocketAddress; use crate::routing::gossip::{NodeAlias, NodeId}; - use crate::util::ser::{BigSize, Hostname, Readable, ReadableArgs, TransactionU16LenLimited, Writeable}; + use crate::util::ser::{BigSize, FixedLengthReader, Hostname, LengthReadable, Readable, ReadableArgs, TransactionU16LenLimited, Writeable}; use crate::util::test_utils; use bitcoin::hashes::hex::FromHex; @@ -4496,6 +4564,13 @@ mod tests { let encoded_trampoline_packet = trampoline_packet.encode(); assert_eq!(encoded_trampoline_packet.len(), 716); + { // verify that a codec round trip works + let mut reader = Cursor::new(&encoded_trampoline_packet); + let mut trampoline_packet_reader = FixedLengthReader::new(&mut reader, encoded_trampoline_packet.len() as u64); + let decoded_trampoline_packet: TrampolineOnionPacket = ::read(&mut trampoline_packet_reader).unwrap(); + assert_eq!(decoded_trampoline_packet.encode(), encoded_trampoline_packet); + } + let msg = msgs::OutboundOnionPayload::TrampolineEntrypoint { multipath_trampoline_data: None, amt_to_forward: 0x0badf00d01020304, diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index aa8ee0ce9be..af209eda152 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -532,7 +532,7 @@ mod tests { // Ensure the onion will not fit all the payloads by adding a large custom TLV. recipient_onion.custom_tlvs.push((13377331, vec![0; 1156])); - let path = Path { hops, blinded_tail: None, }; + let path = Path { hops, trampoline_hops: vec![], blinded_tail: None, }; let onion_keys = super::onion_utils::construct_onion_keys(&secp_ctx, &path, &session_priv).unwrap(); let (onion_payloads, ..) = super::onion_utils::build_onion_payloads( &path, total_amt_msat, recipient_onion, cur_height + 1, &Some(keysend_preimage) @@ -558,6 +558,7 @@ mod tests { let path = Path { hops: hops, + trampoline_hops: vec![], blinded_tail: None, }; diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index f2b5c69e9e6..03c31ee67b1 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -14,7 +14,7 @@ use crate::ln::msgs; use crate::ln::wire::Encode; use crate::ln::{PaymentHash, PaymentPreimage}; use crate::routing::gossip::NetworkUpdate; -use crate::routing::router::{BlindedTail, Path, RouteHop}; +use crate::routing::router::{BlindedTail, Path, RouteHop, TrampolineHop}; use crate::sign::NodeSigner; use crate::util::errors::{self, APIError}; use crate::util::logger::Logger; @@ -173,6 +173,64 @@ pub(super) fn construct_onion_keys( Ok(res) } +pub(super) fn construct_trampoline_keys_callback( + secp_ctx: &Secp256k1, path: &[TrampolineHop], session_priv: &SecretKey, mut callback: FType, +) -> Result<(), secp256k1::Error> +where + T: secp256k1::Signing, + FType: FnMut(SharedSecret, [u8; 32], PublicKey, Option<&TrampolineHop>, 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.iter().map(|h| (&h.pubkey, Some(h))); + for (idx, (pubkey, route_hop_opt)) in unblinded_hops_iter.enumerate() { + let shared_secret = SharedSecret::new(pubkey, &blinded_priv); + + let mut sha = Sha256::engine(); + sha.input(&blinded_pub.serialize()[..]); + sha.input(shared_secret.as_ref()); + let blinding_factor = Sha256::from_engine(sha).to_byte_array(); + + let ephemeral_pubkey = blinded_pub; + + blinded_priv = blinded_priv.mul_tweak(&Scalar::from_be_bytes(blinding_factor).unwrap())?; + blinded_pub = PublicKey::from_secret_key(secp_ctx, &blinded_priv); + + callback(shared_secret, blinding_factor, ephemeral_pubkey, route_hop_opt, idx); + } + + Ok(()) +} + +// can only fail if an intermediary hop has an invalid public key or session_priv is invalid +pub(super) fn construct_trampoline_keys( + secp_ctx: &Secp256k1, path: &[TrampolineHop], session_priv: &SecretKey, +) -> Result, secp256k1::Error> { + let mut res = Vec::with_capacity(path.len()); + + construct_trampoline_keys_callback( + secp_ctx, + &path, + 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) +} + /// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. pub(super) fn build_onion_payloads( path: &Path, total_msat: u64, mut recipient_onion: RecipientOnionFields, @@ -259,6 +317,80 @@ pub(super) fn build_onion_payloads( Ok((res, cur_value_msat, cur_cltv)) } +/// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. +pub(super) fn build_trampoline_payloads( + path: &Path, total_msat: u64, recipient_onion: RecipientOnionFields, starting_htlc_offset: u32, + keysend_preimage: &Option, +) -> Result<(Vec, u64, u32), APIError> { + let mut cur_value_msat = 0u64; + let mut cur_cltv = starting_htlc_offset; + let mut last_node_id = None; + let mut res: Vec = Vec::with_capacity( + path.trampoline_hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), + ); + + for (idx, hop) in path.trampoline_hops.iter().rev().enumerate() { + // First hop gets special values so that it can check, on receipt, that everything is + // exactly as it should be (and the next hop isn't trying to probe to find out if we're + // the intended recipient). + let value_msat = if cur_value_msat == 0 { hop.fee_msat } else { cur_value_msat }; + let cltv = if cur_cltv == starting_htlc_offset { + hop.cltv_expiry_delta + starting_htlc_offset + } else { + cur_cltv + }; + if idx == 0 { + let BlindedTail { + blinding_point, + hops, + final_value_msat, + excess_final_cltv_expiry_delta, + .. + } = path.blinded_tail.as_ref().ok_or(APIError::InvalidRoute { + err: "Trampoline payments must terminate in blinded tails.".to_owned(), + })?; + let mut blinding_point = Some(*blinding_point); + for (i, blinded_hop) in hops.iter().enumerate() { + if i == hops.len() - 1 { + cur_value_msat += final_value_msat; + res.push(msgs::OutboundTrampolinePayload::BlindedReceive { + sender_intended_htlc_amt_msat: *final_value_msat, + total_msat, + cltv_expiry_height: cur_cltv + excess_final_cltv_expiry_delta, + encrypted_tlvs: blinded_hop.encrypted_payload.clone(), + intro_node_blinding_point: blinding_point.take(), + keysend_preimage: *keysend_preimage, + custom_tlvs: recipient_onion.custom_tlvs.clone(), + }); + } else { + res.push(msgs::OutboundTrampolinePayload::BlindedForward { + encrypted_tlvs: blinded_hop.encrypted_payload.clone(), + intro_node_blinding_point: blinding_point.take(), + }); + } + } + } else { + let payload = msgs::OutboundTrampolinePayload::Forward { + amt_to_forward: value_msat, + outgoing_cltv_value: cltv, + outgoing_node_id: last_node_id + .expect("outgoing node id cannot be None after last hop"), + }; + res.insert(0, payload); + } + cur_value_msat += hop.fee_msat; + if cur_value_msat >= 21000000 * 100000000 * 1000 { + return Err(APIError::InvalidRoute { err: "Channel fees overflowed?".to_owned() }); + } + cur_cltv += hop.cltv_expiry_delta as u32; + if cur_cltv >= 500000000 { + return Err(APIError::InvalidRoute { err: "Channel CLTV overflowed?".to_owned() }); + } + last_node_id = Some(hop.pubkey); + } + Ok((res, cur_value_msat, cur_cltv)) +} + /// Length of the onion data packet. Before TLV-based onions this was 20 65-byte hops, though now /// the hops can be of variable length. pub(crate) const ONION_DATA_LEN: usize = 20 * 65; @@ -1289,7 +1421,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops. }, - ], blinded_tail: None }], + ], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }; diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index b05d6f3f729..53b0b9c5717 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1989,7 +1989,7 @@ mod tests { fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, - }], blinded_tail: None }], + }], trampoline_hops: vec![], blinded_tail: None }], route_params: Some(route_params.clone()), }; router.expect_find_route(route_params.clone(), Ok(route.clone())); @@ -2327,6 +2327,7 @@ mod tests { maybe_announced_channel: true, } ], + trampoline_hops: vec![], blinded_tail: None, } ], diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index a75120797ca..ac4b264575a 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -2446,7 +2446,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 2, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2455,7 +2455,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 2, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(route_params.clone()), }; @@ -2477,7 +2477,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 4, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2486,7 +2486,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 4, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(retry_1_params.clone()), }; @@ -2508,7 +2508,7 @@ fn auto_retry_partial_failure() { fee_msat: amt_msat / 4, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(retry_2_params.clone()), }; @@ -2653,7 +2653,7 @@ fn auto_retry_zero_attempts_send_error() { fee_msat: amt_msat, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(route_params.clone()), }; @@ -2751,7 +2751,7 @@ fn retry_multi_path_single_failed_payment() { fee_msat: 10_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2760,7 +2760,7 @@ fn retry_multi_path_single_failed_payment() { fee_msat: 100_000_001, // Our default max-HTLC-value is 10% of the channel value, which this is one more than cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(route_params.clone()), }; @@ -2842,7 +2842,7 @@ fn immediate_retry_on_failure() { fee_msat: 100_000_001, // Our default max-HTLC-value is 10% of the channel value, which this is one more than cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, ], route_params: Some(route_params.clone()), }; @@ -2936,7 +2936,7 @@ fn no_extra_retries_on_back_to_back_fail() { fee_msat: 100_000_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -2953,7 +2953,7 @@ fn no_extra_retries_on_back_to_back_fail() { fee_msat: 100_000_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None } + }], trampoline_hops: vec![], blinded_tail: None } ], route_params: Some(route_params.clone()), }; @@ -3141,7 +3141,7 @@ fn test_simple_partial_retry() { fee_msat: 100_000_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), node_features: nodes[1].node.node_features(), @@ -3158,7 +3158,7 @@ fn test_simple_partial_retry() { fee_msat: 100_000_000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None } + }], trampoline_hops: vec![], blinded_tail: None } ], route_params: Some(route_params.clone()), }; @@ -3312,7 +3312,7 @@ fn test_threaded_payment_retries() { fee_msat: amt_msat / 1000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None }, + }], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[2].node.get_our_node_id(), node_features: nodes[2].node.node_features(), @@ -3329,7 +3329,7 @@ fn test_threaded_payment_retries() { fee_msat: amt_msat - amt_msat / 1000, cltv_expiry_delta: 100, maybe_announced_channel: true, - }], blinded_tail: None } + }], trampoline_hops: vec![], blinded_tail: None } ], route_params: Some(route_params.clone()), }; diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 59ec3f61862..e22121b8957 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -382,6 +382,41 @@ impl_writeable_tlv_based!(RouteHop, { (10, cltv_expiry_delta, required), }); +/// A Trampoline hop in a route, and additional metadata about it. "Hop" is defined as a node. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct TrampolineHop { + /// The node_id of the node at this hop. + pub pubkey: PublicKey, + /// The node_announcement features of the node at this hop. For the last hop, these may be + /// amended to match the features present in the invoice this node generated. + pub node_features: NodeFeatures, + /// The channel_announcement features of the channel that should be used from the previous hop + /// to reach this node. + pub channel_features: ChannelFeatures, + /// The fee taken on this hop (for paying for the use of the *next* channel in the path). + /// If this is the last hop in [`Path::hops`]: + /// * if we're sending to a [`BlindedPath`], this is the fee paid for use of the entire blinded path + /// * otherwise, this is the full value of this [`Path`]'s part of the payment + /// + /// [`BlindedPath`]: crate::blinded_path::BlindedPath + pub fee_msat: u64, + /// The CLTV delta added for this hop. + /// If this is the last hop in [`Path::hops`]: + /// * if we're sending to a [`BlindedPath`], this is the CLTV delta for the entire blinded path + /// * otherwise, this is the CLTV delta expected at the destination + /// + /// [`BlindedPath`]: crate::blinded_path::BlindedPath + pub cltv_expiry_delta: u32, +} + +impl_writeable_tlv_based!(TrampolineHop, { + (0, pubkey, required), + (2, node_features, required), + (6, channel_features, required), + (8, fee_msat, required), + (10, cltv_expiry_delta, required), +}); + /// The blinded portion of a [`Path`], if we're routing to a recipient who provided blinded paths in /// their [`Bolt12Invoice`]. /// @@ -416,6 +451,9 @@ impl_writeable_tlv_based!(BlindedTail, { pub struct Path { /// The list of unblinded hops in this [`Path`]. Must be at least length one. pub hops: Vec, + /// The list of unblinded Trampoline hops. If present, must be at least one. + /// The public key of the first Trampoline hop must match the public key of the last regular hop. + pub trampoline_hops: Vec, /// The blinded path at which this path terminates, if we're sending to one, and its metadata. pub blinded_tail: Option, } @@ -548,7 +586,7 @@ impl Readable for Route { if hops.is_empty() { return Err(DecodeError::InvalidValue); } min_final_cltv_expiry_delta = cmp::min(min_final_cltv_expiry_delta, hops.last().unwrap().cltv_expiry_delta); - paths.push(Path { hops, blinded_tail: None }); + paths.push(Path { hops, trampoline_hops: vec![], blinded_tail: None }); } _init_and_read_len_prefixed_tlv_fields!(reader, { (1, payment_params, (option: ReadableArgs, min_final_cltv_expiry_delta)), @@ -3096,7 +3134,7 @@ where L::Target: Logger { core::mem::replace(&mut hop.cltv_expiry_delta, prev_cltv_expiry_delta) }); - paths.push(Path { hops, blinded_tail }); + paths.push(Path { hops, trampoline_hops: vec![], blinded_tail }); } // Make sure we would never create a route with more paths than we allow. debug_assert!(paths.len() <= payment_params.max_path_count.into()); @@ -6678,7 +6716,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0, maybe_announced_channel: true, }, - ], blinded_tail: None }], + ], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }; @@ -6700,7 +6738,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true, }, - ], blinded_tail: None }, Path { hops: vec![ + ], trampoline_hops: vec![], blinded_tail: None }, Path { hops: vec![ RouteHop { pubkey: PublicKey::from_slice(&>::from_hex("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), @@ -6711,7 +6749,7 @@ mod tests { channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true, }, - ], blinded_tail: None }], + ], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }; @@ -7323,6 +7361,7 @@ mod tests { cltv_expiry_delta: 0, maybe_announced_channel: true, }], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: blinded_path_1.blinded_hops, blinding_point: blinded_path_1.blinding_point, @@ -7337,7 +7376,7 @@ mod tests { fee_msat: 100, cltv_expiry_delta: 0, maybe_announced_channel: true, - }], blinded_tail: None }], + }], trampoline_hops: vec![], blinded_tail: None }], route_params: None, }; let encoded_route = route.encode(); @@ -7387,6 +7426,7 @@ mod tests { cltv_expiry_delta: 0, maybe_announced_channel: false, }], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: blinded_path.blinded_hops, blinding_point: blinded_path.blinding_point, @@ -7430,6 +7470,7 @@ mod tests { maybe_announced_channel: false, } ], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: blinded_path.blinded_hops, blinding_point: blinded_path.blinding_point, diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index 4cb9144d339..60cbdc852e4 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -2307,7 +2307,7 @@ mod tests { path_hop(source_pubkey(), 41, 1), path_hop(target_pubkey(), 42, 2), path_hop(recipient_pubkey(), 43, amount_msat), - ], blinded_tail: None, + ], trampoline_hops: vec![], blinded_tail: None, } } @@ -2788,7 +2788,7 @@ mod tests { }); assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 128); - scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 43, Duration::ZERO); + scorer.payment_path_failed(&Path { hops: path, trampoline_hops: vec![], blinded_tail: None }, 43, Duration::ZERO); let channel = network_graph.read_only().channel(42).unwrap().to_owned(); let (info, _) = channel.as_directed_from(&node_a).unwrap(); @@ -3485,7 +3485,7 @@ mod tests { path_hop(source_pubkey(), 42, 1), path_hop(sender_pubkey(), 41, 0), ]; - scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 42, Duration::from_secs(10 * (16 + 60 * 60))); + scorer.payment_path_failed(&Path { hops: path, trampoline_hops: vec![], blinded_tail: None }, 42, Duration::from_secs(10 * (16 + 60 * 60))); } #[test] @@ -3712,6 +3712,7 @@ pub mod benches { cltv_expiry_delta: 42, maybe_announced_channel: true, }], + trampoline_hops: vec![], blinded_tail: None }; seed = seed.overflowing_mul(6364136223846793005).0.overflowing_add(1).0;