Skip to content

Commit 3515844

Browse files
a-mpcharik-so
andcommitted
Enforce Trampoline constraints
We add a `check_trampoline_contraints` similar to `check_blinded_path_constraints` that compares the Trampoline onion's amount and CLTV values to the limitations impose by outer onion. We also add the expected errors by the spec: `TemporaryTrampolineFailure`, `TrampolineFeeOrExpiryInsufficient` and `UnknownNextTrampoline` to be used in case of errors when doing forwarding or validating the constraints. Finally, we add and modified the following tests: - Modified the unblinded receive to validate when receiving amt less than the expected. - Modified test with wrong CLTV parameters that has fails with new enforcement of CTLV limits. - Add unblinded receive test that forces trampoline onion's CLTV to be greater than the the outer onion packet. Note that there are some TODO's to be fixed in following commits as we need the full trampoline forwarding feature to effectivelly test all caseos. Co-authored-by: Arik Sosman <[email protected]>
1 parent 381416a commit 3515844

File tree

4 files changed

+392
-17
lines changed

4 files changed

+392
-17
lines changed

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 298 additions & 12 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,237 @@ fn test_trampoline_single_hop_receive() {
22062206
do_test_trampoline_single_hop_receive(false);
22072207
}
22082208

2209+
fn do_test_trampoline_unblinded_receive(underpay: bool) {
2210+
// Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2)
2211+
2212+
const TOTAL_NODE_COUNT: usize = 3;
2213+
let secp_ctx = Secp256k1::new();
2214+
2215+
let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT);
2216+
let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs);
2217+
let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]);
2218+
let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs);
2219+
2220+
let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
2221+
let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
2222+
2223+
for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks
2224+
connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1);
2225+
}
2226+
2227+
let alice_node_id = nodes[0].node().get_our_node_id();
2228+
let bob_node_id = nodes[1].node().get_our_node_id();
2229+
let carol_node_id = nodes[2].node().get_our_node_id();
2230+
2231+
let alice_bob_scid = nodes[0].node().list_channels().iter().find(|c| c.channel_id == chan_id_alice_bob).unwrap().short_channel_id.unwrap();
2232+
let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap();
2233+
2234+
let amt_msat = 1000;
2235+
let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None);
2236+
let payee_tlvs = blinded_path::payment::TrampolineForwardTlvs {
2237+
next_trampoline: alice_node_id,
2238+
payment_constraints: PaymentConstraints {
2239+
max_cltv_expiry: u32::max_value(),
2240+
htlc_minimum_msat: amt_msat,
2241+
},
2242+
features: BlindedHopFeatures::empty(),
2243+
payment_relay: PaymentRelay {
2244+
cltv_expiry_delta: 0,
2245+
fee_proportional_millionths: 0,
2246+
fee_base_msat: 0,
2247+
},
2248+
next_blinding_override: None,
2249+
};
2250+
2251+
let carol_unblinded_tlvs = payee_tlvs.encode();
2252+
let path = [((carol_node_id, None), WithoutLength(&carol_unblinded_tlvs))];
2253+
let carol_alice_trampoline_session_priv = secret_from_hex("a0f4b8d7b6c2d0ffdfaf718f76e9decaef4d9fb38a8c4addb95c4007cc3eee03");
2254+
let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &carol_alice_trampoline_session_priv);
2255+
let carol_blinded_hops = blinded_path::utils::construct_blinded_hops(
2256+
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv,
2257+
).unwrap();
2258+
2259+
let route = Route {
2260+
paths: vec![Path {
2261+
hops: vec![
2262+
// Bob
2263+
RouteHop {
2264+
pubkey: bob_node_id,
2265+
node_features: NodeFeatures::empty(),
2266+
short_channel_id: alice_bob_scid,
2267+
channel_features: ChannelFeatures::empty(),
2268+
fee_msat: 1000,
2269+
cltv_expiry_delta: 48,
2270+
maybe_announced_channel: false,
2271+
},
2272+
2273+
// Carol
2274+
RouteHop {
2275+
pubkey: carol_node_id,
2276+
node_features: NodeFeatures::empty(),
2277+
short_channel_id: bob_carol_scid,
2278+
channel_features: ChannelFeatures::empty(),
2279+
fee_msat: 0, // no routing fees because it's the final hop
2280+
cltv_expiry_delta: 48,
2281+
maybe_announced_channel: false,
2282+
}
2283+
],
2284+
blinded_tail: Some(BlindedTail {
2285+
trampoline_hops: vec![
2286+
// Carol
2287+
TrampolineHop {
2288+
pubkey: carol_node_id,
2289+
node_features: Features::empty(),
2290+
fee_msat: 0, // no trampoline fee becuase we are receiving.
2291+
cltv_expiry_delta: 72, // blinded hop cltv to be used building the outer onion.
2292+
},
2293+
],
2294+
hops: carol_blinded_hops,
2295+
blinding_point: carol_blinding_point,
2296+
excess_final_cltv_expiry_delta: 39,
2297+
final_value_msat: amt_msat,
2298+
})
2299+
}],
2300+
route_params: None,
2301+
};
2302+
2303+
let payment_id = PaymentId(payment_hash.0);
2304+
2305+
nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap();
2306+
2307+
let replacement_onion = {
2308+
// create a substitute onion where the last Trampoline hop is an unblinded receive, which we
2309+
// (deliberately) do not support out of the box, therefore necessitating this workaround
2310+
let trampoline_secret_key = secret_from_hex("0134928f7b7ca6769080d70f16be84c812c741f545b49a34db47ce338a205799");
2311+
let prng_seed = secret_from_hex("fe02b4b9054302a3ddf4e1e9f7c411d644aebbd295218ab009dca94435f775a9");
2312+
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty();
2313+
2314+
let blinded_tail = route.paths[0].blinded_tail.clone().unwrap();
2315+
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();
2317+
// pop the last dummy hop
2318+
trampoline_payloads.pop();
2319+
let replacement_payload_amount = if underpay { amt_msat * 2 } else { amt_msat };
2320+
2321+
trampoline_payloads.push(msgs::OutboundTrampolinePayload::Receive {
2322+
payment_data: Some(msgs::FinalOnionHopData {
2323+
payment_secret,
2324+
total_msat: replacement_payload_amount,
2325+
}),
2326+
sender_intended_htlc_amt_msat: replacement_payload_amount,
2327+
// We will use the same cltv to the outer onion: 72 (blinded tail) + 32 (offset).
2328+
cltv_expiry_height: 104,
2329+
});
2330+
2331+
let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys(&secp_ctx, &route.paths[0].blinded_tail.as_ref().unwrap(), &trampoline_secret_key);
2332+
let trampoline_packet = onion_utils::construct_trampoline_onion_packet(
2333+
trampoline_payloads,
2334+
trampoline_onion_keys,
2335+
prng_seed.secret_bytes(),
2336+
&payment_hash,
2337+
None,
2338+
).unwrap();
2339+
2340+
// Get the original inner session private key that the ChannelManager generated so we can
2341+
// re-use it for the outer session private key. This way HMAC validation in attributable
2342+
// errors do not makes the test fail.
2343+
let mut orig_inner_priv_bytes = [0u8; 32];
2344+
nodes[0].node.test_modify_pending_payment(&payment_id, |pmt| {
2345+
if let crate::ln::outbound_payment::PendingOutboundPayment::Retryable { session_privs, .. } = pmt {
2346+
orig_inner_priv_bytes = *session_privs.iter().next().unwrap();
2347+
}
2348+
});
2349+
let inner_session_priv = SecretKey::from_slice(&orig_inner_priv_bytes).unwrap();
2350+
2351+
// Derive the outer session private key from the inner one.
2352+
let outer_session_priv_hash = Sha256::hash(&inner_session_priv.secret_bytes());
2353+
let outer_session_priv = SecretKey::from_slice(&outer_session_priv_hash.to_byte_array()).unwrap();
2354+
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();
2355+
let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv);
2356+
let outer_packet = onion_utils::construct_onion_packet(
2357+
outer_payloads,
2358+
outer_onion_keys,
2359+
prng_seed.secret_bytes(),
2360+
&payment_hash,
2361+
).unwrap();
2362+
2363+
outer_packet
2364+
};
2365+
2366+
check_added_monitors!(&nodes[0], 1);
2367+
2368+
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
2369+
assert_eq!(events.len(), 1);
2370+
let mut first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events);
2371+
let mut update_message = match first_message_event {
2372+
MessageSendEvent::UpdateHTLCs { ref mut updates, .. } => {
2373+
assert_eq!(updates.update_add_htlcs.len(), 1);
2374+
updates.update_add_htlcs.get_mut(0)
2375+
},
2376+
_ => panic!()
2377+
};
2378+
update_message.map(|msg| {
2379+
msg.onion_routing_packet = replacement_onion.clone();
2380+
});
2381+
2382+
let route: &[&Node] = &[&nodes[1], &nodes[2]];
2383+
let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event);
2384+
2385+
let args = if underpay {
2386+
args.with_payment_preimage(payment_preimage)
2387+
.without_claimable_event()
2388+
.expect_failure(HTLCHandlingFailureType::Receive { payment_hash })
2389+
} else {
2390+
args.with_payment_secret(payment_secret)
2391+
};
2392+
2393+
do_pass_along_path(args);
2394+
2395+
if underpay {
2396+
{
2397+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id());
2398+
nodes[1].node.handle_update_fail_htlc(
2399+
nodes[2].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2400+
);
2401+
do_commitment_signed_dance(&nodes[1], &nodes[2], &unblinded_node_updates.commitment_signed, true, false);
2402+
}
2403+
{
2404+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
2405+
nodes[0].node.handle_update_fail_htlc(
2406+
nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2407+
);
2408+
do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false);
2409+
}
2410+
{
2411+
let payment_failed_conditions = PaymentFailedConditions::new()
2412+
.expected_htlc_error_data(LocalHTLCFailureReason::FinalIncorrectHTLCAmount, &[0, 0, 0, 0, 0, 0, 3, 232]);
2413+
expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions);
2414+
}
2415+
} else {
2416+
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
2417+
}
2418+
}
2419+
2420+
#[test]
2421+
fn test_trampoline_unblinded_receive_underpay() {
2422+
do_test_trampoline_unblinded_receive(true);
2423+
}
2424+
22092425
#[test]
2210-
fn test_trampoline_unblinded_receive() {
2426+
fn test_trampoline_unblinded_receive_normal() {
2427+
do_test_trampoline_unblinded_receive(false);
2428+
}
2429+
2430+
#[derive(PartialEq)]
2431+
enum TrampolineConstraintFailureScenarios {
2432+
TrampolineCLTVGreaterThanOnion,
2433+
#[allow(dead_code)]
2434+
// TODO: To test amount greater than onion we need the ability
2435+
// to forward Trampoline payments.
2436+
TrampolineAmountGreaterThanOnion,
2437+
}
2438+
2439+
fn do_test_trampoline_unblinded_receive_constraint_failure(failure_scenario: TrampolineConstraintFailureScenarios) {
22112440
// Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2)
22122441

22132442
const TOTAL_NODE_COUNT: usize = 3;
@@ -2257,6 +2486,15 @@ fn test_trampoline_unblinded_receive() {
22572486
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv,
22582487
).unwrap();
22592488

2489+
// We decide an arbitrary ctlv delta for the blinded hop that will be the only cltv delta
2490+
// in the blinded tail.
2491+
let blinded_hop_cltv = if failure_scenario == TrampolineConstraintFailureScenarios::TrampolineCLTVGreaterThanOnion { 52 } else { 72 };
2492+
// Then when building the trampoline hop we use an arbitrary cltv delta offset to be used
2493+
// when re-building the outer trampoline onion.
2494+
let starting_cltv_offset_trampoline = 32;
2495+
// Finally we decide a forced cltv delta expiry for the trampoline hop itself.
2496+
// This one will be compared against the outer onion ctlv delta.
2497+
let forced_trampoline_cltv_delta = 104;
22602498
let route = Route {
22612499
paths: vec![Path {
22622500
hops: vec![
@@ -2277,7 +2515,7 @@ fn test_trampoline_unblinded_receive() {
22772515
node_features: NodeFeatures::empty(),
22782516
short_channel_id: bob_carol_scid,
22792517
channel_features: ChannelFeatures::empty(),
2280-
fee_msat: 0,
2518+
fee_msat: 0, // no routing fees because it's the final hop
22812519
cltv_expiry_delta: 48,
22822520
maybe_announced_channel: false,
22832521
}
@@ -2289,18 +2527,21 @@ fn test_trampoline_unblinded_receive() {
22892527
pubkey: carol_node_id,
22902528
node_features: Features::empty(),
22912529
fee_msat: amt_msat,
2292-
cltv_expiry_delta: 24,
2530+
cltv_expiry_delta: blinded_hop_cltv, // blinded tail ctlv delta.
22932531
},
22942532
],
22952533
hops: carol_blinded_hops,
22962534
blinding_point: carol_blinding_point,
2535+
// This will be ignored becase we force the cltv_expiry of the trampoline hop.
22972536
excess_final_cltv_expiry_delta: 39,
22982537
final_value_msat: amt_msat,
22992538
})
23002539
}],
23012540
route_params: None,
23022541
};
23032542

2543+
let payment_id = PaymentId(payment_hash.0);
2544+
23042545
nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap();
23052546

23062547
let replacement_onion = {
@@ -2311,18 +2552,17 @@ fn test_trampoline_unblinded_receive() {
23112552
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty();
23122553

23132554
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();
23152555

2556+
let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, amt_msat, &recipient_onion_fields, starting_cltv_offset_trampoline, &None).unwrap();
23162557
// pop the last dummy hop
23172558
trampoline_payloads.pop();
2318-
23192559
trampoline_payloads.push(msgs::OutboundTrampolinePayload::Receive {
23202560
payment_data: Some(msgs::FinalOnionHopData {
23212561
payment_secret,
23222562
total_msat: amt_msat,
23232563
}),
23242564
sender_intended_htlc_amt_msat: amt_msat,
2325-
cltv_expiry_height: 104,
2565+
cltv_expiry_height: forced_trampoline_cltv_delta,
23262566
});
23272567

23282568
let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys(&secp_ctx, &route.paths[0].blinded_tail.as_ref().unwrap(), &trampoline_secret_key);
@@ -2334,10 +2574,20 @@ fn test_trampoline_unblinded_receive() {
23342574
None,
23352575
).unwrap();
23362576

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");
2577+
// Get the original inner session private key that the ChannelManager generated so we can
2578+
// re-use it for the outer session private key. This way HMAC validation in attributable
2579+
// errors do not makes the test fail.
2580+
let mut orig_inner_priv_bytes = [0u8; 32];
2581+
nodes[0].node.test_modify_pending_payment(&payment_id, |pmt| {
2582+
if let crate::ln::outbound_payment::PendingOutboundPayment::Retryable { session_privs, .. } = pmt {
2583+
orig_inner_priv_bytes = *session_privs.iter().next().unwrap();
2584+
}
2585+
});
2586+
let inner_session_priv = SecretKey::from_slice(&orig_inner_priv_bytes).unwrap();
23402587

2588+
// Derive the outer session private key from the inner one.
2589+
let outer_session_priv_hash = Sha256::hash(&inner_session_priv.secret_bytes());
2590+
let outer_session_priv = SecretKey::from_slice(&outer_session_priv_hash.to_byte_array()).unwrap();
23412591
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();
23422592
let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv);
23432593
let outer_packet = onion_utils::construct_onion_packet(
@@ -2368,10 +2618,46 @@ fn test_trampoline_unblinded_receive() {
23682618

23692619
let route: &[&Node] = &[&nodes[1], &nodes[2]];
23702620
let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event)
2371-
.with_payment_secret(payment_secret);
2621+
.with_payment_preimage(payment_preimage)
2622+
.without_claimable_event()
2623+
.expect_failure(HTLCHandlingFailureType::Receive { payment_hash });
2624+
23722625
do_pass_along_path(args);
2626+
{
2627+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id());
2628+
nodes[1].node.handle_update_fail_htlc(
2629+
nodes[2].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2630+
);
2631+
do_commitment_signed_dance(&nodes[1], &nodes[2], &unblinded_node_updates.commitment_signed, true, false);
2632+
}
2633+
{
2634+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
2635+
nodes[0].node.handle_update_fail_htlc(
2636+
nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2637+
);
2638+
do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false);
2639+
}
23732640

2374-
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
2641+
match failure_scenario {
2642+
TrampolineConstraintFailureScenarios::TrampolineAmountGreaterThanOnion => {
2643+
let expected_error_data = amt_msat.to_be_bytes();
2644+
let payment_failed_conditions = PaymentFailedConditions::new()
2645+
.expected_htlc_error_data(LocalHTLCFailureReason::FinalIncorrectHTLCAmount, &expected_error_data);
2646+
expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions);
2647+
},
2648+
TrampolineConstraintFailureScenarios::TrampolineCLTVGreaterThanOnion => {
2649+
// The amount of the outer onion cltv delta plus the trampoline offset.
2650+
let expected_error_data = (blinded_hop_cltv + starting_cltv_offset_trampoline).to_be_bytes();
2651+
let payment_failed_conditions = PaymentFailedConditions::new()
2652+
.expected_htlc_error_data(LocalHTLCFailureReason::FinalIncorrectCLTVExpiry, &expected_error_data);
2653+
expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions);
2654+
}
2655+
}
2656+
}
2657+
2658+
#[test]
2659+
fn test_trampoline_enforced_constraint_cltv() {
2660+
do_test_trampoline_unblinded_receive_constraint_failure(TrampolineConstraintFailureScenarios::TrampolineCLTVGreaterThanOnion);
23752661
}
23762662

23772663
#[test]

lightning/src/ln/channelmanager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5121,7 +5121,7 @@ where
51215121
)
51225122
}
51235123

5124-
#[cfg(all(test, async_payments))]
5124+
#[cfg(test)]
51255125
pub(crate) fn test_modify_pending_payment<Fn>(&self, payment_id: &PaymentId, mut callback: Fn)
51265126
where
51275127
Fn: FnMut(&mut PendingOutboundPayment),

0 commit comments

Comments
 (0)