diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index ca3f0028f3a..84c1700d4c1 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -565,6 +565,7 @@ fn send_payment( maybe_announced_channel: true, }], blinded_tail: None, + trampoline_hops: vec![], }], route_params: None, }); @@ -647,6 +648,7 @@ fn send_hop_payment( }, ], blinded_tail: None, + trampoline_hops: vec![], }], route_params: None, }); diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index af7c7ffb003..c6e7878c7e6 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -2540,7 +2540,7 @@ mod tests { fee_msat: 0, cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA as u32, maybe_announced_channel: true, - }], blinded_tail: None }; + }], trampoline_hops: vec![], blinded_tail: None }; $nodes[0].scorer.write_lock().expect(TestResult::PaymentFailure { path: path.clone(), short_channel_id: scored_scid }); $nodes[0].node.push_pending_event(Event::PaymentPathFailed { diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 5bc446f9724..ddaaadc86a5 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -30,7 +30,7 @@ use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret}; use crate::offers::invoice::Bolt12Invoice; use crate::onion_message::messenger::Responder; use crate::routing::gossip::NetworkUpdate; -use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters}; +use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters, TrampolineHop}; use crate::sign::SpendableOutputDescriptor; use crate::util::errors::APIError; use crate::util::ser::{BigSize, FixedLengthReader, Writeable, Writer, MaybeReadable, Readable, RequiredWrapper, UpgradableRequired, WithoutLength}; @@ -1185,12 +1185,12 @@ pub enum Event { /// events generated or serialized by versions prior to 0.0.122. next_user_channel_id: Option, /// The node id of the previous node. - /// + /// /// This is only `None` for HTLCs received prior to 0.1 or for events serialized by /// versions prior to 0.1 prev_node_id: Option, /// The node id of the next node. - /// + /// /// This is only `None` for HTLCs received prior to 0.1 or for events serialized by /// versions prior to 0.1 next_node_id: Option, @@ -1584,6 +1584,7 @@ impl Writeable for Event { (9, None::, option), // retry in LDK versions prior to 0.0.115 (11, payment_id, option), (13, failure, required), + (15, path.trampoline_hops, optional_vec), }); }, &Event::PendingHTLCsForwardable { time_forwardable: _ } => { @@ -1670,6 +1671,7 @@ impl Writeable for Event { (2, payment_hash, option), (4, path.hops, required_vec), (6, path.blinded_tail, option), + (8, path.trampoline_hops, optional_vec), }) }, &Event::PaymentFailed { ref payment_id, ref payment_hash, ref reason } => { @@ -1734,6 +1736,7 @@ impl Writeable for Event { (2, payment_hash, required), (4, path.hops, required_vec), (6, path.blinded_tail, option), + (8, path.trampoline_hops, optional_vec), }) }, &Event::ProbeFailed { ref payment_id, ref payment_hash, ref path, ref short_channel_id } => { @@ -1744,6 +1747,7 @@ impl Writeable for Event { (4, path.hops, required_vec), (6, short_channel_id, option), (8, path.blinded_tail, option), + (10, path.trampoline_hops, optional_vec) }) }, &Event::HTLCHandlingFailed { ref prev_channel_id, ref failed_next_destination } => { @@ -1919,6 +1923,7 @@ impl MaybeReadable for Event { let mut network_update = None; let mut blinded_tail: Option = None; let mut path: Option> = Some(vec![]); + let mut trampoline_path: Option> = Some(vec![]); let mut short_channel_id = None; let mut payment_id = None; let mut failure_opt = None; @@ -1933,6 +1938,7 @@ impl MaybeReadable for Event { (7, short_channel_id, option), (11, payment_id, option), (13, failure_opt, upgradable_option), + (15, trampoline_path, optional_vec), }); let failure = failure_opt.unwrap_or_else(|| PathFailure::OnPath { network_update }); Ok(Some(Event::PaymentPathFailed { @@ -1940,7 +1946,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: trampoline_path.unwrap_or(vec![]), blinded_tail }, short_channel_id, #[cfg(test)] error_code, @@ -2081,11 +2087,12 @@ impl MaybeReadable for Event { (2, payment_hash, option), (4, path, required_vec), (6, blinded_tail, option), + (8, trampoline_path, optional_vec), }); Ok(Some(Event::PaymentPathSuccessful { payment_id: payment_id.0.unwrap(), payment_hash, - path: Path { hops: path, blinded_tail }, + path: Path { hops: path, trampoline_hops: trampoline_path.unwrap_or(vec![]), blinded_tail }, })) }; f() @@ -2161,11 +2168,12 @@ impl MaybeReadable for Event { (2, payment_hash, required), (4, path, required_vec), (6, blinded_tail, option), + (8, trampoline_path, optional_vec) }); 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: trampoline_path.unwrap_or(vec![]), blinded_tail }, })) }; f() @@ -2178,11 +2186,12 @@ impl MaybeReadable for Event { (4, path, required_vec), (6, short_channel_id, option), (8, blinded_tail, option), + (10, trampoline_path, optional_vec) }); 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: trampoline_path.unwrap_or(vec![]), blinded_tail }, short_channel_id, })) }; diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index c1fad65c14f..39d89efa86f 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -8,6 +8,7 @@ // licenses. use bitcoin::hashes::hex::FromHex; +use bitcoin::hex::DisplayHex; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, schnorr}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; @@ -30,16 +31,18 @@ use crate::ln::outbound_payment::{Retry, IDEMPOTENCY_TIMEOUT_TICKS}; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::offers::nonce::Nonce; use crate::prelude::*; -use crate::routing::router::{BlindedTail, Path, Payee, PaymentParameters, RouteHop, RouteParameters}; +use crate::routing::router::{BlindedTail, Path, Payee, PaymentParameters, RouteHop, RouteParameters, TrampolineHop}; use crate::sign::{NodeSigner, Recipient}; use crate::util::config::UserConfig; -use crate::util::ser::WithoutLength; +use crate::util::ser::{WithoutLength, Writeable}; use crate::util::test_utils; use lightning_invoice::RawBolt11Invoice; #[cfg(async_payments)] use { crate::ln::inbound_payment, crate::types::payment::PaymentPreimage, }; +use types::features::Features; +use crate::blinded_path::BlindedHop; fn blinded_payment_path( payment_secret: PaymentSecret, intro_node_min_htlc: u64, intro_node_max_htlc: u64, @@ -1607,6 +1610,7 @@ fn route_blinding_spec_test_vector() { cltv_expiry_delta: 42, maybe_announced_channel: false, }], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: blinded_hops, blinding_point: bob_blinding_point, @@ -1757,3 +1761,84 @@ fn route_blinding_spec_test_vector() { _ => panic!("Unexpected error") } } + +#[test] +fn test_combined_trampoline_onion_creation_vectors() { + // As per https://github.com/lightning/bolts/blob/fa0594ac2af3531d734f1d707a146d6e13679451/bolt04/trampoline-to-blinded-path-payment-onion-test.json#L251 + + let mut secp_ctx = Secp256k1::new(); + let session_priv = secret_from_hex("a64feb81abd58e473df290e9e1c07dc3e56114495cadf33191f44ba5448ebe99"); + + let path = Path { + hops: vec![ + // Bob + RouteHop { + pubkey: pubkey_from_hex("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"), + node_features: NodeFeatures::empty(), + short_channel_id: 0, + channel_features: ChannelFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + maybe_announced_channel: false, + }, + + // Carol + RouteHop { + pubkey: pubkey_from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"), + node_features: NodeFeatures::empty(), + short_channel_id: (572330 << 40) + (42 << 16) + 2821, + channel_features: ChannelFeatures::empty(), + fee_msat: 150_153_000, + cltv_expiry_delta: 24, + maybe_announced_channel: false, + }, + ], + trampoline_hops: vec![ + // Carol's pubkey + TrampolineHop { + pubkey: pubkey_from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"), + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + // Dave's pubkey (the intro node needs to be duplicated) + TrampolineHop { + pubkey: pubkey_from_hex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"), + node_features: Features::empty(), + fee_msat: 150_500, // incorporate both base and proportional fee + cltv_expiry_delta: 36, + } + ], + blinded_tail: Some(BlindedTail { + hops: vec![ + BlindedHop { + blinded_node_id: pubkey_from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be"), + encrypted_payload: bytes_from_hex("0ccf3c8a58deaa603f657ee2a5ed9d604eb5c8ca1e5f801989afa8f3ea6d789bbdde2c7e7a1ef9ca8c38d2c54760febad8446d3f273ddb537569ef56613846ccd3aba78a"), + }, + BlindedHop { + blinded_node_id: pubkey_from_hex("020e2dbadcc2005e859819ddebbe88a834ae8a6d2b049233c07335f15cd1dc5f22"), + encrypted_payload: bytes_from_hex("bcd747394fbd4d99588da075a623316e15a576df5bc785cccc7cd6ec7b398acce6faf520175f9ec920f2ef261cdb83dc28cc3a0eeb970107b3306489bf771ef5b1213bca811d345285405861d08a655b6c237fa247a8b4491beee20c878a60e9816492026d8feb9dafa84585b253978db6a0aa2945df5ef445c61e801fb82f43d5f00716baf9fc9b3de50bc22950a36bda8fc27bfb1242e5860c7e687438d4133e058770361a19b6c271a2a07788d34dccc27e39b9829b061a4d960eac4a2c2b0f4de506c24f9af3868c0aff6dda27281c"), + } + ], + blinding_point: pubkey_from_hex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e"), + excess_final_cltv_expiry_delta: 0, + final_value_msat: 150_000_000 + }), + }; + + let associated_data_slice = secret_from_hex("e89bc505e84aaca09613833fc58c9069078fb43bfbea0488f34eec9db99b5f82"); + let associated_data = PaymentHash(associated_data_slice.secret_bytes()); + let payment_secret = PaymentSecret(secret_from_hex("7494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da").secret_bytes()); + let outer_session_key = secret_from_hex("4f777e8dac16e6dfe333066d9efb014f7a51d11762ff76eca4d3a95ada99ba3e"); + let outer_onion_prng_seed = onion_utils::gen_pad_from_shared_secret(&outer_session_key.secret_bytes()); + + let amt_msat = 150_000_000; + let cur_height = 800_000; + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); + let (bob_onion, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion_internal(&secp_ctx, &path, &session_priv, amt_msat, &recipient_onion_fields, cur_height, &associated_data, &None, None, [0; 32], Some(payment_secret), Some(outer_session_key), Some(outer_onion_prng_seed)).unwrap(); + + let outer_onion_packet_hex = bob_onion.encode().to_lower_hex_string(); + assert_eq!(outer_onion_packet_hex, "00025fd60556c134ae97e4baedba220a644037754ee67c54fd05e93bf40c17cbb73362fb9dee96001ff229945595b6edb59437a6bc143406d3f90f749892a84d8d430c6890437d26d5bfc599d565316ef51347521075bbab87c59c57bcf20af7e63d7192b46cf171e4f73cb11f9f603915389105d91ad630224bea95d735e3988add1e24b5bf28f1d7128db64284d90a839ba340d088c74b1fb1bd21136b1809428ec5399c8649e9bdf92d2dcfc694deae5046fa5b2bdf646847aaad73f5e95275763091c90e71031cae1f9a770fdea559642c9c02f424a2a28163dd0957e3874bd28a97bec67d18c0321b0e68bc804aa8345b17cb626e2348ca06c8312a167c989521056b0f25c55559d446507d6c491d50605cb79fa87929ce64b0a9860926eeaec2c431d926a1cadb9a1186e4061cb01671a122fc1f57602cbef06d6c194ec4b715c2e3dd4120baca3172cd81900b49fef857fb6d6afd24c983b608108b0a5ac0c1c6c52011f23b8778059ffadd1bb7cd06e2525417365f485a7fd1d4a9ba3818ede7cdc9e71afee8532252d08e2531ca52538655b7e8d912f7ec6d37bbcce8d7ec690709dbf9321e92c565b78e7fe2c22edf23e0902153d1ca15a112ad32fb19695ec65ce11ddf670da7915f05ad4b86c154fb908cb567315d1124f303f75fa075ebde8ef7bb12e27737ad9e4924439097338ea6d7a6fc3721b88c9b830a34e8d55f4c582b74a3895cc848fe57f4fe29f115dabeb6b3175be15d94408ed6771109cfaf57067ae658201082eae7605d26b1449af4425ae8e8f58cdda5c6265f1fd7a386fc6cea3074e4f25b909b96175883676f7610a00fdf34df9eb6c7b9a4ae89b839c69fd1f285e38cdceb634d782cc6d81179759bc9fd47d7fd060470d0b048287764c6837963274e708314f017ac7dc26d0554d59bfcfd3136225798f65f0b0fea337c6b256ebbb63a90b994c0ab93fd8b1d6bd4c74aebe535d6110014cd3d525394027dfe8faa98b4e9b2bee7949eb1961f1b026791092f84deea63afab66603dbe9b6365a102a1fef2f6b9744bc1bb091a8da9130d34d4d39f25dbad191649cfb67e10246364b7ce0c6ec072f9690cabb459d9fda0c849e17535de4357e9907270c75953fca3c845bb613926ecf73205219c7057a4b6bb244c184362bb4e2f24279dc4e60b94a5b1ec11c34081a628428ba5646c995b9558821053ba9c84a05afbf00dabd60223723096516d2f5668f3ec7e11612b01eb7a3a0506189a2272b88e89807943adb34291a17f6cb5516ffd6f945a1c42a524b21f096d66f350b1dad4db455741ae3d0e023309fbda5ef55fb0dc74f3297041448b2be76c525141963934c6afc53d263fb7836626df502d7c2ee9e79cbbd87afd84bbb8dfbf45248af3cd61ad5fac827e7683ca4f91dfad507a8eb9c17b2c9ac5ec051fe645a4a6cb37136f6f19b611e0ea8da7960af2d779507e55f57305bc74b7568928c5dd5132990fe54c22117df91c257d8c7b61935a018a28c1c3b17bab8e4294fa699161ec21123c9fc4e71079df31f300c2822e1246561e04765d3aab333eafd026c7431ac7616debb0e022746f4538e1c6348b600c988eeb2d051fc60c468dca260a84c79ab3ab8342dc345a764672848ea234e17332bc124799daf7c5fcb2e2358514a7461357e1c19c802c5ee32deccf1776885dd825bedd5f781d459984370a6b7ae885d4483a76ddb19b30f47ed47cd56aa5a079a89793dbcad461c59f2e002067ac98dd5a534e525c9c46c2af730741bf1f8629357ec0bfc0bc9ecb31af96777e507648ff4260dc3673716e098d9111dfd245f1d7c55a6de340deb8bd7a053e5d62d760f184dc70ca8fa255b9023b9b9aedfb6e419a5b5951ba0f83b603793830ee68d442d7b88ee1bbf6bbd1bcd6f68cc1af"); + assert_eq!(htlc_msat, 150_153_000); + assert_eq!(htlc_cltv, 800_060); +} diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 18e009e38ea..3006d312472 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -10386,7 +10386,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]), @@ -10762,6 +10762,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 b312e0055ee..33f2633502c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -668,7 +668,7 @@ impl HTLCSource { pub fn dummy() -> Self { assert!(cfg!(not(feature = "grind_signatures"))); HTLCSource::OutboundRoute { - path: Path { hops: Vec::new(), blinded_tail: None }, + path: Path { hops: Vec::new(), trampoline_hops: Vec::new(), blinded_tail: None }, session_priv: SecretKey::from_slice(&[1; 32]).unwrap(), first_hop_htlc_msat: 0, payment_id: PaymentId([2; 32]), @@ -12594,7 +12594,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 ac43efe4499..1a913b5fa24 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1097,7 +1097,7 @@ fn fake_network_test() { ).with_bolt11_features(nodes[1].node.bolt11_invoice_features()).unwrap(); let route_params = RouteParameters::from_payment_params_and_value(payment_params, 1000000); let payment_preimage_1 = send_along_route(&nodes[1], - Route { paths: vec![Path { hops, blinded_tail: None }], route_params: Some(route_params.clone()) }, + Route { paths: vec![Path { hops, trampoline_hops: vec![], blinded_tail: None }], route_params: Some(route_params.clone()) }, &vec!(&nodes[2], &nodes[3], &nodes[1])[..], 1000000).0; let mut hops = Vec::with_capacity(3); @@ -1131,7 +1131,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: Some(route_params) }, + Route { paths: vec![Path { hops, trampoline_hops: vec![], blinded_tail: None }], route_params: Some(route_params) }, &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 659ec65f6cf..09865c8837e 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -1802,7 +1802,6 @@ mod fuzzy_internal_msgs { amt_to_forward: u64, outgoing_cltv_value: u32, }, - #[allow(unused)] TrampolineEntrypoint { amt_to_forward: u64, outgoing_cltv_value: u32, @@ -1834,7 +1833,6 @@ mod fuzzy_internal_msgs { } pub(crate) enum OutboundTrampolinePayload<'a> { - #[allow(unused)] Forward { /// The value, in msat, of the payment after this hop's fee is deducted. amt_to_forward: u64, @@ -1854,12 +1852,10 @@ mod fuzzy_internal_msgs { /// If applicable, features of the BOLT12 invoice being paid. invoice_features: Option, }, - #[allow(unused)] BlindedForward { encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, }, - #[allow(unused)] BlindedReceive { sender_intended_htlc_amt_msat: u64, total_msat: u64, diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 193cdd1582a..a4a1c609c99 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), None @@ -558,6 +558,7 @@ mod tests { let path = Path { hops: hops, + trampoline_hops: vec![], blinded_tail: None, }; diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index abd930a9a91..6bd228b0226 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -19,11 +19,11 @@ use crate::ln::channel::EXPIRE_PREV_CONFIG_TICKS; use crate::ln::channelmanager::{HTLCForwardInfo, FailureCode, CLTV_FAR_FAR_AWAY, DISABLE_GOSSIP_TICKS, MIN_CLTV_EXPIRY_DELTA, PendingAddHTLCInfo, PendingHTLCInfo, PendingHTLCRouting, PaymentId, RecipientOnionFields}; use crate::ln::onion_utils; use crate::routing::gossip::{NetworkUpdate, RoutingFees}; -use crate::routing::router::{get_route, PaymentParameters, Route, RouteParameters, RouteHint, RouteHintHop}; +use crate::routing::router::{get_route, PaymentParameters, Route, RouteParameters, RouteHint, RouteHintHop, Path, TrampolineHop, BlindedTail, RouteHop}; use crate::types::features::{InitFeatures, Bolt11InvoiceFeatures}; use crate::ln::functional_test_utils::test_default_channel_config; use crate::ln::msgs; -use crate::ln::msgs::{ChannelMessageHandler, ChannelUpdate, OutboundTrampolinePayload}; +use crate::ln::msgs::{ChannelMessageHandler, ChannelUpdate, FinalOnionHopData, OutboundOnionPayload, OutboundTrampolinePayload}; use crate::ln::wire::Encode; use crate::util::ser::{Writeable, Writer, BigSize}; use crate::util::test_utils; @@ -40,9 +40,11 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use crate::io; use crate::prelude::*; -use bitcoin::hex::FromHex; - +use bitcoin::hex::{DisplayHex, FromHex}; +use types::features::{ChannelFeatures, Features, NodeFeatures}; +use crate::blinded_path::BlindedHop; use crate::ln::functional_test_utils::*; +use crate::ln::onion_utils::{construct_trampoline_onion_keys, construct_trampoline_onion_packet}; fn run_onion_failure_test(_name: &str, test_case: u8, nodes: &Vec, route: &Route, payment_hash: &PaymentHash, payment_secret: &PaymentSecret, callback_msg: F1, callback_node: F2, expected_retryable: bool, expected_error_code: Option, expected_channel_update: Option, expected_short_channel_id: Option) where F1: for <'a> FnMut(&'a mut msgs::UpdateAddHTLC), @@ -1001,6 +1003,145 @@ fn test_trampoline_onion_payload_serialization() { assert_eq!(carol_payload_hex, "2e020405f5e10004030c35000e2102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"); } +#[test] +fn test_trampoline_onion_payload_construction_vectors() { + // As per https://github.com/lightning/bolts/blob/fa0594ac2af3531d734f1d707a146d6e13679451/bolt04/trampoline-to-blinded-path-payment-onion-test.json#L251 + + let trampoline_payload_carol = OutboundTrampolinePayload::Forward { + amt_to_forward: 150_150_500, + outgoing_cltv_value: 800_036, + outgoing_node_id: PublicKey::from_slice(&>::from_hex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991").unwrap()).unwrap(), + }; + let carol_payload = trampoline_payload_carol.encode().to_lower_hex_string(); + assert_eq!(carol_payload, "2e020408f31d6404030c35240e21032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"); + + let trampoline_payload_dave = OutboundTrampolinePayload::BlindedForward { + encrypted_tlvs: &>::from_hex("0ccf3c8a58deaa603f657ee2a5ed9d604eb5c8ca1e5f801989afa8f3ea6d789bbdde2c7e7a1ef9ca8c38d2c54760febad8446d3f273ddb537569ef56613846ccd3aba78a").unwrap(), + intro_node_blinding_point: Some(PublicKey::from_slice(&>::from_hex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e").unwrap()).unwrap()), + }; + let dave_payload = trampoline_payload_dave.encode().to_lower_hex_string(); + assert_eq!(dave_payload, "690a440ccf3c8a58deaa603f657ee2a5ed9d604eb5c8ca1e5f801989afa8f3ea6d789bbdde2c7e7a1ef9ca8c38d2c54760febad8446d3f273ddb537569ef56613846ccd3aba78a0c2102988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e"); + + let trampoline_payload_eve = OutboundTrampolinePayload::BlindedReceive { + sender_intended_htlc_amt_msat: 150_000_000, + total_msat: 150_000_000, + cltv_expiry_height: 800_000, + encrypted_tlvs: &>::from_hex("bcd747394fbd4d99588da075a623316e15a576df5bc785cccc7cd6ec7b398acce6faf520175f9ec920f2ef261cdb83dc28cc3a0eeb970107b3306489bf771ef5b1213bca811d345285405861d08a655b6c237fa247a8b4491beee20c878a60e9816492026d8feb9dafa84585b253978db6a0aa2945df5ef445c61e801fb82f43d5f00716baf9fc9b3de50bc22950a36bda8fc27bfb1242e5860c7e687438d4133e058770361a19b6c271a2a07788d34dccc27e39b9829b061a4d960eac4a2c2b0f4de506c24f9af3868c0aff6dda27281c").unwrap(), + intro_node_blinding_point: None, + keysend_preimage: None, + custom_tlvs: &vec![], + }; + let eve_payload = trampoline_payload_eve.encode().to_lower_hex_string(); + assert_eq!(eve_payload, "e4020408f0d18004030c35000ad1bcd747394fbd4d99588da075a623316e15a576df5bc785cccc7cd6ec7b398acce6faf520175f9ec920f2ef261cdb83dc28cc3a0eeb970107b3306489bf771ef5b1213bca811d345285405861d08a655b6c237fa247a8b4491beee20c878a60e9816492026d8feb9dafa84585b253978db6a0aa2945df5ef445c61e801fb82f43d5f00716baf9fc9b3de50bc22950a36bda8fc27bfb1242e5860c7e687438d4133e058770361a19b6c271a2a07788d34dccc27e39b9829b061a4d960eac4a2c2b0f4de506c24f9af3868c0aff6dda27281c120408f0d180"); + + let trampoline_payloads = vec![trampoline_payload_carol, trampoline_payload_dave, trampoline_payload_eve]; + + let trampoline_session_key = SecretKey::from_slice(&>::from_hex("a64feb81abd58e473df290e9e1c07dc3e56114495cadf33191f44ba5448ebe99").unwrap()).unwrap(); + let associated_data_slice = SecretKey::from_slice(&>::from_hex("e89bc505e84aaca09613833fc58c9069078fb43bfbea0488f34eec9db99b5f82").unwrap()).unwrap(); + let associated_data = PaymentHash(associated_data_slice.secret_bytes()); + + let trampoline_hops = Path { + hops: vec![], + trampoline_hops: vec![ + // Carol's pubkey + TrampolineHop { + pubkey: PublicKey::from_slice(&>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(), + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + }, + // Dave's pubkey (the intro node needs to be duplicated) + TrampolineHop { + pubkey: PublicKey::from_slice(&>::from_hex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991").unwrap()).unwrap(), + node_features: Features::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + } + ], + blinded_tail: Some(BlindedTail { + hops: vec![ + BlindedHop { + blinded_node_id: PublicKey::from_slice(&>::from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be").unwrap()).unwrap(), + encrypted_payload: vec![], + }, + BlindedHop { + blinded_node_id: PublicKey::from_slice(&>::from_hex("020e2dbadcc2005e859819ddebbe88a834ae8a6d2b049233c07335f15cd1dc5f22").unwrap()).unwrap(), + encrypted_payload: vec![], + } + ], + blinding_point: PublicKey::from_slice(&>::from_hex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e").unwrap()).unwrap(), + excess_final_cltv_expiry_delta: 0, + final_value_msat: 0 + }), + }; + + let trampoline_onion_keys = construct_trampoline_onion_keys(&Secp256k1::new(), &trampoline_hops, &trampoline_session_key).unwrap(); + let trampoline_onion_packet = construct_trampoline_onion_packet(trampoline_payloads, trampoline_onion_keys, [0u8; 32], &associated_data, None).unwrap(); + let trampoline_onion_packet_hex = trampoline_onion_packet.encode().to_lower_hex_string(); + assert_eq!(trampoline_onion_packet_hex, "0002bc59a9abc893d75a8d4f56a6572f9a3507323a8de22abe0496ea8d37da166a8b4bba0e560f1a9deb602bfd98fe9167141d0b61d669df90c0149096d505b85d3d02806e6c12caeb308b878b6bc7f1b15839c038a6443cd3bec3a94c2293165375555f6d7720862b525930f41fddcc02260d197abd93fb58e60835fd97d9dc14e7979c12f59df08517b02e3e4d50e1817de4271df66d522c4e9675df71c635c4176a8381bc22b342ff4e9031cede87f74cc039fca74aa0a3786bc1db2e158a9a520ecb99667ef9a6bbfaf5f0e06f81c27ca48134ba2103229145937c5dc7b8ecc5201d6aeb592e78faa3c05d3a035df77628f0be9b1af3ef7d386dd5cc87b20778f47ebd40dbfcf12b9071c5d7112ab84c3e0c5c14867e684d09a18bc93ac47d73b7343e3403ef6e3b70366835988920e7d772c3719d3596e53c29c4017cb6938421a557ce81b4bb26701c25bf622d4c69f1359dc85857a375c5c74987a4d3152f66987001c68a50c4bf9e0b1dab4ad1a64b0535319bbf6c4fbe4f9c50cb65f5ef887bfb91b0a57c0f86ba3d91cbeea1607fb0c12c6c75d03bbb0d3a3019c40597027f5eebca23083e50ec79d41b1152131853525bf3fc13fb0be62c2e3ce733f59671eee5c4064863fb92ae74be9ca68b9c716f9519fd268478ee27d91d466b0de51404de3226b74217d28250ead9d2c95411e0230570f547d4cc7c1d589791623131aa73965dccc5aa17ec12b442215ce5d346df664d799190df5dd04a13"); + + let outer_payloads = vec![ + // Bob + OutboundOnionPayload::Forward { + short_channel_id: (572330 << 40) + (42 << 16) + 2821, + amt_to_forward: 150153000, + outgoing_cltv_value: 800060, + }, + + // Carol + OutboundOnionPayload::TrampolineEntrypoint { + amt_to_forward: 150153000, + outgoing_cltv_value: 800060, + trampoline_packet: trampoline_onion_packet, + multipath_trampoline_data: Some(FinalOnionHopData{ + payment_secret: PaymentSecret(SecretKey::from_slice(&>::from_hex("7494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da").unwrap()).unwrap().secret_bytes()), + total_msat: 150153000 + }), + } + ]; + + let outer_hops = Path { + hops: vec![ + // Bob + RouteHop { + pubkey: PublicKey::from_slice(&>::from_hex("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()).unwrap(), + node_features: NodeFeatures::empty(), + short_channel_id: 0, + channel_features: ChannelFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + maybe_announced_channel: false, + }, + + // Carol + RouteHop { + pubkey: PublicKey::from_slice(&>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(), + node_features: NodeFeatures::empty(), + short_channel_id: 0, + channel_features: ChannelFeatures::empty(), + fee_msat: 0, + cltv_expiry_delta: 0, + maybe_announced_channel: false, + }, + ], + trampoline_hops: vec![], + blinded_tail: None, + }; + + let bob_payload = outer_payloads[0].encode().to_lower_hex_string(); + assert_eq!(bob_payload, "15020408f3272804030c353c060808bbaa00002a0b05"); + + let carol_payload = outer_payloads[1].encode().to_lower_hex_string(); + assert_eq!(carol_payload, "fd0255020408f3272804030c353c08247494b65bc092b48a75465e43e29be807eb2cc535ce8aaba31012b8ff1ceac5da08f3272814fd02200002bc59a9abc893d75a8d4f56a6572f9a3507323a8de22abe0496ea8d37da166a8b4bba0e560f1a9deb602bfd98fe9167141d0b61d669df90c0149096d505b85d3d02806e6c12caeb308b878b6bc7f1b15839c038a6443cd3bec3a94c2293165375555f6d7720862b525930f41fddcc02260d197abd93fb58e60835fd97d9dc14e7979c12f59df08517b02e3e4d50e1817de4271df66d522c4e9675df71c635c4176a8381bc22b342ff4e9031cede87f74cc039fca74aa0a3786bc1db2e158a9a520ecb99667ef9a6bbfaf5f0e06f81c27ca48134ba2103229145937c5dc7b8ecc5201d6aeb592e78faa3c05d3a035df77628f0be9b1af3ef7d386dd5cc87b20778f47ebd40dbfcf12b9071c5d7112ab84c3e0c5c14867e684d09a18bc93ac47d73b7343e3403ef6e3b70366835988920e7d772c3719d3596e53c29c4017cb6938421a557ce81b4bb26701c25bf622d4c69f1359dc85857a375c5c74987a4d3152f66987001c68a50c4bf9e0b1dab4ad1a64b0535319bbf6c4fbe4f9c50cb65f5ef887bfb91b0a57c0f86ba3d91cbeea1607fb0c12c6c75d03bbb0d3a3019c40597027f5eebca23083e50ec79d41b1152131853525bf3fc13fb0be62c2e3ce733f59671eee5c4064863fb92ae74be9ca68b9c716f9519fd268478ee27d91d466b0de51404de3226b74217d28250ead9d2c95411e0230570f547d4cc7c1d589791623131aa73965dccc5aa17ec12b442215ce5d346df664d799190df5dd04a13"); + + let outer_session_key = SecretKey::from_slice(&>::from_hex("4f777e8dac16e6dfe333066d9efb014f7a51d11762ff76eca4d3a95ada99ba3e").unwrap()).unwrap(); + let outer_onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &outer_hops, &outer_session_key).unwrap(); + let outer_onion_prng_seed = onion_utils::gen_pad_from_shared_secret(&outer_session_key.secret_bytes()); + let outer_onion_packet = onion_utils::construct_onion_packet(outer_payloads, outer_onion_keys, outer_onion_prng_seed, &associated_data).unwrap(); + let outer_onion_packet_hex = outer_onion_packet.encode().to_lower_hex_string(); + assert_eq!(outer_onion_packet_hex, "00025fd60556c134ae97e4baedba220a644037754ee67c54fd05e93bf40c17cbb73362fb9dee96001ff229945595b6edb59437a6bc143406d3f90f749892a84d8d430c6890437d26d5bfc599d565316ef51347521075bbab87c59c57bcf20af7e63d7192b46cf171e4f73cb11f9f603915389105d91ad630224bea95d735e3988add1e24b5bf28f1d7128db64284d90a839ba340d088c74b1fb1bd21136b1809428ec5399c8649e9bdf92d2dcfc694deae5046fa5b2bdf646847aaad73f5e95275763091c90e71031cae1f9a770fdea559642c9c02f424a2a28163dd0957e3874bd28a97bec67d18c0321b0e68bc804aa8345b17cb626e2348ca06c8312a167c989521056b0f25c55559d446507d6c491d50605cb79fa87929ce64b0a9860926eeaec2c431d926a1cadb9a1186e4061cb01671a122fc1f57602cbef06d6c194ec4b715c2e3dd4120baca3172cd81900b49fef857fb6d6afd24c983b608108b0a5ac0c1c6c52011f23b8778059ffadd1bb7cd06e2525417365f485a7fd1d4a9ba3818ede7cdc9e71afee8532252d08e2531ca52538655b7e8d912f7ec6d37bbcce8d7ec690709dbf9321e92c565b78e7fe2c22edf23e0902153d1ca15a112ad32fb19695ec65ce11ddf670da7915f05ad4b86c154fb908cb567315d1124f303f75fa075ebde8ef7bb12e27737ad9e4924439097338ea6d7a6fc3721b88c9b830a34e8d55f4c582b74a3895cc848fe57f4fe29f115dabeb6b3175be15d94408ed6771109cfaf57067ae658201082eae7605d26b1449af4425ae8e8f58cdda5c6265f1fd7a386fc6cea3074e4f25b909b96175883676f7610a00fdf34df9eb6c7b9a4ae89b839c69fd1f285e38cdceb634d782cc6d81179759bc9fd47d7fd060470d0b048287764c6837963274e708314f017ac7dc26d0554d59bfcfd3136225798f65f0b0fea337c6b256ebbb63a90b994c0ab93fd8b1d6bd4c74aebe535d6110014cd3d525394027dfe8faa98b4e9b2bee7949eb1961f1b026791092f84deea63afab66603dbe9b6365a102a1fef2f6b9744bc1bb091a8da9130d34d4d39f25dbad191649cfb67e10246364b7ce0c6ec072f9690cabb459d9fda0c849e17535de4357e9907270c75953fca3c845bb613926ecf73205219c7057a4b6bb244c184362bb4e2f24279dc4e60b94a5b1ec11c34081a628428ba5646c995b9558821053ba9c84a05afbf00dabd60223723096516d2f5668f3ec7e11612b01eb7a3a0506189a2272b88e89807943adb34291a17f6cb5516ffd6f945a1c42a524b21f096d66f350b1dad4db455741ae3d0e023309fbda5ef55fb0dc74f3297041448b2be76c525141963934c6afc53d263fb7836626df502d7c2ee9e79cbbd87afd84bbb8dfbf45248af3cd61ad5fac827e7683ca4f91dfad507a8eb9c17b2c9ac5ec051fe645a4a6cb37136f6f19b611e0ea8da7960af2d779507e55f57305bc74b7568928c5dd5132990fe54c22117df91c257d8c7b61935a018a28c1c3b17bab8e4294fa699161ec21123c9fc4e71079df31f300c2822e1246561e04765d3aab333eafd026c7431ac7616debb0e022746f4538e1c6348b600c988eeb2d051fc60c468dca260a84c79ab3ab8342dc345a764672848ea234e17332bc124799daf7c5fcb2e2358514a7461357e1c19c802c5ee32deccf1776885dd825bedd5f781d459984370a6b7ae885d4483a76ddb19b30f47ed47cd56aa5a079a89793dbcad461c59f2e002067ac98dd5a534e525c9c46c2af730741bf1f8629357ec0bfc0bc9ecb31af96777e507648ff4260dc3673716e098d9111dfd245f1d7c55a6de340deb8bd7a053e5d62d760f184dc70ca8fa255b9023b9b9aedfb6e419a5b5951ba0f83b603793830ee68d442d7b88ee1bbf6bbd1bcd6f68cc1af"); +} + fn do_test_fail_htlc_backwards_with_reason(failure_code: FailureCode) { let chanmon_cfgs = create_chanmon_cfgs(2); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 960209c0e0a..133773395f7 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -15,7 +15,7 @@ use crate::ln::channelmanager::{HTLCSource, RecipientOnionFields}; use crate::ln::msgs; use crate::offers::invoice_request::InvoiceRequest; use crate::routing::gossip::NetworkUpdate; -use crate::routing::router::{Path, RouteHop, RouteParameters}; +use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters, TrampolineHop}; use crate::sign::NodeSigner; use crate::types::features::{ChannelFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage}; @@ -33,10 +33,11 @@ use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; use crate::io::{Cursor, Read}; -use core::ops::Deref; - +use crate::ln::msgs::{FinalOnionHopData, OutboundOnionPayload}; #[allow(unused_imports)] use crate::prelude::*; +use core::ops::Deref; +use types::payment::PaymentSecret; pub(crate) struct OnionKeys { #[cfg(test)] @@ -109,26 +110,180 @@ pub(crate) fn next_hop_pubkey( curr_pubkey.mul_tweak(secp_ctx, &Scalar::from_be_bytes(blinding_factor).unwrap()) } -// can only fail if an intermediary hop has an invalid public key or session_priv is invalid +pub(super) trait HopInfo { + fn node_pubkey(&self) -> &PublicKey; +} + +trait PathHop { + type HopId; + fn hop_id(&self) -> Self::HopId; + fn fee_msat(&self) -> u64; + fn cltv_expiry_delta(&self) -> u32; +} + +impl HopInfo for RouteHop { + fn node_pubkey(&self) -> &PublicKey { + &self.pubkey + } +} + +impl<'a> PathHop for &'a RouteHop { + type HopId = u64; // scid + + fn hop_id(&self) -> Self::HopId { + self.short_channel_id + } + + fn fee_msat(&self) -> u64 { + self.fee_msat + } + + fn cltv_expiry_delta(&self) -> u32 { + self.cltv_expiry_delta + } +} + +impl HopInfo for TrampolineHop { + fn node_pubkey(&self) -> &PublicKey { + &self.pubkey + } +} + +impl<'a> PathHop for &'a TrampolineHop { + type HopId = PublicKey; + + fn hop_id(&self) -> Self::HopId { + self.pubkey + } + + fn fee_msat(&self) -> u64 { + self.fee_msat + } + + fn cltv_expiry_delta(&self) -> u32 { + self.cltv_expiry_delta + } +} + +trait OnionPayload<'a, 'b> { + type PathHopForId: PathHop + 'b; + fn new_forward( + hop_id: <>::PathHopForId as PathHop>::HopId, + amt_to_forward: u64, outgoing_cltv_value: u32, + ) -> Self; + fn new_receive( + recipient_onion: &'a RecipientOnionFields, keysend_preimage: Option, + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + ) -> Self; + fn new_blinded_forward( + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + ) -> Self; + fn new_blinded_receive( + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + keysend_preimage: Option, invoice_request: Option<&'a InvoiceRequest>, + custom_tlvs: &'a Vec<(u64, Vec)>, + ) -> Self; +} +impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { + type PathHopForId = &'b RouteHop; + fn new_forward(short_channel_id: u64, amt_to_forward: u64, outgoing_cltv_value: u32) -> Self { + Self::Forward { short_channel_id, amt_to_forward, outgoing_cltv_value } + } + fn new_receive( + recipient_onion: &'a RecipientOnionFields, keysend_preimage: Option, + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + ) -> Self { + Self::Receive { + payment_data: recipient_onion + .payment_secret + .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }), + payment_metadata: recipient_onion.payment_metadata.as_ref(), + keysend_preimage, + custom_tlvs: &recipient_onion.custom_tlvs, + sender_intended_htlc_amt_msat, + cltv_expiry_height, + } + } + fn new_blinded_forward( + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + ) -> Self { + Self::BlindedForward { encrypted_tlvs, intro_node_blinding_point } + } + fn new_blinded_receive( + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + keysend_preimage: Option, invoice_request: Option<&'a InvoiceRequest>, + custom_tlvs: &'a Vec<(u64, Vec)>, + ) -> Self { + Self::BlindedReceive { + sender_intended_htlc_amt_msat, + total_msat, + cltv_expiry_height, + encrypted_tlvs, + intro_node_blinding_point, + keysend_preimage, + invoice_request, + custom_tlvs, + } + } +} +impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { + type PathHopForId = &'b TrampolineHop; + fn new_forward( + outgoing_node_id: PublicKey, amt_to_forward: u64, outgoing_cltv_value: u32, + ) -> Self { + Self::Forward { outgoing_node_id, amt_to_forward, outgoing_cltv_value } + } + fn new_receive( + _recipient_onion: &'a RecipientOnionFields, _keysend_preimage: Option, + _sender_intended_htlc_amt_msat: u64, _total_msat: u64, _cltv_expiry_height: u32, + ) -> Self { + todo!() + } + fn new_blinded_forward( + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + ) -> Self { + Self::BlindedForward { encrypted_tlvs, intro_node_blinding_point } + } + fn new_blinded_receive( + sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + encrypted_tlvs: &'a Vec, intro_node_blinding_point: Option, + keysend_preimage: Option, _invoice_request: Option<&'a InvoiceRequest>, + custom_tlvs: &'a Vec<(u64, Vec)>, + ) -> Self { + Self::BlindedReceive { + sender_intended_htlc_amt_msat, + total_msat, + cltv_expiry_height, + encrypted_tlvs, + intro_node_blinding_point, + keysend_preimage, + custom_tlvs, + } + } +} + #[inline] -pub(super) fn construct_onion_keys_callback( - secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, mut callback: FType, +pub(super) fn construct_onion_keys_generic_callback( + secp_ctx: &Secp256k1, hops: &[H], blinded_tail: Option<&BlindedTail>, + session_priv: &SecretKey, mut callback: FType, ) -> Result<(), secp256k1::Error> where T: secp256k1::Signing, - FType: FnMut(SharedSecret, [u8; 32], PublicKey, Option<&RouteHop>, usize), + H: HopInfo, + FType: FnMut(SharedSecret, [u8; 32], PublicKey, Option<&H>, usize), { let mut blinded_priv = session_priv.clone(); let mut blinded_pub = PublicKey::from_secret_key(secp_ctx, &blinded_priv); - let unblinded_hops_iter = path.hops.iter().map(|h| (&h.pubkey, Some(h))); - let blinded_pks_iter = path - .blinded_tail - .as_ref() + let unblinded_hops_iter = hops.iter().map(|h| (h.node_pubkey(), Some(h))); + let blinded_pks_iter = blinded_tail .map(|t| t.hops.iter()) .unwrap_or([].iter()) .skip(1) // Skip the intro node because it's included in the unblinded hops .map(|h| (&h.blinded_node_id, None)); + for (idx, (pubkey, route_hop_opt)) in unblinded_hops_iter.chain(blinded_pks_iter).enumerate() { let shared_secret = SharedSecret::new(pubkey, &blinded_priv); @@ -154,9 +309,39 @@ pub(super) fn construct_onion_keys( ) -> Result, secp256k1::Error> { let mut res = Vec::with_capacity(path.hops.len()); - construct_onion_keys_callback( + construct_onion_keys_generic_callback( secp_ctx, - &path, + &path.hops, + path.blinded_tail.as_ref(), + session_priv, + |shared_secret, _blinding_factor, ephemeral_pubkey, _, _| { + let (rho, mu) = gen_rho_mu_from_shared_secret(shared_secret.as_ref()); + + res.push(OnionKeys { + #[cfg(test)] + shared_secret, + #[cfg(test)] + blinding_factor: _blinding_factor, + ephemeral_pubkey, + rho, + mu, + }); + }, + )?; + + Ok(res) +} + +// can only fail if an intermediary hop has an invalid public key or session_priv is invalid +pub(super) fn construct_trampoline_onion_keys( + secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, +) -> Result, secp256k1::Error> { + let mut res = Vec::with_capacity(path.trampoline_hops.len()); + + construct_onion_keys_generic_callback( + secp_ctx, + &path.trampoline_hops, + path.blinded_tail.as_ref(), session_priv, |shared_secret, _blinding_factor, ephemeral_pubkey, _, _| { let (rho, mu) = gen_rho_mu_from_shared_secret(shared_secret.as_ref()); @@ -176,6 +361,39 @@ pub(super) fn construct_onion_keys( Ok(res) } +fn build_trampoline_onion_payloads<'a>( + path: &'a Path, total_msat: u64, recipient_onion: &'a RecipientOnionFields, + starting_htlc_offset: u32, keysend_preimage: &Option, +) -> Result<(Vec>, u64, u32), APIError> { + let mut res: Vec = Vec::with_capacity( + path.trampoline_hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), + ); + let blinded_tail = path.blinded_tail.as_ref().ok_or(APIError::InvalidRoute { + err: "Routes using Trampoline must terminate blindly.".to_string(), + })?; + let blinded_tail_with_hop_iter = BlindedTailHopIter { + hops: blinded_tail.hops.iter(), + blinding_point: blinded_tail.blinding_point, + final_value_msat: blinded_tail.final_value_msat, + excess_final_cltv_expiry_delta: blinded_tail.excess_final_cltv_expiry_delta, + }; + + let (value_msat, cltv) = build_onion_payloads_callback( + path.trampoline_hops.iter(), + Some(blinded_tail_with_hop_iter), + total_msat, + recipient_onion, + starting_htlc_offset, + keysend_preimage, + None, + |action, payload| match action { + PayloadCallbackAction::PushBack => res.push(payload), + PayloadCallbackAction::PushFront => res.insert(0, payload), + }, + )?; + Ok((res, value_msat, cltv)) +} + /// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. pub(super) fn build_onion_payloads<'a>( path: &'a Path, total_msat: u64, recipient_onion: &'a RecipientOnionFields, @@ -185,12 +403,22 @@ pub(super) fn build_onion_payloads<'a>( let mut res: Vec = Vec::with_capacity( path.hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()), ); - let blinded_tail_with_hop_iter = path.blinded_tail.as_ref().map(|bt| BlindedTailHopIter { - hops: bt.hops.iter(), - blinding_point: bt.blinding_point, - final_value_msat: bt.final_value_msat, - excess_final_cltv_expiry_delta: bt.excess_final_cltv_expiry_delta, - }); + + // When Trampoline hops are present, they are presumed to follow the non-Trampoline hops, which + // means that the blinded path needs not be appended to the regular hops, and is only included + // among the Trampoline onion payloads. + let blinded_tail_with_hop_iter = path + .trampoline_hops + .is_empty() + .then(|| { + path.blinded_tail.as_ref().map(|bt| BlindedTailHopIter { + hops: bt.hops.iter(), + blinding_point: bt.blinding_point, + final_value_msat: bt.final_value_msat, + excess_final_cltv_expiry_delta: bt.excess_final_cltv_expiry_delta, + }) + }) + .flatten(); let (value_msat, cltv) = build_onion_payloads_callback( path.hops.iter(), @@ -218,28 +446,29 @@ enum PayloadCallbackAction { PushBack, PushFront, } -fn build_onion_payloads_callback<'a, H, B, F>( +fn build_onion_payloads_callback<'a, 'b, H, B, F, OP>( hops: H, mut blinded_tail: Option>, total_msat: u64, recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, keysend_preimage: &Option, invoice_request: Option<&'a InvoiceRequest>, mut callback: F, ) -> Result<(u64, u32), APIError> where - H: DoubleEndedIterator, + H: DoubleEndedIterator, B: ExactSizeIterator, - F: FnMut(PayloadCallbackAction, msgs::OutboundOnionPayload<'a>), + F: FnMut(PayloadCallbackAction, OP), + OP: OnionPayload<'a, 'b>, { let mut cur_value_msat = 0u64; let mut cur_cltv = starting_htlc_offset; - let mut last_short_channel_id = 0; + let mut last_hop_id = None; for (idx, hop) in hops.rev().enumerate() { // First hop gets special values so that it can check, on receipt, that everything is // exactly as it should be (and the next hop isn't trying to probe to find out if we're // the intended recipient). - let value_msat = if cur_value_msat == 0 { hop.fee_msat } else { cur_value_msat }; + let value_msat = if cur_value_msat == 0 { hop.fee_msat() } else { cur_value_msat }; let cltv = if cur_cltv == starting_htlc_offset { - hop.cltv_expiry_delta + starting_htlc_offset + hop.cltv_expiry_delta() + starting_htlc_offset } else { cur_cltv }; @@ -259,59 +488,52 @@ where cur_value_msat += final_value_msat; callback( PayloadCallbackAction::PushBack, - msgs::OutboundOnionPayload::BlindedReceive { - sender_intended_htlc_amt_msat: final_value_msat, + OP::new_blinded_receive( + final_value_msat, total_msat, - cltv_expiry_height: cur_cltv + excess_final_cltv_expiry_delta, - encrypted_tlvs: &blinded_hop.encrypted_payload, - intro_node_blinding_point: blinding_point.take(), - keysend_preimage: *keysend_preimage, + cur_cltv + excess_final_cltv_expiry_delta, + &blinded_hop.encrypted_payload, + blinding_point.take(), + *keysend_preimage, invoice_request, - custom_tlvs: &recipient_onion.custom_tlvs, - }, + &recipient_onion.custom_tlvs, + ), ); } else { callback( PayloadCallbackAction::PushBack, - msgs::OutboundOnionPayload::BlindedForward { - encrypted_tlvs: &blinded_hop.encrypted_payload, - intro_node_blinding_point: blinding_point.take(), - }, + OP::new_blinded_forward( + &blinded_hop.encrypted_payload, + blinding_point.take(), + ), ); } } } else { callback( PayloadCallbackAction::PushBack, - msgs::OutboundOnionPayload::Receive { - payment_data: recipient_onion.payment_secret.map(|payment_secret| { - msgs::FinalOnionHopData { payment_secret, total_msat } - }), - payment_metadata: recipient_onion.payment_metadata.as_ref(), - keysend_preimage: *keysend_preimage, - custom_tlvs: &recipient_onion.custom_tlvs, - sender_intended_htlc_amt_msat: value_msat, - cltv_expiry_height: cltv, - }, + OP::new_receive( + &recipient_onion, + *keysend_preimage, + value_msat, + total_msat, + cltv, + ), ); } } else { - let payload = msgs::OutboundOnionPayload::Forward { - short_channel_id: last_short_channel_id, - amt_to_forward: value_msat, - outgoing_cltv_value: cltv, - }; + let payload = OP::new_forward(last_hop_id.unwrap(), value_msat, cltv); callback(PayloadCallbackAction::PushFront, payload); } - cur_value_msat += hop.fee_msat; + cur_value_msat += hop.fee_msat(); if cur_value_msat >= 21000000 * 100000000 * 1000 { return Err(APIError::InvalidRoute { err: "Channel fees overflowed?".to_owned() }); } - cur_cltv += hop.cltv_expiry_delta as u32; + cur_cltv += hop.cltv_expiry_delta() as u32; if cur_cltv >= 500000000 { return Err(APIError::InvalidRoute { err: "Channel CLTV overflowed?".to_owned() }); } - last_short_channel_id = hop.short_channel_id; + last_hop_id = Some(hop.hop_id()); } Ok((cur_value_msat, cur_cltv)) } @@ -369,7 +591,7 @@ pub(crate) fn set_max_path_length( best_block_height, &keysend_preimage, invoice_request, - |_, payload| { + |_, payload: msgs::OutboundOnionPayload| { num_reserved_bytes = num_reserved_bytes .saturating_add(payload.serialized_length()) .saturating_add(PAYLOAD_HMAC_LEN); @@ -422,15 +644,26 @@ pub(super) fn construct_onion_packet( ) } -#[allow(unused)] pub(super) fn construct_trampoline_onion_packet( payloads: Vec, onion_keys: Vec, - prng_seed: [u8; 32], associated_data: &PaymentHash, length: u16, + prng_seed: [u8; 32], associated_data: &PaymentHash, length: Option, ) -> Result { - let mut packet_data = vec![0u8; length as usize]; + let minimum_packet_length: usize = payloads.iter().map(|p| p.serialized_length() + 32).sum(); + + assert!( + minimum_packet_length < ONION_DATA_LEN, + "Trampoline onion packet must be smaller than outer onion" + ); + let packet_length = length.unwrap_or(minimum_packet_length as u16) as usize; + assert!( + packet_length >= minimum_packet_length, + "Packet length cannot be smaller than the payloads require." + ); + + let mut packet_data = vec![0u8; packet_length]; let mut chacha = ChaCha20::new(&prng_seed, &[0; 8]); - chacha.process(&vec![0u8; length as usize], &mut packet_data); + chacha.process_in_place(&mut packet_data); construct_onion_packet_with_init_noise::<_, _>( payloads, @@ -880,8 +1113,14 @@ where } }; - construct_onion_keys_callback(secp_ctx, &path, session_priv, callback) - .expect("Route that we sent via spontaneously grew invalid keys in the middle of it?"); + construct_onion_keys_generic_callback( + secp_ctx, + &path.hops, + path.blinded_tail.as_ref(), + session_priv, + callback, + ) + .expect("Route that we sent via spontaneously grew invalid keys in the middle of it?"); if let Some(FailureLearnings { network_update, @@ -1167,21 +1406,124 @@ pub fn create_payment_onion( keysend_preimage: &Option, invoice_request: Option<&InvoiceRequest>, prng_seed: [u8; 32], ) -> Result<(msgs::OnionPacket, u64, u32), APIError> { - let onion_keys = construct_onion_keys(&secp_ctx, &path, &session_priv).map_err(|_| { - APIError::InvalidRoute { err: "Pubkey along hop was maliciously selected".to_owned() } - })?; - let (onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( - &path, + create_payment_onion_internal( + secp_ctx, + path, + session_priv, total_msat, recipient_onion, cur_block_height, + payment_hash, keysend_preimage, invoice_request, - )?; - let onion_packet = construct_onion_packet(onion_payloads, onion_keys, prng_seed, payment_hash) + prng_seed, + None, + None, + None, + ) +} + +/// Build a payment onion, returning the first hop msat and cltv values as well. +/// `cur_block_height` should be set to the best known block height + 1. +pub(crate) fn create_payment_onion_internal( + secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, total_msat: u64, + recipient_onion: &RecipientOnionFields, cur_block_height: u32, payment_hash: &PaymentHash, + keysend_preimage: &Option, invoice_request: Option<&InvoiceRequest>, + prng_seed: [u8; 32], secondary_payment_secret: Option, + secondary_session_priv: Option, secondary_prng_seed: Option<[u8; 32]>, +) -> Result<(msgs::OnionPacket, u64, u32), APIError> { + let mut outer_total_msat = total_msat; + let mut outer_starting_htlc_offset = cur_block_height; + let mut outer_session_priv_override = None; + let mut trampoline_packet_option = None; + + if !path.trampoline_hops.is_empty() { + let trampoline_payloads; + (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = + build_trampoline_onion_payloads( + path, + total_msat, + recipient_onion, + cur_block_height, + keysend_preimage, + )?; + + let onion_keys = + construct_trampoline_onion_keys(&secp_ctx, &path, &session_priv).map_err(|_| { + APIError::InvalidRoute { + err: "Pubkey along hop was maliciously selected".to_owned(), + } + })?; + let trampoline_packet = construct_trampoline_onion_packet( + trampoline_payloads, + onion_keys, + prng_seed, + payment_hash, + None, + ) .map_err(|_| APIError::InvalidRoute { err: "Route size too large considering onion data".to_owned(), })?; + + trampoline_packet_option = Some(trampoline_packet); + } + + let (mut onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( + &path, + outer_total_msat, + recipient_onion, + outer_starting_htlc_offset, + keysend_preimage, + invoice_request, + )?; + + if !path.trampoline_hops.is_empty() { + let last_payload = onion_payloads.pop().ok_or(APIError::InvalidRoute { + err: "Non-Trampoline path needs at least one hop".to_owned(), + })?; + + match last_payload { + OutboundOnionPayload::Receive { payment_data, .. } => { + let fee_delta = path.hops.last().map_or(0, |h| h.fee_msat); + let cltv_delta = path.hops.last().map_or(0, |h| h.cltv_expiry_delta); + let multipath_trampoline_data = payment_data.map(|d| { + let trampoline_payment_secret = secondary_payment_secret.unwrap_or_else(|| { + PaymentSecret(Sha256::hash(&d.payment_secret.0).to_byte_array()) + }); + let total_msat = fee_delta; + FinalOnionHopData { payment_secret: trampoline_payment_secret, total_msat } + }); + onion_payloads.push(OutboundOnionPayload::TrampolineEntrypoint { + amt_to_forward: fee_delta, + outgoing_cltv_value: outer_starting_htlc_offset + cltv_delta, + multipath_trampoline_data, + trampoline_packet: trampoline_packet_option.unwrap(), + }); + }, + _ => { + return Err(APIError::InvalidRoute { + err: "Last non-Trampoline hop must be of type OutboundOnionPayload::Receive" + .to_owned(), + }); + }, + }; + + outer_session_priv_override = Some(secondary_session_priv.unwrap_or_else(|| { + let session_priv_hash = Sha256::hash(&session_priv.secret_bytes()).to_byte_array(); + SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!") + })); + } + + let outer_session_priv = outer_session_priv_override.as_ref().unwrap_or(session_priv); + let onion_keys = construct_onion_keys(&secp_ctx, &path, outer_session_priv).map_err(|_| { + APIError::InvalidRoute { err: "Pubkey along hop was maliciously selected".to_owned() } + })?; + let outer_onion_prng_seed = secondary_prng_seed.unwrap_or(prng_seed); + let onion_packet = + construct_onion_packet(onion_payloads, onion_keys, outer_onion_prng_seed, payment_hash) + .map_err(|_| APIError::InvalidRoute { + err: "Route size too large considering onion data".to_owned(), + })?; Ok((onion_packet, htlc_msat, htlc_cltv)) } @@ -1330,7 +1672,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 80d93387ac3..df589db26cd 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -2553,7 +2553,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())); @@ -2909,6 +2909,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 0c9c5d0e920..cd65c21d300 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -2441,7 +2441,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(), @@ -2450,7 +2450,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()), }; @@ -2472,7 +2472,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(), @@ -2481,7 +2481,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()), }; @@ -2503,7 +2503,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()), }; @@ -2648,7 +2648,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()), }; @@ -2746,7 +2746,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(), @@ -2755,7 +2755,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()), }; @@ -2837,7 +2837,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()), }; @@ -2931,7 +2931,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(), @@ -2948,7 +2948,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()), }; @@ -3136,7 +3136,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(), @@ -3153,7 +3153,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()), }; @@ -3307,7 +3307,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(), @@ -3324,7 +3324,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 78a93aa0d39..b39b4f542b1 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -389,6 +389,35 @@ 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 fee taken on this hop (for paying for the use of the *next* channel in the path). + /// If this is the last hop in [`Path::hops`]: + /// * if we're sending to a [`BlindedPaymentPath`], this is the fee paid for use of the entire + /// blinded path + /// * otherwise, this is the full value of this [`Path`]'s part of the payment + pub fee_msat: u64, + /// The CLTV delta added for this hop. + /// If this is the last hop in [`Path::hops`]: + /// * if we're sending to a [`BlindedPaymentPath`], this is the CLTV delta for the entire + /// blinded path + /// * otherwise, this is the CLTV delta expected at the destination + pub cltv_expiry_delta: u32, +} + +impl_writeable_tlv_based!(TrampolineHop, { + (0, pubkey, required), + (2, node_features, required), + (6, fee_msat, required), + (8, cltv_expiry_delta, required), +}); + /// The blinded portion of a [`Path`], if we're routing to a recipient who provided blinded paths in /// their [`Bolt12Invoice`]. /// @@ -419,6 +448,8 @@ 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. When using Trampoline, must contain at least one 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, } @@ -551,7 +582,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)), @@ -3352,7 +3383,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()); @@ -7076,7 +7107,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, }; @@ -7098,7 +7129,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(), @@ -7109,7 +7140,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, }; @@ -7684,6 +7715,7 @@ mod tests { cltv_expiry_delta: 0, maybe_announced_channel: true, }], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: vec![ BlindedHop { blinded_node_id: ln_test_utils::pubkey(44), encrypted_payload: Vec::new() }, @@ -7701,7 +7733,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(); @@ -7749,6 +7781,7 @@ mod tests { cltv_expiry_delta: 0, maybe_announced_channel: false, }], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: vec![BlindedHop { blinded_node_id: ln_test_utils::pubkey(49), encrypted_payload: Vec::new() }], blinding_point: ln_test_utils::pubkey(48), @@ -7784,6 +7817,7 @@ mod tests { maybe_announced_channel: false, } ], + trampoline_hops: vec![], blinded_tail: Some(BlindedTail { hops: vec![ BlindedHop { blinded_node_id: ln_test_utils::pubkey(45), encrypted_payload: Vec::new() }, diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index 1bcb7fa7492..bc52df35611 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -2199,7 +2199,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, } } @@ -2675,7 +2675,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(); @@ -3372,7 +3372,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]