Skip to content

Commit df22b62

Browse files
committed
Enforce Trampoline constraints
Ensure that the Trampoline onion's amount and CLTV values do not exceed the limitations imposed by the outer onion.
1 parent 50f826f commit df22b62

File tree

4 files changed

+195
-19
lines changed

4 files changed

+195
-19
lines changed

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2061,7 +2061,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) {
20612061
pubkey: carol_node_id,
20622062
node_features: Features::empty(),
20632063
fee_msat: amt_msat,
2064-
cltv_expiry_delta: 24,
2064+
cltv_expiry_delta: 39,
20652065
},
20662066
],
20672067
hops: carol_blinded_hops,
@@ -2175,8 +2175,7 @@ fn test_trampoline_single_hop_receive() {
21752175
do_test_trampoline_single_hop_receive(false);
21762176
}
21772177

2178-
#[test]
2179-
fn test_trampoline_unblinded_receive() {
2178+
fn do_test_trampoline_unblinded_receive(underpay: bool) {
21802179
// Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2)
21812180

21822181
const TOTAL_NODE_COUNT: usize = 3;
@@ -2246,7 +2245,7 @@ fn test_trampoline_unblinded_receive() {
22462245
node_features: NodeFeatures::empty(),
22472246
short_channel_id: bob_carol_scid,
22482247
channel_features: ChannelFeatures::empty(),
2249-
fee_msat: 0,
2248+
fee_msat: 0, // no routing fees because it's the final hop
22502249
cltv_expiry_delta: 48,
22512250
maybe_announced_channel: false,
22522251
}
@@ -2257,8 +2256,8 @@ fn test_trampoline_unblinded_receive() {
22572256
TrampolineHop {
22582257
pubkey: carol_node_id,
22592258
node_features: Features::empty(),
2260-
fee_msat: amt_msat,
2261-
cltv_expiry_delta: 24,
2259+
fee_msat: 0,
2260+
cltv_expiry_delta: 72,
22622261
},
22632262
],
22642263
hops: carol_blinded_hops,
@@ -2270,6 +2269,8 @@ fn test_trampoline_unblinded_receive() {
22702269
route_params: None,
22712270
};
22722271

2272+
// outer 56
2273+
22732274
nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap();
22742275

22752276
let replacement_onion = {
@@ -2285,12 +2286,13 @@ fn test_trampoline_unblinded_receive() {
22852286
// pop the last dummy hop
22862287
trampoline_payloads.pop();
22872288

2289+
let replacement_payload_amount = if underpay { amt_msat * 2 } else { amt_msat };
22882290
trampoline_payloads.push(msgs::OutboundTrampolinePayload::Receive {
22892291
payment_data: Some(msgs::FinalOnionHopData {
22902292
payment_secret,
2291-
total_msat: amt_msat,
2293+
total_msat: replacement_payload_amount,
22922294
}),
2293-
sender_intended_htlc_amt_msat: amt_msat,
2295+
sender_intended_htlc_amt_msat: replacement_payload_amount,
22942296
cltv_expiry_height: 104,
22952297
});
22962298

@@ -2334,15 +2336,50 @@ fn test_trampoline_unblinded_receive() {
23342336
});
23352337

23362338
let route: &[&Node] = &[&nodes[1], &nodes[2]];
2337-
let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event)
2338-
.with_payment_secret(payment_secret);
2339+
let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event);
2340+
let args = if underpay {
2341+
args.with_payment_preimage(payment_preimage)
2342+
.without_claimable_event()
2343+
.expect_failure(HTLCDestination::FailedPayment { payment_hash })
2344+
} else {
2345+
args.with_payment_secret(payment_secret)
2346+
};
2347+
23392348
do_pass_along_path(args);
23402349

2341-
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
2350+
if underpay {
2351+
{
2352+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id());
2353+
nodes[1].node.handle_update_fail_htlc(
2354+
nodes[2].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2355+
);
2356+
do_commitment_signed_dance(&nodes[1], &nodes[2], &unblinded_node_updates.commitment_signed, true, false);
2357+
}
2358+
{
2359+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
2360+
nodes[0].node.handle_update_fail_htlc(
2361+
nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2362+
);
2363+
do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false);
2364+
}
2365+
{
2366+
let payment_failed_conditions = PaymentFailedConditions::new()
2367+
.expected_htlc_error_data(LocalHTLCFailureReason::FinalIncorrectHTLCAmount, &[0, 0, 0, 0, 0, 0, 3, 232]);
2368+
expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions);
2369+
}
2370+
} else {
2371+
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
2372+
}
2373+
}
2374+
2375+
#[test]
2376+
fn test_trampoline_unblinded_receive() {
2377+
do_test_trampoline_unblinded_receive(true);
2378+
do_test_trampoline_unblinded_receive(false);
23422379
}
23432380

23442381
#[test]
2345-
fn test_trampoline_forward_rejection() {
2382+
fn test_trampoline_constraint_enforcement() {
23462383
const TOTAL_NODE_COUNT: usize = 3;
23472384

23482385
let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT);
@@ -2435,7 +2472,7 @@ fn test_trampoline_forward_rejection() {
24352472
let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event)
24362473
.with_payment_preimage(payment_preimage)
24372474
.without_claimable_event()
2438-
.expect_failure(HTLCDestination::FailedPayment { payment_hash });
2475+
.expect_failure(HTLCDestination::InvalidOnion);
24392476
do_pass_along_path(args);
24402477

24412478
{
@@ -2455,7 +2492,7 @@ fn test_trampoline_forward_rejection() {
24552492
{
24562493
// Expect UnknownNextPeer error while we are unable to route forwarding Trampoline payments.
24572494
let payment_failed_conditions = PaymentFailedConditions::new()
2458-
.expected_htlc_error_data(LocalHTLCFailureReason::UnknownNextPeer, &[0; 0]);
2495+
.expected_htlc_error_data(LocalHTLCFailureReason::FinalIncorrectHTLCAmount, &[0, 0, 0, 0, 0, 0, 3, 232]);
24592496
expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions);
24602497
}
24612498
}

lightning/src/ln/onion_payment.rs

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::ln::onion_utils;
1919
use crate::ln::onion_utils::{HTLCFailReason, ONION_DATA_LEN, LocalHTLCFailureReason};
2020
use crate::sign::{NodeSigner, Recipient};
2121
use crate::util::logger::Logger;
22+
use crate::util::ser::Writeable;
2223

2324
#[allow(unused_imports)]
2425
use crate::prelude::*;
@@ -69,6 +70,16 @@ fn check_blinded_forward(
6970
Ok((amt_to_forward, outgoing_cltv_value))
7071
}
7172

73+
fn check_trampoline_onion_constraints(outer_hop_data: &msgs::InboundTrampolineEntrypointPayload, trampoline_cltv_value: u32, trampoline_amount: u64) -> Result<(), LocalHTLCFailureReason> {
74+
if outer_hop_data.outgoing_cltv_value < trampoline_cltv_value {
75+
return Err(LocalHTLCFailureReason::FinalIncorrectCLTVExpiry);
76+
}
77+
if outer_hop_data.multipath_trampoline_data.as_ref().map_or(outer_hop_data.amt_to_forward, |mtd| mtd.total_msat) < trampoline_amount {
78+
return Err(LocalHTLCFailureReason::FinalIncorrectHTLCAmount);
79+
}
80+
Ok(())
81+
}
82+
7283
enum RoutingInfo {
7384
Direct {
7485
short_channel_id: u64,
@@ -129,7 +140,25 @@ pub(super) fn create_fwd_pending_htlc_info(
129140
reason: LocalHTLCFailureReason::InvalidOnionPayload,
130141
err_data: Vec::new(),
131142
}),
132-
onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
143+
onion_utils::Hop::TrampolineForward { ref outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
144+
check_trampoline_onion_constraints(outer_hop_data, next_trampoline_hop_data.outgoing_cltv_value, next_trampoline_hop_data.amt_to_forward).map_err(|reason| {
145+
let mut err_data = Vec::new();
146+
match reason {
147+
LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => {
148+
outer_hop_data.outgoing_cltv_value.write(&mut err_data).unwrap();
149+
}
150+
LocalHTLCFailureReason::FinalIncorrectHTLCAmount => {
151+
outer_hop_data.amt_to_forward.write(&mut err_data).unwrap();
152+
}
153+
_ => unreachable!()
154+
}
155+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's
156+
InboundHTLCErr {
157+
reason,
158+
err_data,
159+
msg: "Underflow calculating outbound amount or CLTV value for Trampoline forward",
160+
}
161+
})?;
133162
(
134163
RoutingInfo::Trampoline {
135164
next_trampoline: next_trampoline_hop_data.next_trampoline,
@@ -144,7 +173,7 @@ pub(super) fn create_fwd_pending_htlc_info(
144173
None
145174
)
146175
},
147-
onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
176+
onion_utils::Hop::TrampolineBlindedForward { ref outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
148177
let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward(
149178
msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features
150179
).map_err(|()| {
@@ -156,6 +185,15 @@ pub(super) fn create_fwd_pending_htlc_info(
156185
err_data: vec![0; 32],
157186
}
158187
})?;
188+
check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward).map_err(|_| {
189+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's, but
190+
// we're inside a blinded path
191+
InboundHTLCErr {
192+
reason: LocalHTLCFailureReason::InvalidOnionBlinding,
193+
err_data: vec![0; 32],
194+
msg: "Underflow calculating outbound amount or CLTV value for Trampoline forward",
195+
}
196+
})?;
159197
(
160198
RoutingInfo::Trampoline {
161199
next_trampoline: next_trampoline_hop_data.next_trampoline,
@@ -274,14 +312,35 @@ pub(super) fn create_recv_pending_htlc_info(
274312
intro_node_blinding_point.is_none(), true, invoice_request)
275313
}
276314
onion_utils::Hop::TrampolineReceive {
315+
ref outer_hop_data,
277316
trampoline_hop_data: msgs::InboundOnionReceivePayload {
278317
payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat,
279318
cltv_expiry_height, payment_metadata, ..
280319
}, ..
281-
} =>
320+
} => {
321+
check_trampoline_onion_constraints(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat).map_err(|reason| {
322+
let mut err_data = Vec::new();
323+
match reason {
324+
LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => {
325+
outer_hop_data.outgoing_cltv_value.write(&mut err_data).unwrap();
326+
}
327+
LocalHTLCFailureReason::FinalIncorrectHTLCAmount => {
328+
outer_hop_data.amt_to_forward.write(&mut err_data).unwrap();
329+
}
330+
_ => unreachable!()
331+
}
332+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's
333+
InboundHTLCErr {
334+
reason,
335+
err_data,
336+
msg: "Underflow calculating skimmable amount or CLTV value for Trampoline receive",
337+
}
338+
})?;
282339
(payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat,
283-
cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None),
340+
cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None)
341+
},
284342
onion_utils::Hop::TrampolineBlindedReceive {
343+
ref outer_hop_data,
285344
trampoline_hop_data: msgs::InboundOnionBlindedReceivePayload {
286345
sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, payment_secret,
287346
intro_node_blinding_point, payment_constraints, payment_context, keysend_preimage,
@@ -298,6 +357,15 @@ pub(super) fn create_recv_pending_htlc_info(
298357
msg: "Amount or cltv_expiry violated blinded payment constraints within Trampoline onion",
299358
}
300359
})?;
360+
check_trampoline_onion_constraints(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat).map_err(|_| {
361+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's, but
362+
// we're inside a blinded path
363+
InboundHTLCErr {
364+
reason: LocalHTLCFailureReason::InvalidOnionBlinding,
365+
err_data: vec![0; 32],
366+
msg: "Underflow calculating skimmable amount or CLTV value for Trampoline receive",
367+
}
368+
})?;
301369
let payment_data = msgs::FinalOnionHopData { payment_secret, total_msat };
302370
(Some(payment_data), keysend_preimage, custom_tlvs,
303371
sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context),
@@ -583,7 +651,54 @@ where
583651
outgoing_cltv_value
584652
})
585653
}
586-
onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, trampoline_shared_secret, incoming_trampoline_public_key, .. } => {
654+
onion_utils::Hop::TrampolineForward { ref outer_hop_data, next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, outer_shared_secret, trampoline_shared_secret, incoming_trampoline_public_key, .. } => {
655+
if let Err(reason) = check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward) {
656+
let mut data = Vec::new();
657+
match reason {
658+
LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => {
659+
outer_hop_data.outgoing_cltv_value.write(&mut data).unwrap();
660+
}
661+
LocalHTLCFailureReason::FinalIncorrectHTLCAmount => {
662+
outer_hop_data.amt_to_forward.write(&mut data).unwrap();
663+
}
664+
_ => unreachable!()
665+
}
666+
return encode_relay_error("Underflow calculating outbound amount or CLTV value for Trampoline forward",
667+
reason, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &data);
668+
}
669+
let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx,
670+
incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes());
671+
Some(NextPacketDetails {
672+
next_packet_pubkey: next_trampoline_packet_pubkey,
673+
outgoing_connector: HopConnector::Trampoline(next_trampoline),
674+
outgoing_amt_msat: amt_to_forward,
675+
outgoing_cltv_value,
676+
})
677+
}
678+
onion_utils::Hop::TrampolineBlindedForward { ref outer_hop_data, 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, .. } => {
679+
let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward(
680+
msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features
681+
) {
682+
Ok((amt, cltv)) => (amt, cltv),
683+
Err(()) => {
684+
return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded forward",
685+
LocalHTLCFailureReason::InvalidOnionBlinding, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &[0; 32]);
686+
}
687+
};
688+
if let Err(reason) = check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward) {
689+
let mut data = Vec::new();
690+
match reason {
691+
LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => {
692+
outer_hop_data.outgoing_cltv_value.write(&mut data).unwrap();
693+
}
694+
LocalHTLCFailureReason::FinalIncorrectHTLCAmount => {
695+
outer_hop_data.amt_to_forward.write(&mut data).unwrap();
696+
}
697+
_ => unreachable!()
698+
}
699+
return encode_relay_error("Underflow calculating outbound amount or CLTV value for Trampoline forward",
700+
reason, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &data);
701+
}
587702
let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx,
588703
incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes());
589704
Some(NextPacketDetails {

lightning/src/ln/onion_utils.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,6 +1606,13 @@ pub enum LocalHTLCFailureReason {
16061606
HTLCMaximum,
16071607
/// The HTLC was failed because our remote peer is offline.
16081608
PeerOffline,
1609+
/// We have been unable to forward a payment to the next Trampoline node, but may be able to
1610+
/// later.
1611+
TemporaryTrampolineFailure,
1612+
/// The amount or CLTV expiry were insufficient to route the payment to the next Trampoline node.
1613+
TrampolineFeeOrExpiryInsufficient,
1614+
/// The specified next Trampoline node cannot be reached from our node.
1615+
UnknownNextTrampoline,
16091616
}
16101617

16111618
impl LocalHTLCFailureReason {
@@ -1647,6 +1654,9 @@ impl LocalHTLCFailureReason {
16471654
Self::InvalidOnionPayload | Self::InvalidTrampolinePayload => PERM | 22,
16481655
Self::MPPTimeout => 23,
16491656
Self::InvalidOnionBlinding => BADONION | PERM | 24,
1657+
Self::TemporaryTrampolineFailure => NODE | 25,
1658+
Self::TrampolineFeeOrExpiryInsufficient => NODE | 26,
1659+
Self::UnknownNextTrampoline => PERM | 27,
16501660
Self::UnknownFailureCode { code } => *code,
16511661
}
16521662
}
@@ -1707,6 +1717,12 @@ impl From<u16> for LocalHTLCFailureReason {
17071717
LocalHTLCFailureReason::MPPTimeout
17081718
} else if value == (BADONION | PERM | 24) {
17091719
LocalHTLCFailureReason::InvalidOnionBlinding
1720+
} else if value == (NODE | 25) {
1721+
LocalHTLCFailureReason::TemporaryTrampolineFailure
1722+
} else if value == (NODE | 26) {
1723+
LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient
1724+
} else if value == (PERM | 27) {
1725+
LocalHTLCFailureReason::UnknownNextTrampoline
17101726
} else {
17111727
LocalHTLCFailureReason::UnknownFailureCode { code: value }
17121728
}
@@ -1759,6 +1775,9 @@ impl_writeable_tlv_based_enum!(LocalHTLCFailureReason,
17591775
(81, HTLCMinimum) => {},
17601776
(83, HTLCMaximum) => {},
17611777
(85, PeerOffline) => {},
1778+
(87, TemporaryTrampolineFailure) => {},
1779+
(89, TrampolineFeeOrExpiryInsufficient) => {},
1780+
(91, UnknownNextTrampoline) => {},
17621781
);
17631782

17641783
#[derive(Clone)] // See Channel::revoke_and_ack for why, tl;dr: Rust bug
@@ -1915,6 +1934,9 @@ impl HTLCFailReason {
19151934
debug_assert!(false, "Unknown failure code: {}", code)
19161935
}
19171936
},
1937+
LocalHTLCFailureReason::TemporaryTrampolineFailure => debug_assert!(data.is_empty()),
1938+
LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient => debug_assert_eq!(data.len(), 10),
1939+
LocalHTLCFailureReason::UnknownNextTrampoline => debug_assert!(data.is_empty()),
19181940
}
19191941

19201942
Self(HTLCFailReasonRepr::Reason { data, failure_reason })

lightning/src/util/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ pub(crate) fn get_onion_error_description(error_code: u16) -> (&'static str, &'s
145145
_c if _c == 21 => ("Node indicated the CLTV expiry in the HTLC is too far in the future", "expiry_too_far"),
146146
_c if _c == PERM|22 => ("Node indicated that the decrypted onion per-hop payload was not understood by it or is incomplete", "invalid_onion_payload"),
147147
_c if _c == 23 => ("The final node indicated the complete amount of the multi-part payment was not received within a reasonable time", "mpp_timeout"),
148+
_c if _c == NODE|25 => ("The Trampoline node was unable to relay the payment to the subsequent Trampoline node.", "temporary_trampoline_failure"),
149+
_c if _c == NODE|26 => ("Node indicated the fee amount or CLTV value was below that required by the Trampoline node", "trampoline_fee_or_expiry_insufficient"),
148150
_ => ("Unknown", ""),
149151
}
150152
}

0 commit comments

Comments
 (0)