Skip to content

Commit e41287a

Browse files
committed
Enforce Trampoline constraints
Trampoline onion amount/CLTV may not exceed outer onion.
1 parent c4d23bc commit e41287a

File tree

4 files changed

+101
-15
lines changed

4 files changed

+101
-15
lines changed

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 48 additions & 11 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,11 +2336,46 @@ 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(0x2000 | 26, &[0; 0]);
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]

lightning/src/ln/onion_payment.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ fn check_blinded_forward(
6161
Ok((amt_to_forward, outgoing_cltv_value))
6262
}
6363

64+
fn check_trampoline_sanity(outer_hop_data: &msgs::InboundTrampolineEntrypointPayload, trampoline_cltv_value: u32, trampoline_amount: u64) -> Result<(), ()> {
65+
if outer_hop_data.outgoing_cltv_value < trampoline_cltv_value {
66+
return Err(());
67+
}
68+
if outer_hop_data.amt_to_forward < trampoline_amount {
69+
return Err(());
70+
}
71+
Ok(())
72+
}
73+
6474
enum RoutingInfo {
6575
Direct {
6676
short_channel_id: u64,
@@ -121,7 +131,15 @@ pub(super) fn create_fwd_pending_htlc_info(
121131
err_code: 0x4000 | 22,
122132
err_data: Vec::new(),
123133
}),
124-
onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
134+
onion_utils::Hop::TrampolineForward { ref outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
135+
check_trampoline_sanity(outer_hop_data, next_trampoline_hop_data.outgoing_cltv_value, next_trampoline_hop_data.amt_to_forward).map_err(|()| {
136+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's
137+
InboundHTLCErr {
138+
msg: "Underflow calculating outbound amount or CLTV value for Trampoline forward",
139+
err_code: 0x2000 | 26,
140+
err_data: Vec::new(),
141+
}
142+
})?;
125143
(
126144
RoutingInfo::Trampoline {
127145
next_trampoline: next_trampoline_hop_data.next_trampoline,
@@ -136,7 +154,7 @@ pub(super) fn create_fwd_pending_htlc_info(
136154
None
137155
)
138156
},
139-
onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
157+
onion_utils::Hop::TrampolineBlindedForward { ref outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
140158
let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward(
141159
msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features
142160
).map_err(|()| {
@@ -148,6 +166,15 @@ pub(super) fn create_fwd_pending_htlc_info(
148166
err_data: vec![0; 32],
149167
}
150168
})?;
169+
check_trampoline_sanity(outer_hop_data, outgoing_cltv_value, amt_to_forward).map_err(|()| {
170+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's, but
171+
// we're inside a blinded path
172+
InboundHTLCErr {
173+
msg: "Underflow calculating outbound amount or CLTV value for Trampoline forward",
174+
err_code: INVALID_ONION_BLINDING,
175+
err_data: vec![0; 32],
176+
}
177+
})?;
151178
(
152179
RoutingInfo::Trampoline {
153180
next_trampoline: next_trampoline_hop_data.next_trampoline,
@@ -266,14 +293,25 @@ pub(super) fn create_recv_pending_htlc_info(
266293
intro_node_blinding_point.is_none(), true, invoice_request)
267294
}
268295
onion_utils::Hop::TrampolineReceive {
296+
ref outer_hop_data,
269297
trampoline_hop_data: msgs::InboundOnionReceivePayload {
270298
payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat,
271299
cltv_expiry_height, payment_metadata, ..
272300
}, ..
273-
} =>
301+
} => {
302+
check_trampoline_sanity(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat).map_err(|()| {
303+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's
304+
InboundHTLCErr {
305+
msg: "Underflow calculating skimmable amount or CLTV value for Trampoline receive",
306+
err_code: 0x2000 | 26,
307+
err_data: Vec::new(),
308+
}
309+
})?;
274310
(payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat,
275-
cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None),
311+
cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None)
312+
},
276313
onion_utils::Hop::TrampolineBlindedReceive {
314+
ref outer_hop_data,
277315
trampoline_hop_data: msgs::InboundOnionBlindedReceivePayload {
278316
sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, payment_secret,
279317
intro_node_blinding_point, payment_constraints, payment_context, keysend_preimage,
@@ -290,6 +328,15 @@ pub(super) fn create_recv_pending_htlc_info(
290328
msg: "Amount or cltv_expiry violated blinded payment constraints within Trampoline onion",
291329
}
292330
})?;
331+
check_trampoline_sanity(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat).map_err(|()| {
332+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's, but
333+
// we're inside a blinded path
334+
InboundHTLCErr {
335+
msg: "Underflow calculating skimmable amount or CLTV value for Trampoline receive",
336+
err_code: INVALID_ONION_BLINDING,
337+
err_data: vec![0; 32],
338+
}
339+
})?;
293340
let payment_data = msgs::FinalOnionHopData { payment_secret, total_msat };
294341
(Some(payment_data), keysend_preimage, custom_tlvs,
295342
sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context),

lightning/src/ln/onion_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,7 @@ impl HTLCFailReason {
15801580
else if failure_code == 21 { debug_assert!(data.is_empty()) }
15811581
else if failure_code == 22 | PERM { debug_assert!(data.len() <= 11) }
15821582
else if failure_code == 23 { debug_assert!(data.is_empty()) }
1583+
else if failure_code == 26 | NODE { debug_assert!(data.is_empty()) }
15831584
else if failure_code == INVALID_ONION_BLINDING { debug_assert_eq!(data.len(), 32) }
15841585
else if failure_code & BADONION != 0 {
15851586
// We set some bogus BADONION failure codes in test, so ignore unknown ones.

lightning/src/util/errors.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ pub(crate) fn get_onion_error_description(error_code: u16) -> (&'static str, &'s
123123
match error_code {
124124
_c if _c == PERM|1 => ("The realm byte was not understood by the processing node", "invalid_realm"),
125125
_c if _c == NODE|2 => ("Node indicated temporary node failure", "temporary_node_failure"),
126+
_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"),
126127
_c if _c == PERM|NODE|2 => ("Node indicated permanent node failure", "permanent_node_failure"),
127128
_c if _c == PERM|NODE|3 => ("Node indicated the required node feature is missing in the onion", "required_node_feature_missing"),
128129
_c if _c == BADONION|PERM|4 => ("Node indicated the version by is not understood", "invalid_onion_version"),

0 commit comments

Comments
 (0)