Skip to content

Commit cb5376d

Browse files
committed
ln/test: add tests for mpp accumulation of trampoline forwards
1 parent 314d03b commit cb5376d

File tree

4 files changed

+216
-3
lines changed

4 files changed

+216
-3
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ pub enum BlindedFailure {
524524
}
525525

526526
#[derive(PartialEq, Eq)]
527-
enum OnionPayload {
527+
pub(crate) enum OnionPayload {
528528
/// Indicates this incoming onion payload is for the purpose of paying an invoice.
529529
Invoice {
530530
/// This is only here for backwards-compatibility in serialization, in the future it can be
@@ -539,7 +539,7 @@ enum OnionPayload {
539539

540540
/// HTLCs that are to us and can be failed/claimed by the user
541541
#[derive(PartialEq, Eq)]
542-
struct ClaimableHTLC {
542+
pub(crate) struct ClaimableHTLC {
543543
prev_hop: HTLCPreviousHopData,
544544
cltv_expiry: u32,
545545
/// The amount (in msats) of this MPP part
@@ -5788,6 +5788,20 @@ impl<
57885788
self.pending_outbound_payments.test_set_payment_metadata(payment_id, new_payment_metadata);
57895789
}
57905790

5791+
#[cfg(test)]
5792+
pub(crate) fn test_handle_trampoline_htlc(
5793+
&self, claimable_htlc: ClaimableHTLC, onion_fields: RecipientOnionFields,
5794+
payment_hash: PaymentHash, next_hop_info: NextTrampolineHopInfo, next_node_id: PublicKey,
5795+
) -> Result<(), (HTLCSource, onion_utils::HTLCFailReason)> {
5796+
self.handle_trampoline_htlc(
5797+
claimable_htlc,
5798+
onion_fields,
5799+
payment_hash,
5800+
next_hop_info,
5801+
next_node_id,
5802+
)
5803+
}
5804+
57915805
/// Pays a [`Bolt11Invoice`] associated with the `payment_id`. See [`Self::send_payment`] for more info.
57925806
///
57935807
/// # Payment Id

lightning/src/ln/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ mod reorg_tests;
118118
mod shutdown_tests;
119119
#[cfg(any(feature = "_test_utils", test))]
120120
pub mod splicing_tests;
121+
#[cfg(test)]
122+
mod trampoline_forward_tests;
121123
#[cfg(any(test, feature = "_externalize_tests"))]
122124
#[allow(unused_mut)]
123125
pub mod update_fee_tests;

lightning/src/ln/onion_utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1909,7 +1909,7 @@ impl From<&HTLCFailReason> for HTLCHandlingFailureReason {
19091909

19101910
#[derive(Clone)] // See Channel::revoke_and_ack for why, tl;dr: Rust bug
19111911
#[cfg_attr(test, derive(PartialEq))]
1912-
pub(super) struct HTLCFailReason(HTLCFailReasonRepr);
1912+
pub(crate) struct HTLCFailReason(HTLCFailReasonRepr);
19131913

19141914
#[derive(Clone)] // See Channel::revoke_and_ack for why, tl;dr: Rust bug
19151915
#[cfg_attr(test, derive(PartialEq))]
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// This file is Copyright its original authors, visible in version control
2+
// history.
3+
//
4+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7+
// You may not use this file except in accordance with one or both of these
8+
// licenses.
9+
10+
//! Tests for trampoline MPP accumulation and forwarding validation in
11+
//! [`ChannelManager::handle_trampoline_htlc`].
12+
13+
use crate::chain::transaction::OutPoint;
14+
use crate::events::HTLCHandlingFailureReason;
15+
use crate::ln::channelmanager::{ClaimableHTLC, HTLCPreviousHopData, OnionPayload};
16+
use crate::ln::functional_test_utils::*;
17+
use crate::ln::msgs;
18+
use crate::ln::onion_utils::LocalHTLCFailureReason;
19+
use crate::ln::outbound_payment::{NextTrampolineHopInfo, RecipientOnionFields};
20+
use crate::ln::types::ChannelId;
21+
use crate::types::payment::{PaymentHash, PaymentSecret};
22+
23+
use bitcoin::hashes::Hash;
24+
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
25+
26+
fn test_prev_hop_data() -> HTLCPreviousHopData {
27+
HTLCPreviousHopData {
28+
prev_outbound_scid_alias: 0,
29+
user_channel_id: None,
30+
htlc_id: 0,
31+
incoming_packet_shared_secret: [0; 32],
32+
phantom_shared_secret: None,
33+
trampoline_shared_secret: Some([0; 32]),
34+
blinded_failure: None,
35+
channel_id: ChannelId::from_bytes([0; 32]),
36+
outpoint: OutPoint { txid: bitcoin::Txid::all_zeros(), index: 0 },
37+
counterparty_node_id: None,
38+
cltv_expiry: None,
39+
}
40+
}
41+
42+
fn test_trampoline_onion_packet() -> msgs::TrampolineOnionPacket {
43+
let secp = Secp256k1::new();
44+
let test_secret = SecretKey::from_slice(&[42; 32]).unwrap();
45+
msgs::TrampolineOnionPacket {
46+
version: 0,
47+
public_key: PublicKey::from_secret_key(&secp, &test_secret),
48+
hop_data: vec![0; 650],
49+
hmac: [0; 32],
50+
}
51+
}
52+
53+
fn test_onion_fields(total_msat: u64) -> RecipientOnionFields {
54+
RecipientOnionFields {
55+
payment_secret: Some(PaymentSecret([0; 32])),
56+
total_mpp_amount_msat: total_msat,
57+
payment_metadata: None,
58+
custom_tlvs: Vec::new(),
59+
}
60+
}
61+
62+
enum TrampolineMppValidationTestCase {
63+
FeeInsufficient,
64+
CltvInsufficient,
65+
TrampolineAmountExceedsReceived,
66+
TrampolineCLTVExceedsReceived,
67+
MismatchedPaymentSecret,
68+
}
69+
70+
/// Sends two MPP parts through [`ChannelManager::handle_trampoline_htlc`], testing various MPP
71+
/// validation steps with a base case that succeeds.
72+
fn do_test_trampoline_mpp_validation(test_case: Option<TrampolineMppValidationTestCase>) {
73+
let update_add_value: u64 = 500_000; // Actual amount we received in update_add_htlc.
74+
let update_add_cltv: u32 = 500; // Actual CLTV we received in update_add_htlc.
75+
let sender_intended_incoming_value: u64 = 500_000; // Amount we expect for one HTLC, outer onion.
76+
let incoming_mpp_total: u64 = 1_000_000; // Total we expect to receive across MPP parts, outer onion.
77+
let mut next_trampoline_amount: u64 = 750_000; // Total next trampoline expects, inner onion.
78+
let mut next_trampoline_cltv: u32 = 100; // CLTV next trampoline expects, inner onion.
79+
80+
// By default, set our forwarding fee and CLTV delta to exactly what we're being offered
81+
// for this trampoline forward, so that we can force failures by just adding one.
82+
let mut forwarding_fee_base_msat = incoming_mpp_total - next_trampoline_amount;
83+
let mut cltv_delta = update_add_cltv - next_trampoline_cltv;
84+
let mut mismatch_payment_secret = false;
85+
86+
let expected = match test_case {
87+
Some(TrampolineMppValidationTestCase::FeeInsufficient) => {
88+
forwarding_fee_base_msat += 1;
89+
LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient
90+
},
91+
Some(TrampolineMppValidationTestCase::CltvInsufficient) => {
92+
cltv_delta += 1;
93+
LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient
94+
},
95+
Some(TrampolineMppValidationTestCase::TrampolineAmountExceedsReceived) => {
96+
next_trampoline_amount = incoming_mpp_total + 1;
97+
LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient
98+
},
99+
Some(TrampolineMppValidationTestCase::TrampolineCLTVExceedsReceived) => {
100+
next_trampoline_cltv = update_add_cltv + 1;
101+
LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient
102+
},
103+
Some(TrampolineMppValidationTestCase::MismatchedPaymentSecret) => {
104+
mismatch_payment_secret = true;
105+
LocalHTLCFailureReason::InvalidTrampolineForward
106+
},
107+
// We currently reject trampoline forwards once accumulated.
108+
None => LocalHTLCFailureReason::TemporaryTrampolineFailure,
109+
};
110+
111+
let chanmon_cfgs = create_chanmon_cfgs(1);
112+
let node_cfgs = create_node_cfgs(1, &chanmon_cfgs);
113+
let mut cfg = test_default_channel_config();
114+
cfg.channel_config.forwarding_fee_base_msat = forwarding_fee_base_msat as u32;
115+
cfg.channel_config.forwarding_fee_proportional_millionths = 0;
116+
cfg.channel_config.cltv_expiry_delta = cltv_delta as u16;
117+
let node_chanmgrs = create_node_chanmgrs(1, &node_cfgs, &[Some(cfg)]);
118+
let nodes = create_network(1, &node_cfgs, &node_chanmgrs);
119+
120+
let payment_hash = PaymentHash([1; 32]);
121+
122+
let secp = Secp256k1::new();
123+
let test_secret = SecretKey::from_slice(&[2; 32]).unwrap();
124+
let next_trampoline = PublicKey::from_secret_key(&secp, &test_secret);
125+
let next_hop_info = NextTrampolineHopInfo {
126+
onion_packet: test_trampoline_onion_packet(),
127+
blinding_point: None,
128+
amount_msat: next_trampoline_amount,
129+
cltv_expiry_height: next_trampoline_cltv,
130+
};
131+
132+
let htlc1 = ClaimableHTLC::new(
133+
test_prev_hop_data(),
134+
update_add_value,
135+
sender_intended_incoming_value,
136+
update_add_cltv,
137+
OnionPayload::Trampoline { next_hop_info: next_hop_info.clone(), next_trampoline },
138+
None,
139+
);
140+
assert!(nodes[0]
141+
.node
142+
.test_handle_trampoline_htlc(
143+
htlc1,
144+
test_onion_fields(incoming_mpp_total),
145+
payment_hash,
146+
next_hop_info.clone(),
147+
next_trampoline,
148+
)
149+
.is_ok());
150+
151+
let htlc2 = ClaimableHTLC::new(
152+
test_prev_hop_data(),
153+
update_add_value,
154+
sender_intended_incoming_value,
155+
update_add_cltv,
156+
OnionPayload::Trampoline { next_hop_info: next_hop_info.clone(), next_trampoline },
157+
None,
158+
);
159+
let onion2 = if mismatch_payment_secret {
160+
RecipientOnionFields {
161+
payment_secret: Some(PaymentSecret([1; 32])),
162+
total_mpp_amount_msat: incoming_mpp_total,
163+
payment_metadata: None,
164+
custom_tlvs: Vec::new(),
165+
}
166+
} else {
167+
test_onion_fields(incoming_mpp_total)
168+
};
169+
let result = nodes[0].node.test_handle_trampoline_htlc(
170+
htlc2,
171+
onion2,
172+
payment_hash,
173+
next_hop_info,
174+
next_trampoline,
175+
);
176+
177+
assert_eq!(
178+
HTLCHandlingFailureReason::from(&result.expect_err("expect trampoline failure").1),
179+
HTLCHandlingFailureReason::Local { reason: expected },
180+
);
181+
}
182+
183+
#[test]
184+
fn test_trampoline_mpp_validation() {
185+
do_test_trampoline_mpp_validation(Some(TrampolineMppValidationTestCase::FeeInsufficient));
186+
do_test_trampoline_mpp_validation(Some(TrampolineMppValidationTestCase::CltvInsufficient));
187+
do_test_trampoline_mpp_validation(Some(
188+
TrampolineMppValidationTestCase::TrampolineAmountExceedsReceived,
189+
));
190+
do_test_trampoline_mpp_validation(Some(
191+
TrampolineMppValidationTestCase::TrampolineCLTVExceedsReceived,
192+
));
193+
do_test_trampoline_mpp_validation(Some(
194+
TrampolineMppValidationTestCase::MismatchedPaymentSecret,
195+
));
196+
do_test_trampoline_mpp_validation(None);
197+
}

0 commit comments

Comments
 (0)