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