Skip to content

Commit 29195fb

Browse files
a-mpcharik-so
andcommitted
Enforce Trampoline constraints
Ensure that the Trampolin onion's amount and CLTV values does not exceed the limitations imposed by the outer onion. Co-authored-by: Arik Sosman <[email protected]>
1 parent 664511b commit 29195fb

File tree

4 files changed

+185
-20
lines changed

4 files changed

+185
-20
lines changed

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,7 +2086,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) {
20862086
pubkey: carol_node_id,
20872087
node_features: Features::empty(),
20882088
fee_msat: amt_msat,
2089-
cltv_expiry_delta: 24,
2089+
cltv_expiry_delta: 104,
20902090
},
20912091
],
20922092
hops: carol_blinded_hops,
@@ -2206,8 +2206,7 @@ fn test_trampoline_single_hop_receive() {
22062206
do_test_trampoline_single_hop_receive(false);
22072207
}
22082208

2209-
#[test]
2210-
fn test_trampoline_unblinded_receive() {
2209+
fn do_test_trampoline_unblinded_receive(underpay: bool) {
22112210
// Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2)
22122211

22132212
const TOTAL_NODE_COUNT: usize = 3;
@@ -2277,7 +2276,7 @@ fn test_trampoline_unblinded_receive() {
22772276
node_features: NodeFeatures::empty(),
22782277
short_channel_id: bob_carol_scid,
22792278
channel_features: ChannelFeatures::empty(),
2280-
fee_msat: 0,
2279+
fee_msat: 0, // no routing fees because it's the final hop
22812280
cltv_expiry_delta: 48,
22822281
maybe_announced_channel: false,
22832282
}
@@ -2288,8 +2287,8 @@ fn test_trampoline_unblinded_receive() {
22882287
TrampolineHop {
22892288
pubkey: carol_node_id,
22902289
node_features: Features::empty(),
2291-
fee_msat: amt_msat,
2292-
cltv_expiry_delta: 24,
2290+
fee_msat: 0,
2291+
cltv_expiry_delta: 72,
22932292
},
22942293
],
22952294
hops: carol_blinded_hops,
@@ -2301,6 +2300,8 @@ fn test_trampoline_unblinded_receive() {
23012300
route_params: None,
23022301
};
23032302

2303+
let payment_id = PaymentId(payment_hash.0);
2304+
23042305
nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap();
23052306

23062307
let replacement_onion = {
@@ -2311,17 +2312,18 @@ fn test_trampoline_unblinded_receive() {
23112312
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty();
23122313

23132314
let blinded_tail = route.paths[0].blinded_tail.clone().unwrap();
2314-
let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, amt_msat, &recipient_onion_fields, 32, &None).unwrap();
23152315

2316+
let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, amt_msat, &recipient_onion_fields, 32, &None).unwrap();
23162317
// pop the last dummy hop
23172318
trampoline_payloads.pop();
2319+
let replacement_payload_amount = if underpay { amt_msat * 2 } else { amt_msat };
23182320

23192321
trampoline_payloads.push(msgs::OutboundTrampolinePayload::Receive {
23202322
payment_data: Some(msgs::FinalOnionHopData {
23212323
payment_secret,
2322-
total_msat: amt_msat,
2324+
total_msat: replacement_payload_amount,
23232325
}),
2324-
sender_intended_htlc_amt_msat: amt_msat,
2326+
sender_intended_htlc_amt_msat: replacement_payload_amount,
23252327
cltv_expiry_height: 104,
23262328
});
23272329

@@ -2334,10 +2336,20 @@ fn test_trampoline_unblinded_receive() {
23342336
None,
23352337
).unwrap();
23362338

2337-
// Use a different session key to construct the replacement onion packet. Note that the sender isn't aware of
2338-
// this and won't be able to decode the fulfill hold times.
2339-
let outer_session_priv = secret_from_hex("e52c20461ed7acd46c4e7b591a37610519179482887bd73bf3b94617f8f03677");
2339+
// Get the original inner session private key that the ChannelManager generated so we can
2340+
// re-use it for the outer session private key. This way HMAC validation in attributable
2341+
// errors do not makes the test fail.
2342+
let mut orig_inner_priv_bytes = [0u8; 32];
2343+
nodes[0].node.test_modify_pending_payment(&payment_id, |pmt| {
2344+
if let crate::ln::outbound_payment::PendingOutboundPayment::Retryable { session_privs, .. } = pmt {
2345+
orig_inner_priv_bytes = *session_privs.iter().next().unwrap();
2346+
}
2347+
});
2348+
let inner_session_priv = SecretKey::from_slice(&orig_inner_priv_bytes).unwrap();
23402349

2350+
// Derive the outer session private key from the inner one.
2351+
let outer_session_priv_hash = Sha256::hash(&inner_session_priv.secret_bytes());
2352+
let outer_session_priv = SecretKey::from_slice(&outer_session_priv_hash.to_byte_array()).unwrap();
23412353
let (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();
23422354
let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv);
23432355
let outer_packet = onion_utils::construct_onion_packet(
@@ -2367,11 +2379,51 @@ fn test_trampoline_unblinded_receive() {
23672379
});
23682380

23692381
let route: &[&Node] = &[&nodes[1], &nodes[2]];
2370-
let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event)
2371-
.with_payment_secret(payment_secret);
2382+
let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event);
2383+
2384+
let args = if underpay {
2385+
args.with_payment_preimage(payment_preimage)
2386+
.without_claimable_event()
2387+
.expect_failure(HTLCHandlingFailureType::Receive { payment_hash })
2388+
} else {
2389+
args.with_payment_secret(payment_secret)
2390+
};
2391+
23722392
do_pass_along_path(args);
23732393

2374-
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
2394+
if underpay {
2395+
{
2396+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id());
2397+
nodes[1].node.handle_update_fail_htlc(
2398+
nodes[2].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2399+
);
2400+
do_commitment_signed_dance(&nodes[1], &nodes[2], &unblinded_node_updates.commitment_signed, true, false);
2401+
}
2402+
{
2403+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
2404+
nodes[0].node.handle_update_fail_htlc(
2405+
nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2406+
);
2407+
do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false);
2408+
}
2409+
{
2410+
let payment_failed_conditions = PaymentFailedConditions::new()
2411+
.expected_htlc_error_data(LocalHTLCFailureReason::FinalIncorrectHTLCAmount, &[0, 0, 0, 0, 0, 0, 3, 232]);
2412+
expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions);
2413+
}
2414+
} else {
2415+
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
2416+
}
2417+
}
2418+
2419+
#[test]
2420+
fn test_trampoline_unblinded_receive_underpay() {
2421+
do_test_trampoline_unblinded_receive(true);
2422+
}
2423+
2424+
#[test]
2425+
fn test_trampoline_unblinded_receive_normal() {
2426+
do_test_trampoline_unblinded_receive(false);
23752427
}
23762428

23772429
#[test]

lightning/src/ln/channelmanager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5091,7 +5091,7 @@ where
50915091
)
50925092
}
50935093

5094-
#[cfg(all(test, async_payments))]
5094+
#[cfg(all(test))]
50955095
pub(crate) fn test_modify_pending_payment<Fn>(&self, payment_id: &PaymentId, mut callback: Fn)
50965096
where
50975097
Fn: FnMut(&mut PendingOutboundPayment),

lightning/src/ln/onion_payment.rs

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use crate::sign::{NodeSigner, Recipient};
2222
use crate::types::features::BlindedHopFeatures;
2323
use crate::types::payment::PaymentHash;
2424
use crate::util::logger::Logger;
25+
use crate::util::ser::Writeable;
2526

2627
#[allow(unused_imports)]
2728
use crate::prelude::*;
@@ -74,6 +75,24 @@ fn check_blinded_forward(
7475
Ok((amt_to_forward, outgoing_cltv_value))
7576
}
7677

78+
fn check_trampoline_onion_constraints(
79+
outer_hop_data: &msgs::InboundTrampolineEntrypointPayload, trampoline_cltv_value: u32,
80+
trampoline_amount: u64,
81+
) -> Result<(), LocalHTLCFailureReason> {
82+
if outer_hop_data.outgoing_cltv_value < trampoline_cltv_value {
83+
return Err(LocalHTLCFailureReason::FinalIncorrectCLTVExpiry);
84+
}
85+
let outgoing_amount = outer_hop_data
86+
.multipath_trampoline_data
87+
.as_ref()
88+
.map_or(outer_hop_data.amt_to_forward, |mtd| mtd.total_msat);
89+
if outgoing_amount < trampoline_amount {
90+
return Err(LocalHTLCFailureReason::FinalIncorrectHTLCAmount);
91+
}
92+
93+
Ok(())
94+
}
95+
7796
enum RoutingInfo {
7897
Direct {
7998
short_channel_id: u64,
@@ -135,7 +154,25 @@ pub(super) fn create_fwd_pending_htlc_info(
135154
reason: LocalHTLCFailureReason::InvalidOnionPayload,
136155
err_data: Vec::new(),
137156
}),
138-
onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
157+
onion_utils::Hop::TrampolineForward { ref outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
158+
check_trampoline_onion_constraints(outer_hop_data, next_trampoline_hop_data.outgoing_cltv_value, next_trampoline_hop_data.amt_to_forward).map_err(|reason| {
159+
let mut err_data = Vec::new();
160+
match reason {
161+
LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => {
162+
outer_hop_data.outgoing_cltv_value.write(&mut err_data).unwrap();
163+
}
164+
LocalHTLCFailureReason::FinalIncorrectHTLCAmount => {
165+
outer_hop_data.amt_to_forward.write(&mut err_data).unwrap();
166+
}
167+
_ => unreachable!()
168+
}
169+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's
170+
InboundHTLCErr {
171+
reason,
172+
err_data,
173+
msg: "Underflow calculating outbound amount or CLTV value for Trampoline forward",
174+
}
175+
})?;
139176
(
140177
RoutingInfo::Trampoline {
141178
next_trampoline: next_trampoline_hop_data.next_trampoline,
@@ -150,7 +187,7 @@ pub(super) fn create_fwd_pending_htlc_info(
150187
None
151188
)
152189
},
153-
onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
190+
onion_utils::Hop::TrampolineBlindedForward { ref outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
154191
let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward(
155192
msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features
156193
).map_err(|()| {
@@ -162,6 +199,15 @@ pub(super) fn create_fwd_pending_htlc_info(
162199
err_data: vec![0; 32],
163200
}
164201
})?;
202+
check_trampoline_onion_constraints(outer_hop_data, outgoing_cltv_value, amt_to_forward).map_err(|_| {
203+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's, but
204+
// we're inside a blinded path
205+
InboundHTLCErr {
206+
reason: LocalHTLCFailureReason::InvalidOnionBlinding,
207+
err_data: vec![0; 32],
208+
msg: "Underflow calculating outbound amount or CLTV value for Trampoline forward",
209+
}
210+
})?;
165211
(
166212
RoutingInfo::Trampoline {
167213
next_trampoline: next_trampoline_hop_data.next_trampoline,
@@ -281,14 +327,35 @@ pub(super) fn create_recv_pending_htlc_info(
281327
intro_node_blinding_point.is_none(), true, invoice_request)
282328
}
283329
onion_utils::Hop::TrampolineReceive {
330+
ref outer_hop_data,
284331
trampoline_hop_data: msgs::InboundOnionReceivePayload {
285332
payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat,
286333
cltv_expiry_height, payment_metadata, ..
287334
}, ..
288-
} =>
335+
} => {
336+
check_trampoline_onion_constraints(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat).map_err(|reason| {
337+
let mut err_data = Vec::new();
338+
match reason {
339+
LocalHTLCFailureReason::FinalIncorrectCLTVExpiry => {
340+
outer_hop_data.outgoing_cltv_value.write(&mut err_data).unwrap();
341+
}
342+
LocalHTLCFailureReason::FinalIncorrectHTLCAmount => {
343+
outer_hop_data.amt_to_forward.write(&mut err_data).unwrap();
344+
}
345+
_ => unreachable!()
346+
}
347+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's
348+
InboundHTLCErr {
349+
reason,
350+
err_data,
351+
msg: "Underflow calculating skimmable amount or CLTV value for Trampoline receive",
352+
}
353+
})?;
289354
(payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat,
290-
cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None),
355+
cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None)
356+
}
291357
onion_utils::Hop::TrampolineBlindedReceive {
358+
ref outer_hop_data,
292359
trampoline_hop_data: msgs::InboundOnionBlindedReceivePayload {
293360
sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, payment_secret,
294361
intro_node_blinding_point, payment_constraints, payment_context, keysend_preimage,
@@ -306,6 +373,15 @@ pub(super) fn create_recv_pending_htlc_info(
306373
}
307374
})?;
308375
let payment_data = msgs::FinalOnionHopData { payment_secret, total_msat };
376+
check_trampoline_onion_constraints(outer_hop_data, cltv_expiry_height, sender_intended_htlc_amt_msat).map_err(|_| {
377+
// The Trampoline onion's amt and CLTV values cannot exceed the outer onion's, but
378+
// we're inside a blinded path
379+
InboundHTLCErr {
380+
reason: LocalHTLCFailureReason::InvalidOnionBlinding,
381+
err_data: vec![0; 32],
382+
msg: "Underflow calculating skimmable amount or CLTV value for Trampoline receive",
383+
}
384+
})?;
309385
(Some(payment_data), keysend_preimage, custom_tlvs,
310386
sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context),
311387
intro_node_blinding_point.is_none(), true, invoice_request)
@@ -602,6 +678,25 @@ where
602678
outgoing_cltv_value,
603679
})
604680
}
681+
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, .. } => {
682+
let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward(
683+
msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features
684+
) {
685+
Ok((amt, cltv)) => (amt, cltv),
686+
Err(()) => {
687+
return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded forward",
688+
LocalHTLCFailureReason::InvalidOnionBlinding, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &[0; 32]);
689+
}
690+
};
691+
let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx,
692+
incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes());
693+
Some(NextPacketDetails {
694+
next_packet_pubkey: next_trampoline_packet_pubkey,
695+
outgoing_connector: HopConnector::Trampoline(next_trampoline),
696+
outgoing_amt_msat: amt_to_forward,
697+
outgoing_cltv_value,
698+
})
699+
}
605700
_ => None
606701
};
607702

lightning/src/ln/onion_utils.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,6 +1678,13 @@ pub enum LocalHTLCFailureReason {
16781678
HTLCMaximum,
16791679
/// The HTLC was failed because our remote peer is offline.
16801680
PeerOffline,
1681+
/// We have been unable to forward a payment to the next Trampoline node but may be able to
1682+
/// do it later.
1683+
TemporaryTrampolineFailure,
1684+
/// The amount or CLTV expiry were insufficient to route the payment to the next Trampoline.
1685+
TrampolineFeeOrExpiryInsufficient,
1686+
/// The specified next Trampoline node cannot be reached from our node.
1687+
UnkonwnNextTrampoline,
16811688
}
16821689

16831690
impl LocalHTLCFailureReason {
@@ -1718,6 +1725,9 @@ impl LocalHTLCFailureReason {
17181725
Self::InvalidOnionPayload | Self::InvalidTrampolinePayload => PERM | 22,
17191726
Self::MPPTimeout => 23,
17201727
Self::InvalidOnionBlinding => BADONION | PERM | 24,
1728+
Self::TemporaryTrampolineFailure => PERM | 25,
1729+
Self::TrampolineFeeOrExpiryInsufficient => PERM | 26,
1730+
Self::UnkonwnNextTrampoline => PERM | 27,
17211731
Self::UnknownFailureCode { code } => *code,
17221732
}
17231733
}
@@ -1852,6 +1862,9 @@ impl_writeable_tlv_based_enum!(LocalHTLCFailureReason,
18521862
(79, HTLCMinimum) => {},
18531863
(81, HTLCMaximum) => {},
18541864
(83, PeerOffline) => {},
1865+
(85, TemporaryTrampolineFailure) => {},
1866+
(87, TrampolineFeeOrExpiryInsufficient) => {},
1867+
(89, UnkonwnNextTrampoline) => {},
18551868
);
18561869

18571870
impl From<&HTLCFailReason> for HTLCHandlingFailureReason {
@@ -2018,6 +2031,11 @@ impl HTLCFailReason {
20182031
debug_assert!(false, "Unknown failure code: {}", code)
20192032
}
20202033
},
2034+
LocalHTLCFailureReason::TemporaryTrampolineFailure => debug_assert!(data.is_empty()),
2035+
LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient => {
2036+
debug_assert!(data.is_empty())
2037+
},
2038+
LocalHTLCFailureReason::UnkonwnNextTrampoline => debug_assert!(data.is_empty()),
20212039
}
20222040

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

0 commit comments

Comments
 (0)