Skip to content

Commit 0931218

Browse files
committed
Construct forwarding onion [rephrase]
Construct Trampoline forwarding onion with throwaway session_priv…
1 parent 02a39bf commit 0931218

File tree

2 files changed

+321
-0
lines changed

2 files changed

+321
-0
lines changed

lightning/src/ln/blinded_payment_tests.rs

+162
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

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

0 commit comments

Comments
 (0)