From 8296b14f97872d9acfed13f26774ca76cdc1d4d0 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 17 Nov 2025 10:22:59 -0500 Subject: [PATCH 1/5] ln/fmt: move rustfmt_skip to per-function in blinded_payment_tests To allow formatting on new code, move to per-function skips. --- lightning/src/ln/blinded_payment_tests.rs | 92 ++++++++++++++++------- 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 5bf015e7c81..6ca2e7b3d27 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1,5 +1,3 @@ -#![cfg_attr(rustfmt, rustfmt_skip)] - // This file is Copyright its original authors, visible in version control // history. // @@ -9,39 +7,46 @@ // You may not use this file except in accordance with one or both of these // 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}; -use crate::blinded_path; -use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12RefundContext, ForwardTlvs, PaymentConstraints, PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs, PAYMENT_PADDING_ROUND_OFF}; +use crate::blinded_path::payment::{ + BlindedPaymentPath, Bolt12RefundContext, ForwardTlvs, PaymentConstraints, PaymentContext, + PaymentForwardNode, PaymentRelay, ReceiveTlvs, PAYMENT_PADDING_ROUND_OFF, +}; use crate::blinded_path::utils::is_padded; +use crate::blinded_path::{self, BlindedHop}; use crate::events::{Event, HTLCHandlingFailureType, PaymentFailureReason}; -use crate::ln::types::ChannelId; -use crate::types::payment::{PaymentHash, PaymentSecret}; use crate::ln::channelmanager; use crate::ln::channelmanager::{HTLCFailureMsg, PaymentId, RecipientOnionFields}; -use crate::types::features::{BlindedHopFeatures, ChannelFeatures, NodeFeatures}; use crate::ln::functional_test_utils::*; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs; -use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, UnsignedGossipMessage, MessageSendEvent}; +use crate::ln::msgs::{ + BaseMessageHandler, ChannelMessageHandler, MessageSendEvent, UnsignedGossipMessage, +}; use crate::ln::onion_payment; use crate::ln::onion_utils::{self, LocalHTLCFailureReason}; use crate::ln::outbound_payment::{Retry, IDEMPOTENCY_TIMEOUT_TICKS}; +use crate::ln::types::ChannelId; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::prelude::*; -use crate::routing::router::{BlindedTail, Path, Payee, PaymentParameters, RouteHop, RouteParameters, TrampolineHop}; +use crate::routing::router::Route; +use crate::routing::router::{ + BlindedTail, Path, Payee, PaymentParameters, RouteHop, RouteParameters, TrampolineHop, +}; use crate::sign::{NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient}; +use crate::types::features::{BlindedHopFeatures, ChannelFeatures, NodeFeatures}; +use crate::types::payment::{PaymentHash, PaymentSecret}; use crate::util::config::UserConfig; use crate::util::ser::{WithoutLength, Writeable}; use crate::util::test_utils; +use bitcoin::hashes::hex::FromHex; +use bitcoin::hex::DisplayHex; +use bitcoin::secp256k1::ecdh::SharedSecret; +use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; +use bitcoin::secp256k1::{schnorr, PublicKey, Scalar, Secp256k1, SecretKey}; use lightning_invoice::RawBolt11Invoice; use types::features::Features; -use crate::blinded_path::BlindedHop; -use crate::routing::router::Route; +#[rustfmt::skip] pub fn blinded_payment_path( payment_secret: PaymentSecret, intro_node_min_htlc: u64, intro_node_max_htlc: u64, node_ids: Vec, channel_upds: &[&msgs::UnsignedChannelUpdate], @@ -94,20 +99,24 @@ pub fn blinded_payment_path( } pub fn get_blinded_route_parameters( - amt_msat: u64, payment_secret: PaymentSecret, intro_node_min_htlc: u64, intro_node_max_htlc: u64, - node_ids: Vec, channel_upds: &[&msgs::UnsignedChannelUpdate], - keys_manager: &test_utils::TestKeysInterface + amt_msat: u64, payment_secret: PaymentSecret, intro_node_min_htlc: u64, + intro_node_max_htlc: u64, node_ids: Vec, + channel_upds: &[&msgs::UnsignedChannelUpdate], keys_manager: &test_utils::TestKeysInterface, ) -> RouteParameters { RouteParameters::from_payment_params_and_value( - PaymentParameters::blinded(vec![ - blinded_payment_path( - payment_secret, intro_node_min_htlc, intro_node_max_htlc, node_ids, channel_upds, - keys_manager - ) - ]), amt_msat + PaymentParameters::blinded(vec![blinded_payment_path( + payment_secret, + intro_node_min_htlc, + intro_node_max_htlc, + node_ids, + channel_upds, + keys_manager, + )]), + amt_msat, ) } +#[rustfmt::skip] pub fn fail_blinded_htlc_backwards( payment_hash: PaymentHash, intro_node_idx: usize, nodes: &[&Node], retry_expected: bool @@ -149,6 +158,7 @@ fn one_hop_blinded_path() { do_one_hop_blinded_path(false); } +#[rustfmt::skip] fn do_one_hop_blinded_path(success: bool) { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); @@ -191,6 +201,7 @@ fn do_one_hop_blinded_path(success: bool) { } #[test] +#[rustfmt::skip] fn mpp_to_one_hop_blinded_path() { let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); @@ -270,6 +281,7 @@ fn mpp_to_one_hop_blinded_path() { } #[test] +#[rustfmt::skip] fn mpp_to_three_hop_blinded_paths() { let chanmon_cfgs = create_chanmon_cfgs(6); let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); @@ -363,6 +375,7 @@ fn forward_checks_failure() { do_forward_checks_failure(ForwardCheckFail::OutboundChannelCheck, false); } +#[rustfmt::skip] fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { // Ensure we'll fail backwards properly if a forwarding check fails on initial update_add // receipt. @@ -500,6 +513,7 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { } #[test] +#[rustfmt::skip] fn failed_backwards_to_intro_node() { // Ensure the intro node will error backwards properly even if the downstream node did not blind // their error. @@ -570,12 +584,14 @@ enum ProcessPendingHTLCsCheck { } #[test] +#[rustfmt::skip] fn forward_fail_in_process_pending_htlc_fwds() { do_forward_fail_in_process_pending_htlc_fwds(ProcessPendingHTLCsCheck::FwdPeerDisconnected, true); do_forward_fail_in_process_pending_htlc_fwds(ProcessPendingHTLCsCheck::FwdPeerDisconnected, false); do_forward_fail_in_process_pending_htlc_fwds(ProcessPendingHTLCsCheck::FwdChannelClosed, true); do_forward_fail_in_process_pending_htlc_fwds(ProcessPendingHTLCsCheck::FwdChannelClosed, false); } +#[rustfmt::skip] fn do_forward_fail_in_process_pending_htlc_fwds(check: ProcessPendingHTLCsCheck, intro_fails: bool) { // Ensure the intro node will error backwards properly if the HTLC fails in // process_pending_htlc_forwards. @@ -685,6 +701,8 @@ fn blinded_intercept_payment() { do_blinded_intercept_payment(true); do_blinded_intercept_payment(false); } + +#[rustfmt::skip] fn do_blinded_intercept_payment(intercept_node_fails: bool) { let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); @@ -767,6 +785,7 @@ fn do_blinded_intercept_payment(intercept_node_fails: bool) { } #[test] +#[rustfmt::skip] fn two_hop_blinded_path_success() { let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); @@ -788,6 +807,7 @@ fn two_hop_blinded_path_success() { } #[test] +#[rustfmt::skip] fn three_hop_blinded_path_success() { let chanmon_cfgs = create_chanmon_cfgs(5); let node_cfgs = create_node_cfgs(5, &chanmon_cfgs); @@ -817,6 +837,7 @@ fn three_hop_blinded_path_success() { } #[test] +#[rustfmt::skip] fn three_hop_blinded_path_fail() { // Test that an intermediate blinded forwarding node gets failed back to with // malformed and also fails back themselves with malformed. @@ -876,6 +897,7 @@ fn multi_hop_receiver_fail() { do_multi_hop_receiver_fail(ReceiveCheckFail::PaymentConstraints); } +#[rustfmt::skip] fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { // Test that the receiver to a multihop blinded path fails back correctly. let chanmon_cfgs = create_chanmon_cfgs(3); @@ -1076,6 +1098,7 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { } #[test] +#[rustfmt::skip] fn blinded_path_retries() { let chanmon_cfgs = create_chanmon_cfgs(4); // Make one blinded path's fees slightly higher so they are tried in a deterministic order. @@ -1183,6 +1206,7 @@ fn blinded_path_retries() { } #[test] +#[rustfmt::skip] fn min_htlc() { // The min htlc of a blinded path is the max (htlc_min - following_fees) along the path. Make sure // the payment succeeds when we calculate the min htlc this way. @@ -1259,6 +1283,7 @@ fn min_htlc() { } #[test] +#[rustfmt::skip] fn conditionally_round_fwd_amt() { // Previously, the (rng-found) feerates below caught a bug where an intermediate node would // calculate an amt_to_forward that underpaid them by 1 msat, caused by rounding up the outbound @@ -1309,8 +1334,8 @@ fn conditionally_round_fwd_amt() { expect_payment_sent(&nodes[0], payment_preimage, Some(Some(expected_fee)), true, true); } - #[test] +#[rustfmt::skip] fn custom_tlvs_to_blinded_path() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); @@ -1365,6 +1390,7 @@ fn custom_tlvs_to_blinded_path() { } #[test] +#[rustfmt::skip] fn fails_receive_tlvs_authentication() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); @@ -1454,6 +1480,7 @@ fn fails_receive_tlvs_authentication() { } #[test] +#[rustfmt::skip] fn blinded_payment_path_padding() { // Make sure that for a blinded payment path, all encrypted payloads are padded to equal lengths. let chanmon_cfgs = create_chanmon_cfgs(5); @@ -1503,7 +1530,7 @@ fn pubkey_from_hex(hex: &str) -> PublicKey { fn update_add_msg( amount_msat: u64, cltv_expiry: u32, blinding_point: Option, - onion_routing_packet: msgs::OnionPacket + onion_routing_packet: msgs::OnionPacket, ) -> msgs::UpdateAddHTLC { msgs::UpdateAddHTLC { channel_id: ChannelId::from_bytes([0; 32]), @@ -1519,6 +1546,7 @@ fn update_add_msg( } #[test] +#[rustfmt::skip] fn route_blinding_spec_test_vector() { let mut secp_ctx = Secp256k1::new(); let bob_secret = secret_from_hex("4242424242424242424242424242424242424242424242424242424242424242"); @@ -1749,6 +1777,7 @@ fn route_blinding_spec_test_vector() { } #[test] +#[rustfmt::skip] 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 @@ -1832,6 +1861,7 @@ fn test_combined_trampoline_onion_creation_vectors() { } #[test] +#[rustfmt::skip] fn test_trampoline_inbound_payment_decoding() { let secp_ctx = Secp256k1::new(); let session_priv = secret_from_hex("0303030303030303030303030303030303030303030303030303030303030303"); @@ -1978,6 +2008,7 @@ fn test_trampoline_inbound_payment_decoding() { } #[test] +#[rustfmt::skip] fn test_trampoline_forward_payload_encoded_as_receive() { // Test that we'll fail backwards as expected when receiving a well-formed blinded forward // trampoline onion payload with no next hop present. @@ -2165,6 +2196,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { } } +#[rustfmt::skip] fn do_test_trampoline_single_hop_receive(success: bool) { const TOTAL_NODE_COUNT: usize = 3; let secp_ctx = Secp256k1::new(); @@ -2266,6 +2298,7 @@ fn test_trampoline_single_hop_receive() { do_test_trampoline_single_hop_receive(false); } +#[rustfmt::skip] fn do_test_trampoline_unblinded_receive(success: bool) { // Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2) @@ -2416,11 +2449,12 @@ fn do_test_trampoline_unblinded_receive(success: bool) { #[test] fn test_trampoline_unblinded_receive() { - do_test_trampoline_unblinded_receive(true); - do_test_trampoline_unblinded_receive(false); + do_test_trampoline_unblinded_receive(true); + do_test_trampoline_unblinded_receive(false); } #[test] +#[rustfmt::skip] fn test_trampoline_forward_rejection() { const TOTAL_NODE_COUNT: usize = 3; From 7b393a722b0da780488d29d9e2344fea76bb98cd Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 17 Nov 2025 09:55:59 -0500 Subject: [PATCH 2/5] ln/fmt: remove rustfmt::skip from do_test_trampoline_unblinded_receive Remove skip without fixing up any of the ugly formatting, so that the diff is a bit more readable in review. --- lightning/src/ln/blinded_payment_tests.rs | 112 ++++++++++++++++------ 1 file changed, 83 insertions(+), 29 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 6ca2e7b3d27..96db8948144 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2298,7 +2298,6 @@ fn test_trampoline_single_hop_receive() { do_test_trampoline_single_hop_receive(false); } -#[rustfmt::skip] fn do_test_trampoline_unblinded_receive(success: bool) { // Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2) @@ -2307,24 +2306,46 @@ fn do_test_trampoline_unblinded_receive(success: bool) { let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]); + let node_chanmgrs = + create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]); let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs); - let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); - let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let (_, _, chan_id_alice_bob, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let (_, _, chan_id_bob_carol, _) = + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); - for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks - connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1); + for i in 0..TOTAL_NODE_COUNT { + // connect all nodes' blocks + connect_blocks( + &nodes[i], + (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1, + ); } let bob_node_id = nodes[1].node().get_our_node_id(); let carol_node_id = nodes[2].node().get_our_node_id(); - let alice_bob_scid = nodes[0].node().list_channels().iter().find(|c| c.channel_id == chan_id_alice_bob).unwrap().short_channel_id.unwrap(); - let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap(); + let alice_bob_scid = nodes[0] + .node() + .list_channels() + .iter() + .find(|c| c.channel_id == chan_id_alice_bob) + .unwrap() + .short_channel_id + .unwrap(); + let bob_carol_scid = nodes[1] + .node() + .list_channels() + .iter() + .find(|c| c.channel_id == chan_id_bob_carol) + .unwrap() + .short_channel_id + .unwrap(); let amt_msat = 1000; - let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); + let (payment_preimage, payment_hash, payment_secret) = + get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); let route = Route { paths: vec![Path { hops: vec![ @@ -2338,7 +2359,6 @@ fn do_test_trampoline_unblinded_receive(success: bool) { cltv_expiry_delta: 48, maybe_announced_channel: false, }, - // Carol RouteHop { pubkey: carol_node_id, @@ -2348,7 +2368,7 @@ fn do_test_trampoline_unblinded_receive(success: bool) { fee_msat: 0, cltv_expiry_delta: 48, maybe_announced_channel: false, - } + }, ], blinded_tail: Some(BlindedTail { trampoline_hops: vec![ @@ -2363,12 +2383,12 @@ fn do_test_trampoline_unblinded_receive(success: bool) { // The blinded path data is unused because we replace the onion of the last hop hops: vec![BlindedHop { blinded_node_id: PublicKey::from_slice(&[2; 33]).unwrap(), - encrypted_payload: vec![42; 32] + encrypted_payload: vec![42; 32], }], blinding_point: PublicKey::from_slice(&[2; 33]).unwrap(), excess_final_cltv_expiry_delta: 39, final_value_msat: amt_msat, - }) + }), }], route_params: None, }; @@ -2376,46 +2396,78 @@ fn do_test_trampoline_unblinded_receive(success: bool) { // We need the session priv to construct an invalid onion packet later. let override_random_bytes = [42; 32]; *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(override_random_bytes); - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + nodes[0] + .node + .send_payment_with_route( + route.clone(), + payment_hash, + RecipientOnionFields::spontaneous_empty(), + PaymentId(payment_hash.0), + ) + .unwrap(); let replacement_onion = { // create a substitute onion where the last Trampoline hop is an unblinded receive, which we // (deliberately) do not support out of the box, therefore necessitating this workaround let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap(); - let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); + let trampoline_session_priv = + onion_utils::compute_trampoline_session_priv(&outer_session_priv); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); let blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); - let (_, _, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, amt_msat, &recipient_onion_fields, 32, &None).unwrap(); + let (_, _, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads( + &blinded_tail, + amt_msat, + &recipient_onion_fields, + 32, + &None, + ) + .unwrap(); let trampoline_payloads = vec![msgs::OutboundTrampolinePayload::Receive { - payment_data: Some(msgs::FinalOnionHopData { - payment_secret, - total_msat: amt_msat, - }), + payment_data: Some(msgs::FinalOnionHopData { payment_secret, total_msat: amt_msat }), sender_intended_htlc_amt_msat: amt_msat, cltv_expiry_height: 104, }]; - let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys(&secp_ctx, &route.paths[0].blinded_tail.as_ref().unwrap(), &trampoline_session_priv); + let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys( + &secp_ctx, + &route.paths[0].blinded_tail.as_ref().unwrap(), + &trampoline_session_priv, + ); let trampoline_packet = onion_utils::construct_trampoline_onion_packet( trampoline_payloads, trampoline_onion_keys, override_random_bytes, &payment_hash, None, - ).unwrap(); + ) + .unwrap(); // Use a different session key to construct the replacement onion packet. Note that the sender isn't aware of // this and won't be able to decode the fulfill hold times. - let (outer_payloads, _, _) = onion_utils::build_onion_payloads(&route.paths[0], amt_msat, &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap(); - let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv); + let (outer_payloads, _, _) = onion_utils::build_onion_payloads( + &route.paths[0], + amt_msat, + &recipient_onion_fields, + outer_starting_htlc_offset, + &None, + None, + Some(trampoline_packet), + ) + .unwrap(); + let outer_onion_keys = onion_utils::construct_onion_keys( + &secp_ctx, + &route.clone().paths[0], + &outer_session_priv, + ); let outer_packet = onion_utils::construct_onion_packet( outer_payloads, outer_onion_keys, override_random_bytes, &payment_hash, - ).unwrap(); + ) + .unwrap(); outer_packet }; @@ -2424,21 +2476,23 @@ fn do_test_trampoline_unblinded_receive(success: bool) { let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); - let mut first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let mut first_message_event = + remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); let mut update_message = match first_message_event { MessageSendEvent::UpdateHTLCs { ref mut updates, .. } => { assert_eq!(updates.update_add_htlcs.len(), 1); updates.update_add_htlcs.get_mut(0) }, - _ => panic!() + _ => panic!(), }; update_message.map(|msg| { msg.onion_routing_packet = replacement_onion.clone(); }); let route: &[&Node] = &[&nodes[1], &nodes[2]]; - let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event) - .with_payment_secret(payment_secret); + let args = + PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event) + .with_payment_secret(payment_secret); do_pass_along_path(args); if success { claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); From 77ea9c70ab79b940fca411f0e437c3df7da298be Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 17 Nov 2025 09:57:29 -0500 Subject: [PATCH 3/5] ln/fmt: clean up ugly formatting of do_test_trampoline_unblinded_receive --- lightning/src/ln/blinded_payment_tests.rs | 49 ++++++----------------- lightning/src/ln/functional_test_utils.rs | 10 +++++ 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 96db8948144..b8bc7770755 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -2306,17 +2306,14 @@ fn do_test_trampoline_unblinded_receive(success: bool) { let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs); - let node_chanmgrs = - create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]); + let user_cfgs = &vec![None; TOTAL_NODE_COUNT]; + let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &user_cfgs); let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs); - let (_, _, chan_id_alice_bob, _) = - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); - let (_, _, chan_id_bob_carol, _) = - create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let alice_bob_chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let bob_carol_chan = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); for i in 0..TOTAL_NODE_COUNT { - // connect all nodes' blocks connect_blocks( &nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1, @@ -2326,22 +2323,8 @@ fn do_test_trampoline_unblinded_receive(success: bool) { let bob_node_id = nodes[1].node().get_our_node_id(); let carol_node_id = nodes[2].node().get_our_node_id(); - let alice_bob_scid = nodes[0] - .node() - .list_channels() - .iter() - .find(|c| c.channel_id == chan_id_alice_bob) - .unwrap() - .short_channel_id - .unwrap(); - let bob_carol_scid = nodes[1] - .node() - .list_channels() - .iter() - .find(|c| c.channel_id == chan_id_bob_carol) - .unwrap() - .short_channel_id - .unwrap(); + let alice_bob_scid = get_scid_from_channel_id(&nodes[0], alice_bob_chan.2); + let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan.2); let amt_msat = 1000; let (payment_preimage, payment_hash, payment_secret) = @@ -2349,7 +2332,6 @@ fn do_test_trampoline_unblinded_receive(success: bool) { let route = Route { paths: vec![Path { hops: vec![ - // Bob RouteHop { pubkey: bob_node_id, node_features: NodeFeatures::empty(), @@ -2359,7 +2341,6 @@ fn do_test_trampoline_unblinded_receive(success: bool) { cltv_expiry_delta: 48, maybe_announced_channel: false, }, - // Carol RouteHop { pubkey: carol_node_id, node_features: NodeFeatures::empty(), @@ -2371,15 +2352,12 @@ fn do_test_trampoline_unblinded_receive(success: bool) { }, ], blinded_tail: Some(BlindedTail { - trampoline_hops: vec![ - // Carol - TrampolineHop { - pubkey: carol_node_id, - node_features: Features::empty(), - fee_msat: amt_msat, - cltv_expiry_delta: 24, - }, - ], + trampoline_hops: vec![TrampolineHop { + pubkey: carol_node_id, + node_features: Features::empty(), + fee_msat: amt_msat, + cltv_expiry_delta: 24, + }], // The blinded path data is unused because we replace the onion of the last hop hops: vec![BlindedHop { blinded_node_id: PublicKey::from_slice(&[2; 33]).unwrap(), @@ -2476,8 +2454,7 @@ fn do_test_trampoline_unblinded_receive(success: bool) { let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); - let mut first_message_event = - remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let mut first_message_event = remove_first_msg_event_to_node(&bob_node_id, &mut events); let mut update_message = match first_message_event { MessageSendEvent::UpdateHTLCs { ref mut updates, .. } => { assert_eq!(updates.update_add_htlcs.len(), 1); diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index e31630a4926..22cce2c71d6 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -5526,3 +5526,13 @@ pub fn create_batch_channel_funding<'a, 'b, 'c>( } return (tx, funding_created_msgs); } + +pub fn get_scid_from_channel_id<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, channel_id: ChannelId) -> u64 { + node.node() + .list_channels() + .iter() + .find(|c| c.channel_id == channel_id) + .unwrap() + .short_channel_id + .unwrap() +} From a05f2d246ccbba8dc998b03e9b998ecd40af0184 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 14 Aug 2025 15:00:03 -0400 Subject: [PATCH 4/5] ln: add trampoline LocalHTLCFailureReason variants per spec This commit adds three new local htlc failure error reasons: `TemporaryTrampolineFailure`, `TrampolineFeeOrExpiryInsufficient`, and `UnknownNextTrampoline` for trampoline payment forwarding failures. --- lightning/src/ln/onion_utils.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index e32b39775fe..75fa46fcea7 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -1663,6 +1663,13 @@ pub enum LocalHTLCFailureReason { PeerOffline, /// The HTLC was failed because the channel balance was overdrawn. ChannelBalanceOverdrawn, + /// We have been unable to forward a payment to the next Trampoline node but may be able to + /// do it later. + TemporaryTrampolineFailure, + /// The amount or CLTV expiry were insufficient to route the payment to the next Trampoline. + TrampolineFeeOrExpiryInsufficient, + /// The specified next Trampoline node cannot be reached from our node. + UnknownNextTrampoline, } impl LocalHTLCFailureReason { @@ -1704,6 +1711,9 @@ impl LocalHTLCFailureReason { Self::InvalidOnionPayload | Self::InvalidTrampolinePayload => PERM | 22, Self::MPPTimeout => 23, Self::InvalidOnionBlinding => BADONION | PERM | 24, + Self::TemporaryTrampolineFailure => NODE | 25, + Self::TrampolineFeeOrExpiryInsufficient => NODE | 26, + Self::UnknownNextTrampoline => PERM | 27, Self::UnknownFailureCode { code } => *code, } } @@ -1863,7 +1873,10 @@ ser_failure_reasons!( (39, HTLCMinimum), (40, HTLCMaximum), (41, PeerOffline), - (42, ChannelBalanceOverdrawn) + (42, ChannelBalanceOverdrawn), + (43, TemporaryTrampolineFailure), + (44, TrampolineFeeOrExpiryInsufficient), + (45, UnknownNextTrampoline) ); impl From<&HTLCFailReason> for HTLCHandlingFailureReason { @@ -2031,6 +2044,11 @@ impl HTLCFailReason { debug_assert!(false, "Unknown failure code: {}", code) } }, + LocalHTLCFailureReason::TemporaryTrampolineFailure => debug_assert!(data.is_empty()), + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient => { + debug_assert_eq!(data.len(), 10) + }, + LocalHTLCFailureReason::UnknownNextTrampoline => debug_assert!(data.is_empty()), } Self(HTLCFailReasonRepr::Reason { data, failure_reason }) From b43670e7836b41ab2078bd28eadae5a0137252b4 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 14 Nov 2025 09:49:30 -0500 Subject: [PATCH 5/5] ln: enforce trampoline onion constraints We add a `check_trampoline_constraints` similar to `check_blinded_path_constraints` that compares the Trampoline onion's amount and CLTV values to the limitations imposed by the outer onion. Tests are added to cover validation of blinded and unblinded trampoline payloads against their outer onion. These are consolidated with our existing coverage for successful receives. Co-authored-by: Arik Sosman Co-authored-by: Maurice Poirrier --- lightning/src/ln/blinded_payment_tests.rs | 424 ++++++++++++++++------ lightning/src/ln/msgs.rs | 1 + lightning/src/ln/onion_payment.rs | 73 +++- lightning/src/ln/onion_utils.rs | 1 + 4 files changed, 391 insertions(+), 108 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index b8bc7770755..1ec5ef30d41 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -42,7 +42,7 @@ use bitcoin::hashes::hex::FromHex; use bitcoin::hex::DisplayHex; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; -use bitcoin::secp256k1::{schnorr, PublicKey, Scalar, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{schnorr, All, PublicKey, Scalar, Secp256k1, SecretKey}; use lightning_invoice::RawBolt11Invoice; use types::features::Features; @@ -2266,7 +2266,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { pubkey: carol_node_id, node_features: Features::empty(), fee_msat: amt_msat, - cltv_expiry_delta: 24, + cltv_expiry_delta: 104, }, ], hops: blinded_path.blinded_hops().to_vec(), @@ -2298,9 +2298,230 @@ fn test_trampoline_single_hop_receive() { do_test_trampoline_single_hop_receive(false); } -fn do_test_trampoline_unblinded_receive(success: bool) { - // Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2) +#[derive(Copy, Clone, PartialEq, Eq)] +enum TrampolineTestCase { + Success, + Underpayment, + OuterCLTVLessThanTrampoline, +} + +impl<'a> TrampolineTestCase { + fn payment_failed_conditions( + self, final_payment_amt: &'a [u8], final_cltv_delta: &'a [u8], + ) -> Option> { + match self { + TrampolineTestCase::Success => None, + TrampolineTestCase::Underpayment => { + Some(PaymentFailedConditions::new().expected_htlc_error_data( + LocalHTLCFailureReason::FinalIncorrectHTLCAmount, + final_payment_amt, + )) + }, + TrampolineTestCase::OuterCLTVLessThanTrampoline => { + Some(PaymentFailedConditions::new().expected_htlc_error_data( + LocalHTLCFailureReason::FinalIncorrectCLTVExpiry, + final_cltv_delta, + )) + }, + } + } + + fn expected_log(&self) -> Option<(&str, &str, usize)> { + match self { + TrampolineTestCase::Success => None, + TrampolineTestCase::Underpayment => Some(( + "lightning::ln::channelmanager", + "Trampoline onion's amt value exceeded the outer onion's", + 1, + )), + TrampolineTestCase::OuterCLTVLessThanTrampoline => Some(( + "lightning::ln::channelmanager", + "Trampoline onion's CLTV value exceeded the outer onion's", + 1, + )), + } + } + + fn outer_onion_cltv(&self, outer_cltv: u32) -> u32 { + if *self == TrampolineTestCase::OuterCLTVLessThanTrampoline { + return outer_cltv / 2; + } + outer_cltv + } + + fn outer_onion_amt(&self, original_amt: u64) -> u64 { + if *self == TrampolineTestCase::Underpayment { + return original_amt / 2; + } + original_amt + } +} + +#[test] +fn test_trampoline_unblinded_receive() { + do_test_trampoline_relay(false, TrampolineTestCase::Success); + do_test_trampoline_relay(false, TrampolineTestCase::Underpayment); + do_test_trampoline_relay(false, TrampolineTestCase::OuterCLTVLessThanTrampoline); +} + +#[test] +fn test_trampoline_blinded_receive() { + do_test_trampoline_relay(true, TrampolineTestCase::Success); + do_test_trampoline_relay(true, TrampolineTestCase::Underpayment); + do_test_trampoline_relay(true, TrampolineTestCase::OuterCLTVLessThanTrampoline); +} + +/// Creates a blinded tail where Carol receives via a blinded path. +fn create_blinded_tail( + secp_ctx: &Secp256k1, override_random_bytes: [u8; 32], carol_node_id: PublicKey, + carol_auth_key: ReceiveAuthKey, trampoline_cltv_expiry_delta: u32, final_value_msat: u64, + payment_secret: PaymentSecret, +) -> BlindedTail { + let outer_session_priv = SecretKey::from_slice(&override_random_bytes).unwrap(); + let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); + + let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &trampoline_session_priv); + let carol_blinded_hops = { + let payee_tlvs = ReceiveTlvs { + payment_secret, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: final_value_msat, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + } + .encode(); + + let path = [((carol_node_id, Some(carol_auth_key)), WithoutLength(&payee_tlvs))]; + blinded_path::utils::construct_blinded_hops( + &secp_ctx, + path.into_iter(), + &trampoline_session_priv, + ) + }; + + BlindedTail { + trampoline_hops: vec![TrampolineHop { + pubkey: carol_node_id, + node_features: Features::empty(), + fee_msat: final_value_msat, + cltv_expiry_delta: trampoline_cltv_expiry_delta, + }], + hops: carol_blinded_hops, + blinding_point: carol_blinding_point, + excess_final_cltv_expiry_delta: 39, + final_value_msat, + } +} + +// Creates a replacement onion that is used to produce scenarios that we don't support, specifically +// unblinded receives and invalid payloads. +fn replacement_onion( + test_case: TrampolineTestCase, secp_ctx: &Secp256k1, override_random_bytes: [u8; 32], + route: Route, original_amt_msat: u64, starting_htlc_offset: u32, original_trampoline_cltv: u32, + payment_hash: PaymentHash, payment_secret: PaymentSecret, blinded: bool, +) -> msgs::OnionPacket { + let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap(); + let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); + + let blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); + + // Rebuild our trampoline packet from the original route. If we want to test Carol receiving + // as an unblinded trampoline hop, we switch out her inner trampoline onion with a direct + // receive payload because LDK doesn't support unblinded trampoline receives. + let (trampoline_packet, outer_total_msat, outer_starting_htlc_offset) = { + let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = + onion_utils::build_trampoline_onion_payloads( + &blinded_tail, + original_amt_msat, + &recipient_onion_fields, + starting_htlc_offset, + &None, + ) + .unwrap(); + + if !blinded { + trampoline_payloads = vec![msgs::OutboundTrampolinePayload::Receive { + payment_data: Some(msgs::FinalOnionHopData { + payment_secret, + total_msat: original_amt_msat, + }), + sender_intended_htlc_amt_msat: original_amt_msat, + cltv_expiry_height: original_trampoline_cltv + starting_htlc_offset, + }]; + } + + let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys( + &secp_ctx, + &blinded_tail, + &trampoline_session_priv, + ); + let trampoline_packet = onion_utils::construct_trampoline_onion_packet( + trampoline_payloads, + trampoline_onion_keys, + override_random_bytes, + &payment_hash, + None, + ) + .unwrap(); + + (trampoline_packet, outer_total_msat, outer_starting_htlc_offset) + }; + + // Use a different session key to construct the replacement onion packet. Note that the + // sender isn't aware of this and won't be able to decode the fulfill hold times. + let (mut outer_payloads, _, _) = onion_utils::build_onion_payloads( + &route.paths[0], + outer_total_msat, + &recipient_onion_fields, + outer_starting_htlc_offset, + &None, + None, + Some(trampoline_packet), + ) + .unwrap(); + assert_eq!(outer_payloads.len(), 2); + + // If we're trying to test invalid payloads, we modify Carol's *outer* onion to have values + // that are inconsistent with her inner onion. We need to do this manually because we + // (obviously) can't construct an invalid onion with LDK's built in functions. + match &mut outer_payloads[1] { + msgs::OutboundOnionPayload::TrampolineEntrypoint { + amt_to_forward, + outgoing_cltv_value, + .. + } => match test_case { + TrampolineTestCase::Underpayment => { + *amt_to_forward = test_case.outer_onion_amt(original_amt_msat) + }, + TrampolineTestCase::OuterCLTVLessThanTrampoline => { + *outgoing_cltv_value = + test_case.outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset) + }, + _ => {}, + }, + _ => panic!("final payload is not trampoline entrypoint"), + } + + let outer_onion_keys = + onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv); + onion_utils::construct_onion_packet( + outer_payloads, + outer_onion_keys, + override_random_bytes, + &payment_hash, + ) + .unwrap() +} + +// Test relay of payments to a trampoline, testing success and trampoline-related relay failures. +// This test relies on manually replacing parts of our onion to: +// - Test unblinded trampoline receives, which are not natively supported in LDK. +// - To hit validation errors by manipulating the trampoline's outer packet. Without this, we would +// have to manually construct the onion. +fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { const TOTAL_NODE_COUNT: usize = 3; let secp_ctx = Secp256k1::new(); @@ -2320,15 +2541,24 @@ fn do_test_trampoline_unblinded_receive(success: bool) { ); } + let alice_node_id = nodes[0].node.get_our_node_id(); let bob_node_id = nodes[1].node().get_our_node_id(); let carol_node_id = nodes[2].node().get_our_node_id(); let alice_bob_scid = get_scid_from_channel_id(&nodes[0], alice_bob_chan.2); let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan.2); - let amt_msat = 1000; + let original_amt_msat = 1000; + let original_trampoline_cltv = 72; + let starting_htlc_offset = 32; + let (payment_preimage, payment_hash, payment_secret) = - get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); + get_payment_preimage_hash(&nodes[2], Some(original_amt_msat), None); + + // We need the session priv to replace the onion packet later. + let override_random_bytes = [42; 32]; + *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(override_random_bytes); + let route = Route { paths: vec![Path { hops: vec![ @@ -2351,29 +2581,21 @@ fn do_test_trampoline_unblinded_receive(success: bool) { maybe_announced_channel: false, }, ], - blinded_tail: Some(BlindedTail { - trampoline_hops: vec![TrampolineHop { - pubkey: carol_node_id, - node_features: Features::empty(), - fee_msat: amt_msat, - cltv_expiry_delta: 24, - }], - // The blinded path data is unused because we replace the onion of the last hop - hops: vec![BlindedHop { - blinded_node_id: PublicKey::from_slice(&[2; 33]).unwrap(), - encrypted_payload: vec![42; 32], - }], - blinding_point: PublicKey::from_slice(&[2; 33]).unwrap(), - excess_final_cltv_expiry_delta: 39, - final_value_msat: amt_msat, - }), + // Create a blinded tail where Carol is receiving. In our unblinded test cases, we'll + // override this anyway (with an unblinded receive, which LDK doesn't allow). + blinded_tail: Some(create_blinded_tail( + &secp_ctx, + override_random_bytes, + carol_node_id, + nodes[2].keys_manager.get_receive_auth_key(), + original_trampoline_cltv, + original_amt_msat, + payment_secret, + )), }], route_params: None, }; - // We need the session priv to construct an invalid onion packet later. - let override_random_bytes = [42; 32]; - *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(override_random_bytes); nodes[0] .node .send_payment_with_route( @@ -2384,72 +2606,6 @@ fn do_test_trampoline_unblinded_receive(success: bool) { ) .unwrap(); - let replacement_onion = { - // create a substitute onion where the last Trampoline hop is an unblinded receive, which we - // (deliberately) do not support out of the box, therefore necessitating this workaround - let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap(); - let trampoline_session_priv = - onion_utils::compute_trampoline_session_priv(&outer_session_priv); - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); - - let blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); - let (_, _, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads( - &blinded_tail, - amt_msat, - &recipient_onion_fields, - 32, - &None, - ) - .unwrap(); - let trampoline_payloads = vec![msgs::OutboundTrampolinePayload::Receive { - payment_data: Some(msgs::FinalOnionHopData { payment_secret, total_msat: amt_msat }), - sender_intended_htlc_amt_msat: amt_msat, - cltv_expiry_height: 104, - }]; - - let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys( - &secp_ctx, - &route.paths[0].blinded_tail.as_ref().unwrap(), - &trampoline_session_priv, - ); - let trampoline_packet = onion_utils::construct_trampoline_onion_packet( - trampoline_payloads, - trampoline_onion_keys, - override_random_bytes, - &payment_hash, - None, - ) - .unwrap(); - - // Use a different session key to construct the replacement onion packet. Note that the sender isn't aware of - // this and won't be able to decode the fulfill hold times. - - let (outer_payloads, _, _) = onion_utils::build_onion_payloads( - &route.paths[0], - amt_msat, - &recipient_onion_fields, - outer_starting_htlc_offset, - &None, - None, - Some(trampoline_packet), - ) - .unwrap(); - let outer_onion_keys = onion_utils::construct_onion_keys( - &secp_ctx, - &route.clone().paths[0], - &outer_session_priv, - ); - let outer_packet = onion_utils::construct_onion_packet( - outer_payloads, - outer_onion_keys, - override_random_bytes, - &payment_hash, - ) - .unwrap(); - - outer_packet - }; - check_added_monitors!(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -2462,28 +2618,88 @@ fn do_test_trampoline_unblinded_receive(success: bool) { }, _ => panic!(), }; + + // Replace the onion to test different scenarios: + // - If !blinded: Creates unblinded receive in trampoline onion + // - If blinded: Modifies outer onion to create outer/inner mismatches if testing failures update_message.map(|msg| { - msg.onion_routing_packet = replacement_onion.clone(); + msg.onion_routing_packet = replacement_onion( + test_case, + &secp_ctx, + override_random_bytes, + route, + original_amt_msat, + starting_htlc_offset, + original_trampoline_cltv, + payment_hash, + payment_secret, + blinded, + ) }); let route: &[&Node] = &[&nodes[1], &nodes[2]]; - let args = - PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event) - .with_payment_secret(payment_secret); + let args = PassAlongPathArgs::new( + &nodes[0], + route, + original_amt_msat, + payment_hash, + first_message_event, + ); + + let amt_bytes = test_case.outer_onion_amt(original_amt_msat).to_be_bytes(); + let cltv_bytes = + test_case.outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset).to_be_bytes(); + let payment_failure = test_case.payment_failed_conditions(&amt_bytes, &cltv_bytes).map(|p| { + if blinded { + PaymentFailedConditions::new() + .expected_htlc_error_data(LocalHTLCFailureReason::InvalidOnionBlinding, &[0; 32]) + } else { + p + } + }); + let args = if payment_failure.is_some() { + args.with_payment_preimage(payment_preimage) + .without_claimable_event() + .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) + } else { + args.with_payment_secret(payment_secret) + }; + do_pass_along_path(args); - if success { - claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); + + if let Some(failure) = payment_failure { + let node_updates = get_htlc_update_msgs(&nodes[2], &bob_node_id); + nodes[1].node.handle_update_fail_htlc(carol_node_id, &node_updates.update_fail_htlcs[0]); + do_commitment_signed_dance( + &nodes[1], + &nodes[2], + &node_updates.commitment_signed, + true, + false, + ); + + let node_updates = get_htlc_update_msgs(&nodes[1], &alice_node_id); + nodes[0].node.handle_update_fail_htlc(bob_node_id, &node_updates.update_fail_htlcs[0]); + do_commitment_signed_dance( + &nodes[0], + &nodes[1], + &node_updates.commitment_signed, + false, + false, + ); + + expect_payment_failed_conditions(&nodes[0], payment_hash, false, failure); + + // Because we support blinded paths, we also assert on our expected logs to make sure + // that the failure reason hidden by obfuscated blinded errors is as expected. + if let Some((module, line, count)) = test_case.expected_log() { + nodes[2].logger.assert_log_contains(module, line, count); + } } else { - fail_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_hash); + claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); } } -#[test] -fn test_trampoline_unblinded_receive() { - do_test_trampoline_unblinded_receive(true); - do_test_trampoline_unblinded_receive(false); -} - #[test] #[rustfmt::skip] fn test_trampoline_forward_rejection() { diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 8e230fab1d9..3c244f8fb67 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -2380,6 +2380,7 @@ mod fuzzy_internal_msgs { BlindedReceive(InboundOnionBlindedReceivePayload), } + #[derive(Debug)] pub(crate) enum OutboundOnionPayload<'a> { Forward { short_channel_id: u64, diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index f52a2d56e85..a9c75b90315 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -74,6 +74,32 @@ fn check_blinded_forward( Ok((amt_to_forward, outgoing_cltv_value)) } +fn check_trampoline_onion_constraints( + outer_hop_data: &msgs::InboundTrampolineEntrypointPayload, trampoline_cltv_value: u32, + trampoline_amount: u64, +) -> Result<(), InboundHTLCErr> { + if outer_hop_data.outgoing_cltv_value < trampoline_cltv_value { + return Err(InboundHTLCErr { + reason: LocalHTLCFailureReason::FinalIncorrectCLTVExpiry, + err_data: outer_hop_data.outgoing_cltv_value.to_be_bytes().to_vec(), + msg: "Trampoline onion's CLTV value exceeded the outer onion's", + }); + } + let outgoing_amount = outer_hop_data + .multipath_trampoline_data + .as_ref() + .map_or(outer_hop_data.amt_to_forward, |mtd| mtd.total_msat); + if outgoing_amount < trampoline_amount { + return Err(InboundHTLCErr { + reason: LocalHTLCFailureReason::FinalIncorrectHTLCAmount, + err_data: outgoing_amount.to_be_bytes().to_vec(), + msg: "Trampoline onion's amt value exceeded the outer onion's", + }); + } + + Ok(()) +} + enum RoutingInfo { Direct { short_channel_id: u64, @@ -135,7 +161,9 @@ pub(super) fn create_fwd_pending_htlc_info( reason: LocalHTLCFailureReason::InvalidOnionPayload, err_data: Vec::new(), }), - onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + onion_utils::Hop::TrampolineForward { ref outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + // TODO: return reason as forward issue, not as receiving issue when forwarding is ready. + check_trampoline_onion_constraints(outer_hop_data, next_trampoline_hop_data.outgoing_cltv_value, next_trampoline_hop_data.amt_to_forward)?; ( RoutingInfo::Trampoline { next_trampoline: next_trampoline_hop_data.next_trampoline, @@ -150,7 +178,7 @@ pub(super) fn create_fwd_pending_htlc_info( None ) }, - onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + onion_utils::Hop::TrampolineBlindedForward { ref outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward( msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features ).map_err(|()| { @@ -162,6 +190,13 @@ pub(super) fn create_fwd_pending_htlc_info( err_data: vec![0; 32], } })?; + check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward).map_err(|e| { + InboundHTLCErr { + reason: LocalHTLCFailureReason::InvalidOnionBlinding, + err_data: vec![0; 32], + msg: e.msg, + } + })?; ( RoutingInfo::Trampoline { next_trampoline: next_trampoline_hop_data.next_trampoline, @@ -282,16 +317,20 @@ pub(super) fn create_recv_pending_htlc_info( intro_node_blinding_point.is_none(), true, invoice_request, None) } onion_utils::Hop::TrampolineReceive { + ref outer_hop_data, trampoline_shared_secret, trampoline_hop_data: msgs::InboundOnionReceivePayload { payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, cltv_expiry_height, payment_metadata, .. }, .. - } => + } => { + check_trampoline_onion_constraints(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat)?; (payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, - cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None, Some(trampoline_shared_secret.secret_bytes())), + cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None, Some(trampoline_shared_secret.secret_bytes())) + }, onion_utils::Hop::TrampolineBlindedReceive { trampoline_shared_secret, + ref outer_hop_data, trampoline_hop_data: msgs::InboundOnionBlindedReceivePayload { sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, payment_secret, intro_node_blinding_point, payment_constraints, payment_context, keysend_preimage, @@ -309,6 +348,13 @@ pub(super) fn create_recv_pending_htlc_info( } })?; let payment_data = msgs::FinalOnionHopData { payment_secret, total_msat }; + check_trampoline_onion_constraints(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat).map_err(|e| { + InboundHTLCErr { + reason: LocalHTLCFailureReason::InvalidOnionBlinding, + err_data: vec![0; 32], + msg: e.msg, + } + })?; (Some(payment_data), keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context), intro_node_blinding_point.is_none(), true, invoice_request, Some(trampoline_shared_secret.secret_bytes())) @@ -606,6 +652,25 @@ where outgoing_cltv_value, }) } + onion_utils::Hop::TrampolineBlindedForward { next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload { next_trampoline, ref payment_relay, ref payment_constraints, ref features, .. }, outer_shared_secret, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { + let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward( + msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features + ) { + Ok((amt, cltv)) => (amt, cltv), + Err(()) => { + return encode_relay_error("Trampoline blinded forward amt or CLTV values exceeded the outer onion's", + LocalHTLCFailureReason::InvalidOnionBlinding, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &[0; 32]); + } + }; + let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, + incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); + Some(NextPacketDetails { + next_packet_pubkey: next_trampoline_packet_pubkey, + outgoing_connector: HopConnector::Trampoline(next_trampoline), + outgoing_amt_msat: amt_to_forward, + outgoing_cltv_value, + }) + } _ => None }; diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 75fa46fcea7..fa218ba31c6 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -475,6 +475,7 @@ pub(super) fn build_onion_payloads<'a>( PayloadCallbackAction::PushFront => res.insert(0, payload), }, )?; + Ok((res, value_msat, cltv)) }