diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 5bf015e7c81..1ec5ef30d41 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, All, 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(); @@ -2234,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(), @@ -2266,36 +2298,270 @@ 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(); 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); + for i in 0..TOTAL_NODE_COUNT { + connect_blocks( + &nodes[i], + (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1, + ); } + 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 = 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 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(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 amt_msat = 1000; - 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![ - // Bob RouteHop { pubkey: bob_node_id, node_features: NodeFeatures::empty(), @@ -2305,8 +2571,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(), @@ -2315,112 +2579,129 @@ 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![ - // Carol - 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(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 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 - }; + nodes[0] + .node + .send_payment_with_route( + route.clone(), + payment_hash, + RecipientOnionFields::spontaneous_empty(), + PaymentId(payment_hash.0), + ) + .unwrap(); check_added_monitors!(&nodes[0], 1); 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); updates.update_add_htlcs.get_mut(0) }, - _ => panic!() + _ => 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() { const TOTAL_NODE_COUNT: usize = 3; 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() +} 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 e32b39775fe..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)) } @@ -1663,6 +1664,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 +1712,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 +1874,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 +2045,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 })