Skip to content

Commit 7a47922

Browse files
committed
Construct forwarding onion [rephrase]
Construct Trampoline forwarding onion with throwaway session_priv…
1 parent b5aa36b commit 7a47922

File tree

2 files changed

+321
-0
lines changed

2 files changed

+321
-0
lines changed

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,3 +2497,165 @@ fn test_trampoline_forward_rejection() {
24972497
expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions);
24982498
}
24992499
}
2500+
2501+
#[test]
2502+
fn test_unblinded_trampoline_forward() {
2503+
// Simulate a payment of A (0) -> B (1) -> C(Trampoline) (2) -> D(Trampoline(receive)) (3)
2504+
// trampoline hops C -> T0 (4) -> D
2505+
// make it fail at B, then at C's outer onion, then at C's inner onion
2506+
const TOTAL_NODE_COUNT: usize = 5;
2507+
let secp_ctx = Secp256k1::new();
2508+
2509+
let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT);
2510+
let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs);
2511+
let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]);
2512+
let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs);
2513+
2514+
let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
2515+
let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
2516+
let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 1_000_000, 0);
2517+
let (_, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 4, 3, 1_000_000, 0);
2518+
2519+
for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks
2520+
connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1);
2521+
}
2522+
2523+
let alice_node_id = nodes[0].node().get_our_node_id();
2524+
let bob_node_id = nodes[1].node().get_our_node_id();
2525+
let carol_node_id = nodes[2].node().get_our_node_id();
2526+
let dave_node_id = nodes[3].node().get_our_node_id();
2527+
2528+
let alice_bob_scid = nodes[0].node().list_channels().iter().find(|c| c.channel_id == chan_id_alice_bob).unwrap().short_channel_id.unwrap();
2529+
let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap();
2530+
2531+
let amt_msat = 1000;
2532+
let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[3], Some(amt_msat), None);
2533+
2534+
let route = Route {
2535+
paths: vec![Path {
2536+
hops: vec![
2537+
// Bob
2538+
RouteHop {
2539+
pubkey: bob_node_id,
2540+
node_features: NodeFeatures::empty(),
2541+
short_channel_id: alice_bob_scid,
2542+
channel_features: ChannelFeatures::empty(),
2543+
fee_msat: 1000, // forwarding fee to Carol
2544+
cltv_expiry_delta: 48,
2545+
maybe_announced_channel: false,
2546+
},
2547+
2548+
// Carol
2549+
RouteHop {
2550+
pubkey: carol_node_id,
2551+
node_features: NodeFeatures::empty(),
2552+
short_channel_id: bob_carol_scid,
2553+
channel_features: ChannelFeatures::empty(),
2554+
fee_msat: 2000, // fee for the usage of the entire blinded path, including Trampoline
2555+
cltv_expiry_delta: 48,
2556+
maybe_announced_channel: false,
2557+
}
2558+
],
2559+
blinded_tail: Some(BlindedTail {
2560+
trampoline_hops: vec![
2561+
// Carol
2562+
TrampolineHop {
2563+
pubkey: carol_node_id,
2564+
node_features: Features::empty(),
2565+
fee_msat: amt_msat,
2566+
cltv_expiry_delta: 176, // let her cook
2567+
},
2568+
2569+
// Dave (recipient)
2570+
TrampolineHop {
2571+
pubkey: dave_node_id,
2572+
node_features: Features::empty(),
2573+
fee_msat: 0, // no need to charge a fee as the recipient
2574+
cltv_expiry_delta: 24,
2575+
},
2576+
],
2577+
hops: vec![
2578+
// Dave's blinded node id
2579+
BlindedHop {
2580+
blinded_node_id: pubkey_from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be"),
2581+
encrypted_payload: bytes_from_hex("0ccf3c8a58deaa603f657ee2a5ed9d604eb5c8ca1e5f801989afa8f3ea6d789bbdde2c7e7a1ef9ca8c38d2c54760febad8446d3f273ddb537569ef56613846ccd3aba78a"),
2582+
}
2583+
],
2584+
blinding_point: alice_node_id,
2585+
excess_final_cltv_expiry_delta: 39,
2586+
final_value_msat: amt_msat,
2587+
})
2588+
}],
2589+
route_params: None,
2590+
};
2591+
2592+
nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap();
2593+
2594+
let replacement_onion = {
2595+
// create a substitute onion where the last Trampoline hop is an unblinded receive, which we
2596+
// (deliberately) do not support out of the box, therefore necessitating this workaround
2597+
let trampoline_secret_key = secret_from_hex("0134928f7b7ca6769080d70f16be84c812c741f545b49a34db47ce338a205799");
2598+
let prng_seed = secret_from_hex("fe02b4b9054302a3ddf4e1e9f7c411d644aebbd295218ab009dca94435f775a9");
2599+
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty();
2600+
2601+
let blinded_tail = route.paths[0].blinded_tail.clone().unwrap();
2602+
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();
2603+
2604+
// pop the last dummy hop
2605+
trampoline_payloads.pop();
2606+
2607+
trampoline_payloads.push(msgs::OutboundTrampolinePayload::Receive {
2608+
payment_data: Some(msgs::FinalOnionHopData {
2609+
payment_secret,
2610+
total_msat: amt_msat,
2611+
}),
2612+
sender_intended_htlc_amt_msat: amt_msat,
2613+
cltv_expiry_height: 96,
2614+
});
2615+
2616+
let trampoline_onion_keys = onion_utils::construct_trampoline_onion_keys(&secp_ctx, &route.paths[0].blinded_tail.as_ref().unwrap(), &trampoline_secret_key).unwrap();
2617+
let trampoline_packet = onion_utils::construct_trampoline_onion_packet(
2618+
trampoline_payloads,
2619+
trampoline_onion_keys,
2620+
prng_seed.secret_bytes(),
2621+
&payment_hash,
2622+
None,
2623+
).unwrap();
2624+
2625+
let outer_session_priv = secret_from_hex("e52c20461ed7acd46c4e7b591a37610519179482887bd73bf3b94617f8f03677");
2626+
2627+
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();
2628+
let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv).unwrap();
2629+
let outer_packet = onion_utils::construct_onion_packet(
2630+
outer_payloads,
2631+
outer_onion_keys,
2632+
prng_seed.secret_bytes(),
2633+
&payment_hash,
2634+
).unwrap();
2635+
2636+
outer_packet
2637+
};
2638+
2639+
check_added_monitors!(&nodes[0], 1);
2640+
2641+
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
2642+
assert_eq!(events.len(), 1);
2643+
let mut first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events);
2644+
let mut update_message = match first_message_event {
2645+
MessageSendEvent::UpdateHTLCs { ref mut updates, .. } => {
2646+
assert_eq!(updates.update_add_htlcs.len(), 1);
2647+
updates.update_add_htlcs.get_mut(0)
2648+
},
2649+
_ => panic!()
2650+
};
2651+
update_message.map(|msg| {
2652+
msg.onion_routing_packet = replacement_onion.clone();
2653+
});
2654+
2655+
let route: &[&Node] = &[&nodes[1], &nodes[2], &nodes[4], &nodes[3]];
2656+
let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event)
2657+
.with_payment_secret(payment_secret);
2658+
do_pass_along_path(args);
2659+
2660+
claim_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[4], &nodes[3]], payment_preimage);
2661+
}

lightning/src/ln/channelmanager.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6204,6 +6204,165 @@ where
62046204
}
62056205
None
62066206
},
6207+
HTLCForwardInfo::AddHTLC(PendingAddHTLCInfo {
6208+
prev_short_channel_id, prev_htlc_id, prev_channel_id, prev_funding_outpoint,
6209+
prev_user_channel_id, prev_counterparty_node_id, forward_info: PendingHTLCInfo {
6210+
incoming_shared_secret, payment_hash, outgoing_amt_msat, outgoing_cltv_value,
6211+
routing: PendingHTLCRouting::TrampolineForward {
6212+
ref onion_packet, blinded, incoming_cltv_expiry, ref hops, ..
6213+
}, skimmed_fee_msat, incoming_amt_msat
6214+
},
6215+
}) => {
6216+
let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData {
6217+
short_channel_id: prev_short_channel_id,
6218+
user_channel_id: Some(prev_user_channel_id),
6219+
counterparty_node_id: prev_counterparty_node_id,
6220+
channel_id: prev_channel_id,
6221+
outpoint: prev_funding_outpoint,
6222+
htlc_id: prev_htlc_id,
6223+
incoming_packet_shared_secret: incoming_shared_secret,
6224+
// Phantom payments are only PendingHTLCRouting::Receive.
6225+
phantom_shared_secret: None,
6226+
blinded_failure: blinded.map(|b| b.failure),
6227+
cltv_expiry: Some(incoming_cltv_expiry),
6228+
});
6229+
let next_blinding_point = blinded.and_then(|b| {
6230+
b.next_blinding_override.or_else(|| {
6231+
let encrypted_tlvs_ss = self.node_signer.ecdh(
6232+
Recipient::Node, &b.inbound_blinding_point, None
6233+
).unwrap().secret_bytes();
6234+
onion_utils::next_hop_pubkey(
6235+
&self.secp_ctx, b.inbound_blinding_point, &encrypted_tlvs_ss
6236+
).ok()
6237+
})
6238+
});
6239+
6240+
let mut full_outgoing_amt_msat = outgoing_amt_msat;
6241+
let mut full_outgoing_cltv = outgoing_cltv_value;
6242+
6243+
let outer_onion_packet = {
6244+
let path = Path {
6245+
hops: hops.clone(),
6246+
blinded_tail: None,
6247+
};
6248+
let recipient_onion = RecipientOnionFields::spontaneous_empty();
6249+
let (mut onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads(
6250+
&path,
6251+
outgoing_amt_msat,
6252+
&recipient_onion,
6253+
outgoing_cltv_value,
6254+
&None,
6255+
None,
6256+
None,
6257+
).unwrap();
6258+
6259+
if let Some(last_payload) = onion_payloads.last_mut() {
6260+
match last_payload {
6261+
msgs::OutboundOnionPayload::Receive { sender_intended_htlc_amt_msat, cltv_expiry_height, .. } => {
6262+
*last_payload = match next_blinding_point {
6263+
None => msgs::OutboundOnionPayload::TrampolineEntrypoint {
6264+
amt_to_forward: *sender_intended_htlc_amt_msat,
6265+
outgoing_cltv_value: *cltv_expiry_height,
6266+
multipath_trampoline_data: None,
6267+
trampoline_packet: onion_packet.clone(),
6268+
},
6269+
Some(blinding_point) => msgs::OutboundOnionPayload::BlindedTrampolineEntrypoint {
6270+
amt_to_forward: *sender_intended_htlc_amt_msat,
6271+
outgoing_cltv_value: *cltv_expiry_height,
6272+
multipath_trampoline_data: None,
6273+
trampoline_packet: onion_packet.clone(),
6274+
current_path_key: blinding_point,
6275+
}
6276+
};
6277+
}
6278+
_ => {
6279+
unreachable!("Last element must always initially be of type Receive.");
6280+
}
6281+
}
6282+
}
6283+
6284+
let outer_session_priv = SecretKey::from_slice(&self.entropy_source.get_secure_random_bytes()).unwrap();
6285+
let onion_keys = onion_utils::construct_onion_keys(&self.secp_ctx, &path, &outer_session_priv).map_err(|_| {
6286+
APIError::InvalidRoute { err: "Pubkey along hop was maliciously selected".to_owned() }
6287+
}).unwrap();
6288+
let outer_onion_prng_seed = self.entropy_source.get_secure_random_bytes();
6289+
let onion_packet = onion_utils::construct_onion_packet(onion_payloads, onion_keys, outer_onion_prng_seed, &payment_hash).unwrap();
6290+
6291+
full_outgoing_amt_msat = htlc_msat;
6292+
full_outgoing_cltv = htlc_cltv;
6293+
6294+
onion_packet
6295+
};
6296+
6297+
// Forward the HTLC over the most appropriate channel with the corresponding peer,
6298+
// applying non-strict forwarding.
6299+
// The channel with the least amount of outbound liquidity will be used to maximize the
6300+
// probability of being able to successfully forward a subsequent HTLC.
6301+
let maybe_optimal_channel = peer_state.channel_by_id.values_mut()
6302+
.filter_map(Channel::as_funded_mut)
6303+
.filter_map(|chan| {
6304+
let balances = chan.get_available_balances(&self.fee_estimator);
6305+
if full_outgoing_amt_msat <= balances.next_outbound_htlc_limit_msat &&
6306+
full_outgoing_amt_msat >= balances.next_outbound_htlc_minimum_msat &&
6307+
chan.context.is_usable() {
6308+
Some((chan, balances))
6309+
} else {
6310+
None
6311+
}
6312+
})
6313+
.min_by_key(|(_, balances)| balances.next_outbound_htlc_limit_msat).map(|(c, _)| c);
6314+
let optimal_channel = match maybe_optimal_channel {
6315+
Some(chan) => chan,
6316+
None => {
6317+
// Fall back to the specified channel to return an appropriate error.
6318+
if let Some(chan) = peer_state.channel_by_id
6319+
.get_mut(&forward_chan_id)
6320+
.and_then(Channel::as_funded_mut)
6321+
{
6322+
chan
6323+
} else {
6324+
forwarding_channel_not_found!(core::iter::once(forward_info).chain(draining_pending_forwards));
6325+
break;
6326+
}
6327+
}
6328+
};
6329+
6330+
let logger = WithChannelContext::from(&self.logger, &optimal_channel.context, Some(payment_hash));
6331+
let channel_description = if optimal_channel.context.get_short_channel_id() == Some(short_chan_id) {
6332+
"specified"
6333+
} else {
6334+
"alternate"
6335+
};
6336+
log_trace!(logger, "Forwarding HTLC from SCID {} with payment_hash {} and next hop SCID {} over {} channel {} with corresponding peer {}",
6337+
prev_short_channel_id, &payment_hash, short_chan_id, channel_description, optimal_channel.context.channel_id(), &counterparty_node_id);
6338+
if let Err(e) = optimal_channel.queue_add_htlc(full_outgoing_amt_msat,
6339+
payment_hash, full_outgoing_cltv, htlc_source.clone(),
6340+
outer_onion_packet.clone(), skimmed_fee_msat, next_blinding_point, &self.fee_estimator,
6341+
&&logger)
6342+
{
6343+
if let ChannelError::Ignore(msg) = e {
6344+
log_trace!(logger, "Failed to forward HTLC with payment_hash {} to peer {}: {}", &payment_hash, &counterparty_node_id, msg);
6345+
} else {
6346+
panic!("Stated return value requirements in send_htlc() were not met");
6347+
}
6348+
6349+
if let Some(chan) = peer_state.channel_by_id
6350+
.get_mut(&forward_chan_id)
6351+
.and_then(Channel::as_funded_mut)
6352+
{
6353+
let failure_code = 0x1000|7;
6354+
let data = self.get_htlc_inbound_temp_fail_data(failure_code);
6355+
failed_forwards.push((htlc_source, payment_hash,
6356+
HTLCFailReason::reason(failure_code, data),
6357+
HTLCDestination::NextHopChannel { node_id: Some(chan.context.get_counterparty_node_id()), channel_id: forward_chan_id }
6358+
));
6359+
} else {
6360+
forwarding_channel_not_found!(core::iter::once(forward_info).chain(draining_pending_forwards));
6361+
break;
6362+
}
6363+
}
6364+
None
6365+
},
62076366
HTLCForwardInfo::AddHTLC { .. } => {
62086367
panic!("short_channel_id != 0 should imply any pending_forward entries are of type Forward");
62096368
},

0 commit comments

Comments
 (0)