Skip to content

Commit 3ad2b6b

Browse files
committed
Construct forwarding onion [rephrase]
Construct Trampoline forwarding onion with throwaway session_priv…
1 parent 6603b47 commit 3ad2b6b

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

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.context.get_available_balances(&chan.funding, &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)