Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0b87404

Browse files
committedMar 27, 2025
Handle Trampoline receives
Handle payment receives with payment details located inside an inner onion.
1 parent fa0b3be commit 0b87404

File tree

2 files changed

+247
-1
lines changed

2 files changed

+247
-1
lines changed
 

Diff for: ‎lightning/src/ln/blinded_payment_tests.rs

+217
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use crate::util::test_utils;
4040
use lightning_invoice::RawBolt11Invoice;
4141
use types::features::Features;
4242
use crate::blinded_path::BlindedHop;
43+
use crate::routing::router::Route;
4344

4445
pub fn blinded_payment_path(
4546
payment_secret: PaymentSecret, intro_node_min_htlc: u64, intro_node_max_htlc: u64,
@@ -1957,3 +1958,219 @@ fn test_trampoline_inbound_payment_decoding() {
19571958
panic!();
19581959
};
19591960
}
1961+
1962+
fn do_test_trampoline_single_hop_receive(success: bool) {
1963+
const TOTAL_NODE_COUNT: usize = 3;
1964+
let secp_ctx = Secp256k1::new();
1965+
1966+
let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT);
1967+
let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs);
1968+
let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]);
1969+
let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs);
1970+
1971+
let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
1972+
let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
1973+
1974+
for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks
1975+
connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1);
1976+
}
1977+
1978+
let alice_node_id = nodes[0].node().get_our_node_id();
1979+
let bob_node_id = nodes[1].node().get_our_node_id();
1980+
let carol_node_id = nodes[2].node().get_our_node_id();
1981+
1982+
let alice_bob_scid = nodes[0].node().list_channels().iter().find(|c| c.channel_id == chan_id_alice_bob).unwrap().short_channel_id.unwrap();
1983+
let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap();
1984+
1985+
let amt_msat = 1000;
1986+
let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None);
1987+
1988+
let carol_alice_trampoline_session_priv = secret_from_hex("a0f4b8d7b6c2d0ffdfaf718f76e9decaef4d9fb38a8c4addb95c4007cc3eee03");
1989+
let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &carol_alice_trampoline_session_priv);
1990+
let carol_blinded_hops = if success {
1991+
let payee_tlvs = UnauthenticatedReceiveTlvs {
1992+
payment_secret,
1993+
payment_constraints: PaymentConstraints {
1994+
max_cltv_expiry: u32::max_value(),
1995+
htlc_minimum_msat: amt_msat,
1996+
},
1997+
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
1998+
};
1999+
2000+
let nonce = Nonce([42u8; 16]);
2001+
let expanded_key = nodes[2].keys_manager.get_inbound_payment_key();
2002+
let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key);
2003+
let carol_unblinded_tlvs = payee_tlvs.encode();
2004+
2005+
let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))];
2006+
blinded_path::utils::construct_blinded_hops(
2007+
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv
2008+
).unwrap()
2009+
} else {
2010+
let payee_tlvs = blinded_path::payment::TrampolineForwardTlvs {
2011+
next_trampoline: alice_node_id,
2012+
payment_constraints: PaymentConstraints {
2013+
max_cltv_expiry: u32::max_value(),
2014+
htlc_minimum_msat: amt_msat,
2015+
},
2016+
features: BlindedHopFeatures::empty(),
2017+
payment_relay: PaymentRelay {
2018+
cltv_expiry_delta: 0,
2019+
fee_proportional_millionths: 0,
2020+
fee_base_msat: 0,
2021+
},
2022+
next_blinding_override: None,
2023+
};
2024+
2025+
let carol_unblinded_tlvs = payee_tlvs.encode();
2026+
let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))];
2027+
blinded_path::utils::construct_blinded_hops(
2028+
&secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv
2029+
).unwrap()
2030+
};
2031+
2032+
let route = Route {
2033+
paths: vec![Path {
2034+
hops: vec![
2035+
// Bob
2036+
RouteHop {
2037+
pubkey: bob_node_id,
2038+
node_features: NodeFeatures::empty(),
2039+
short_channel_id: alice_bob_scid,
2040+
channel_features: ChannelFeatures::empty(),
2041+
fee_msat: 1000,
2042+
cltv_expiry_delta: 48,
2043+
maybe_announced_channel: false,
2044+
},
2045+
2046+
// Carol
2047+
RouteHop {
2048+
pubkey: carol_node_id,
2049+
node_features: NodeFeatures::empty(),
2050+
short_channel_id: bob_carol_scid,
2051+
channel_features: ChannelFeatures::empty(),
2052+
fee_msat: 0,
2053+
cltv_expiry_delta: 48,
2054+
maybe_announced_channel: false,
2055+
}
2056+
],
2057+
blinded_tail: Some(BlindedTail {
2058+
trampoline_hops: vec![
2059+
// Carol
2060+
TrampolineHop {
2061+
pubkey: carol_node_id,
2062+
node_features: Features::empty(),
2063+
fee_msat: amt_msat,
2064+
cltv_expiry_delta: 24,
2065+
},
2066+
],
2067+
hops: carol_blinded_hops,
2068+
blinding_point: carol_blinding_point,
2069+
excess_final_cltv_expiry_delta: 39,
2070+
final_value_msat: amt_msat,
2071+
})
2072+
}],
2073+
route_params: None,
2074+
};
2075+
2076+
nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap();
2077+
2078+
check_added_monitors!(&nodes[0], 1);
2079+
2080+
if success {
2081+
pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], amt_msat, payment_hash, payment_secret);
2082+
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
2083+
} else {
2084+
let replacement_onion = {
2085+
// create a substitute onion where the last Trampoline hop is a forward
2086+
let trampoline_secret_key = secret_from_hex("0134928f7b7ca6769080d70f16be84c812c741f545b49a34db47ce338a205799");
2087+
let prng_seed = secret_from_hex("fe02b4b9054302a3ddf4e1e9f7c411d644aebbd295218ab009dca94435f775a9");
2088+
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty();
2089+
2090+
let mut blinded_tail = route.paths[0].blinded_tail.clone().unwrap();
2091+
2092+
// append some dummy blinded hop so the intro hop looks like a forward
2093+
blinded_tail.hops.push(BlindedHop {
2094+
blinded_node_id: alice_node_id,
2095+
encrypted_payload: vec![],
2096+
});
2097+
2098+
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();
2099+
2100+
// pop the last dummy hop
2101+
trampoline_payloads.pop();
2102+
2103+
let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys(&secp_ctx, &route.paths[0].blinded_tail.as_ref().unwrap(), &trampoline_secret_key).unwrap();
2104+
let trampoline_packet = onion_utils::construct_trampoline_onion_packet(
2105+
trampoline_payloads,
2106+
trampoline_onion_keys,
2107+
prng_seed.secret_bytes(),
2108+
&payment_hash,
2109+
None,
2110+
).unwrap();
2111+
2112+
let outer_session_priv = secret_from_hex("e52c20461ed7acd46c4e7b591a37610519179482887bd73bf3b94617f8f03677");
2113+
2114+
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();
2115+
let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv).unwrap();
2116+
let outer_packet = onion_utils::construct_onion_packet(
2117+
outer_payloads,
2118+
outer_onion_keys,
2119+
prng_seed.secret_bytes(),
2120+
&payment_hash,
2121+
).unwrap();
2122+
2123+
outer_packet
2124+
};
2125+
2126+
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
2127+
assert_eq!(events.len(), 1);
2128+
let mut first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events);
2129+
let mut update_message = match first_message_event {
2130+
MessageSendEvent::UpdateHTLCs { ref mut updates, .. } => {
2131+
assert_eq!(updates.update_add_htlcs.len(), 1);
2132+
updates.update_add_htlcs.get_mut(0)
2133+
},
2134+
_ => panic!()
2135+
};
2136+
update_message.map(|msg| {
2137+
msg.onion_routing_packet = replacement_onion.clone();
2138+
});
2139+
2140+
let route: &[&Node] = &[&nodes[1], &nodes[2]];
2141+
let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event)
2142+
.with_payment_preimage(payment_preimage)
2143+
.without_claimable_event()
2144+
.expect_failure(HTLCDestination::InvalidOnion);
2145+
do_pass_along_path(args);
2146+
2147+
{
2148+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id());
2149+
nodes[1].node.handle_update_fail_htlc(
2150+
nodes[2].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2151+
);
2152+
do_commitment_signed_dance(&nodes[1], &nodes[2], &unblinded_node_updates.commitment_signed, true, false);
2153+
}
2154+
{
2155+
let unblinded_node_updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
2156+
nodes[0].node.handle_update_fail_htlc(
2157+
nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0]
2158+
);
2159+
do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false);
2160+
}
2161+
{
2162+
let payment_failed_conditions = PaymentFailedConditions::new()
2163+
.expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 0]);
2164+
expect_payment_failed_conditions(&nodes[0], payment_hash, true, payment_failed_conditions);
2165+
}
2166+
}
2167+
}
2168+
2169+
#[test]
2170+
fn test_trampoline_single_hop_receive() {
2171+
// Simulate a payment of A (0) -> B (1) -> C(Trampoline (blinded intro)) (2)
2172+
do_test_trampoline_single_hop_receive(true);
2173+
2174+
// Simulate a payment failure of A (0) -> B (1) -> C(Trampoline (blinded forward)) (2)
2175+
do_test_trampoline_single_hop_receive(false);
2176+
}

Diff for: ‎lightning/src/ln/onion_payment.rs

+30-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,36 @@ pub(super) fn create_recv_pending_htlc_info(
265265
sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context),
266266
intro_node_blinding_point.is_none(), true, invoice_request)
267267
}
268-
onion_utils::Hop::TrampolineReceive { .. } | onion_utils::Hop::TrampolineBlindedReceive { .. } => todo!(),
268+
onion_utils::Hop::TrampolineReceive {
269+
trampoline_hop_data: msgs::InboundOnionReceivePayload {
270+
payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat,
271+
cltv_expiry_height, payment_metadata, ..
272+
}, ..
273+
} =>
274+
(payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat,
275+
cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None),
276+
onion_utils::Hop::TrampolineBlindedReceive {
277+
trampoline_hop_data: msgs::InboundOnionBlindedReceivePayload {
278+
sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, payment_secret,
279+
intro_node_blinding_point, payment_constraints, payment_context, keysend_preimage,
280+
custom_tlvs, invoice_request
281+
}, ..
282+
} => {
283+
check_blinded_payment_constraints(
284+
sender_intended_htlc_amt_msat, cltv_expiry, &payment_constraints,
285+
)
286+
.map_err(|()| {
287+
InboundHTLCErr {
288+
err_code: INVALID_ONION_BLINDING,
289+
err_data: vec![0; 32],
290+
msg: "Amount or cltv_expiry violated blinded payment constraints within Trampoline onion",
291+
}
292+
})?;
293+
let payment_data = msgs::FinalOnionHopData { payment_secret, total_msat };
294+
(Some(payment_data), keysend_preimage, custom_tlvs,
295+
sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context),
296+
intro_node_blinding_point.is_none(), true, invoice_request)
297+
},
269298
onion_utils::Hop::Forward { .. } => {
270299
return Err(InboundHTLCErr {
271300
err_code: 0x4000|22,

0 commit comments

Comments
 (0)