Skip to content

Commit 4b4208d

Browse files
committed
Trampoline error decryption and vector tests
Create unit tests covering the hybrid outer and inner onion shared secrets, as well as the Trampoline error test vectors. Additionally, we allow the `outer_session_priv` to be overridden to accommodate the test vector requirements. We do so by separating out a façade method without the override.
1 parent 3da1c88 commit 4b4208d

File tree

1 file changed

+250
-20
lines changed

1 file changed

+250
-20
lines changed

lightning/src/ln/onion_utils.rs

Lines changed: 250 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -937,23 +937,54 @@ pub(crate) struct DecodedOnionFailure {
937937
pub(crate) onion_error_data: Option<Vec<u8>>,
938938
}
939939

940+
pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
941+
secp_ctx: &Secp256k1<T>, logger: &L, htlc_source: &HTLCSource,
942+
encrypted_packet: OnionErrorPacket,
943+
) -> DecodedOnionFailure
944+
where
945+
L::Target: Logger,
946+
{
947+
let (path, primary_session_priv) = match htlc_source {
948+
HTLCSource::OutboundRoute { ref path, ref session_priv, .. } => (path, session_priv),
949+
_ => unreachable!(),
950+
};
951+
952+
let (outer_session_priv, inner_session_priv) = if path.has_trampoline_hops() {
953+
// If we have Trampoline hops, the outer onion session_priv is a hash of the inner one.
954+
let session_priv_hash = Sha256::hash(&primary_session_priv.secret_bytes()).to_byte_array();
955+
(
956+
&SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!"),
957+
Some(primary_session_priv),
958+
)
959+
} else {
960+
(primary_session_priv, None)
961+
};
962+
963+
process_onion_failure_inner(
964+
secp_ctx,
965+
logger,
966+
htlc_source,
967+
outer_session_priv,
968+
inner_session_priv,
969+
encrypted_packet,
970+
)
971+
}
972+
940973
/// Process failure we got back from upstream on a payment we sent (implying htlc_source is an
941974
/// OutboundRoute).
942975
#[inline]
943-
pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
944-
secp_ctx: &Secp256k1<T>, logger: &L, htlc_source: &HTLCSource,
945-
mut encrypted_packet: OnionErrorPacket,
976+
pub(super) fn process_onion_failure_inner<T: secp256k1::Signing, L: Deref>(
977+
secp_ctx: &Secp256k1<T>, logger: &L, htlc_source: &HTLCSource, outer_session_priv: &SecretKey,
978+
inner_session_priv: Option<&SecretKey>, mut encrypted_packet: OnionErrorPacket,
946979
) -> DecodedOnionFailure
947980
where
948981
L::Target: Logger,
949982
{
950-
let (path, session_priv, first_hop_htlc_msat) = match htlc_source {
951-
HTLCSource::OutboundRoute {
952-
ref path, ref session_priv, ref first_hop_htlc_msat, ..
953-
} => (path, session_priv, first_hop_htlc_msat),
954-
_ => {
955-
unreachable!()
983+
let (path, first_hop_htlc_msat) = match htlc_source {
984+
HTLCSource::OutboundRoute { ref path, ref first_hop_htlc_msat, .. } => {
985+
(path, first_hop_htlc_msat)
956986
},
987+
_ => unreachable!(),
957988
};
958989

959990
// Learnings from the HTLC failure to inform future payment retries and scoring.
@@ -1002,12 +1033,6 @@ where
10021033
}
10031034
}
10041035

1005-
let outer_session_priv = path.has_trampoline_hops().then(|| {
1006-
// if we have Trampoline hops, the outer onion session_priv is a hash of the inner one
1007-
let session_priv_hash = Sha256::hash(&session_priv.secret_bytes()).to_byte_array();
1008-
SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!")
1009-
});
1010-
10111036
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
10121037

10131038
// We are first collecting all the unblinded `RouteHop`s inside `onion_keys`. Then, if applicable,
@@ -1018,7 +1043,7 @@ where
10181043
&path.hops,
10191044
// if we have Trampoline hops, the blinded hops are part of the inner Trampoline onion
10201045
if path.has_trampoline_hops() { None } else { path.blinded_tail.as_ref() },
1021-
outer_session_priv.as_ref().unwrap_or(session_priv),
1046+
outer_session_priv,
10221047
|shared_secret, _, _, route_hop_option: Option<&RouteHop>, _| {
10231048
onion_keys.push((route_hop_option.map(|rh| ErrorHop::RouteHop(rh)), shared_secret))
10241049
},
@@ -1031,7 +1056,7 @@ where
10311056
// Trampoline hops are part of the blinded tail, so this can never panic
10321057
&path.blinded_tail.as_ref().unwrap().trampoline_hops,
10331058
path.blinded_tail.as_ref(),
1034-
session_priv,
1059+
inner_session_priv.expect("Trampoline hops always have an inner session priv"),
10351060
|shared_secret, _, _, trampoline_hop_option: Option<&TrampolineHop>, _| {
10361061
onion_keys.push((
10371062
trampoline_hop_option.map(|th| ErrorHop::TrampolineHop(th)),
@@ -2039,11 +2064,11 @@ mod tests {
20392064
use crate::prelude::*;
20402065
use crate::util::test_utils::TestLogger;
20412066

2067+
use super::*;
20422068
use bitcoin::hex::FromHex;
20432069
use bitcoin::secp256k1::Secp256k1;
20442070
use bitcoin::secp256k1::{PublicKey, SecretKey};
2045-
2046-
use super::*;
2071+
use types::features::Features;
20472072

20482073
fn get_test_session_key() -> SecretKey {
20492074
let hex = "4141414141414141414141414141414141414141414141414141414141414141";
@@ -2400,10 +2425,215 @@ mod tests {
24002425
// Assert that the original failure can be retrieved and that all hmacs check out.
24012426
let decrypted_failure =
24022427
process_onion_failure(&ctx_full, &logger, &htlc_source, onion_error);
2403-
24042428
assert_eq!(decrypted_failure.onion_error_code, Some(0x2002));
24052429
}
24062430

2431+
fn build_trampoline_test_path() -> Path {
2432+
Path {
2433+
hops: vec![
2434+
// Bob
2435+
RouteHop {
2436+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()).unwrap(),
2437+
node_features: NodeFeatures::empty(),
2438+
short_channel_id: 0,
2439+
channel_features: ChannelFeatures::empty(),
2440+
fee_msat: 3_000,
2441+
cltv_expiry_delta: 24,
2442+
maybe_announced_channel: false,
2443+
},
2444+
2445+
// Carol
2446+
RouteHop {
2447+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(),
2448+
node_features: NodeFeatures::empty(),
2449+
short_channel_id: (572330 << 40) + (42 << 16) + 2821,
2450+
channel_features: ChannelFeatures::empty(),
2451+
fee_msat: 153_000,
2452+
cltv_expiry_delta: 0,
2453+
maybe_announced_channel: false,
2454+
},
2455+
],
2456+
blinded_tail: Some(BlindedTail {
2457+
trampoline_hops: vec![
2458+
// Carol's pubkey
2459+
TrampolineHop {
2460+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()).unwrap(),
2461+
node_features: Features::empty(),
2462+
fee_msat: 2_500,
2463+
cltv_expiry_delta: 24,
2464+
},
2465+
2466+
// Dave's pubkey
2467+
TrampolineHop {
2468+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145").unwrap()).unwrap(),
2469+
node_features: Features::empty(),
2470+
fee_msat: 2_500,
2471+
cltv_expiry_delta: 24,
2472+
},
2473+
2474+
// Emily's pubkey
2475+
TrampolineHop {
2476+
pubkey: PublicKey::from_slice(&<Vec<u8>>::from_hex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991").unwrap()).unwrap(),
2477+
node_features: Features::empty(),
2478+
fee_msat: 150_500,
2479+
cltv_expiry_delta: 36,
2480+
},
2481+
],
2482+
2483+
// Dummy blinded hop (because LDK doesn't allow unblinded Trampoline receives)
2484+
hops: vec![
2485+
// Emily's dummy blinded node id
2486+
BlindedHop {
2487+
blinded_node_id: PublicKey::from_slice(&<Vec<u8>>::from_hex("0295d40514096a8be54859e7dfe947b376eaafea8afe5cb4eb2c13ff857ed0b4be").unwrap()).unwrap(),
2488+
encrypted_payload: vec![],
2489+
}
2490+
],
2491+
blinding_point: PublicKey::from_slice(&<Vec<u8>>::from_hex("02988face71e92c345a068f740191fd8e53be14f0bb957ef730d3c5f76087b960e").unwrap()).unwrap(),
2492+
excess_final_cltv_expiry_delta: 0,
2493+
final_value_msat: 150_000_000,
2494+
}),
2495+
}
2496+
}
2497+
2498+
#[test]
2499+
fn test_trampoline_onion_error_cryptography() {
2500+
// TODO(arik): check intermediate hops' perspectives once we have implemented forwarding
2501+
2502+
let secp_ctx = Secp256k1::new();
2503+
let logger: Arc<TestLogger> = Arc::new(TestLogger::new());
2504+
let dummy_amt_msat = 150_000_000;
2505+
2506+
{
2507+
// test vector per https://github.com/lightning/bolts/blob/079f761bf68caa48544bd6bf0a29591d43425b0b/bolt04/trampoline-onion-error-test.json
2508+
// all dummy values
2509+
let trampoline_session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
2510+
let outer_session_priv = SecretKey::from_slice(&[4; 32]).unwrap();
2511+
2512+
let htlc_source = HTLCSource::OutboundRoute {
2513+
path: build_trampoline_test_path(),
2514+
session_priv: trampoline_session_priv,
2515+
first_hop_htlc_msat: dummy_amt_msat,
2516+
payment_id: PaymentId([1; 32]),
2517+
};
2518+
2519+
let error_packet_hex = "f8941a320b8fde4ad7b9b920c69cbf334114737497d93059d77e591eaa78d6334d3e2aeefcb0cc83402eaaf91d07d695cd895d9cad1018abdaf7d2a49d7657b1612729db7f393f0bb62b25afaaaa326d72a9214666025385033f2ec4605dcf1507467b5726d806da180ea224a7d8631cd31b0bdd08eead8bfe14fc8c7475e17768b1321b54dd4294aecc96da391efe0ca5bd267a45ee085c85a60cf9a9ac152fa4795fff8700a3ea4f848817f5e6943e855ab2e86f6929c9e885d8b20c49b14d2512c59ed21f10bd38691110b0d82c00d9fa48a20f10c7550358724c6e8e2b966e56a0aadf458695b273768062fa7c6e60eb72d4cdc67bf525c194e4a17fdcaa0e9d80480b586bf113f14eea530b6728a1c53fe5cee092e24a90f21f4b764015e7ed5e23";
2520+
let error_packet =
2521+
OnionErrorPacket { data: <Vec<u8>>::from_hex(error_packet_hex).unwrap() };
2522+
let decrypted_failure = process_onion_failure_inner(
2523+
&secp_ctx,
2524+
&logger,
2525+
&htlc_source,
2526+
&outer_session_priv,
2527+
Some(&trampoline_session_priv),
2528+
error_packet,
2529+
);
2530+
assert_eq!(decrypted_failure.onion_error_code, Some(0x400f));
2531+
}
2532+
2533+
{
2534+
// shared secret cryptography sanity tests
2535+
let session_priv = get_test_session_key();
2536+
let path = build_trampoline_test_path();
2537+
2538+
let trampoline_onion_keys = construct_trampoline_onion_keys(
2539+
&secp_ctx,
2540+
&path.blinded_tail.as_ref().unwrap(),
2541+
&session_priv,
2542+
)
2543+
.unwrap();
2544+
2545+
let outer_onion_keys = {
2546+
let session_priv_hash = Sha256::hash(&session_priv.secret_bytes()).to_byte_array();
2547+
let outer_session_priv = SecretKey::from_slice(&session_priv_hash[..]).unwrap();
2548+
construct_onion_keys(&Secp256k1::new(), &path, &outer_session_priv).unwrap()
2549+
};
2550+
2551+
let htlc_source = HTLCSource::OutboundRoute {
2552+
path,
2553+
session_priv,
2554+
first_hop_htlc_msat: dummy_amt_msat,
2555+
payment_id: PaymentId([1; 32]),
2556+
};
2557+
2558+
{
2559+
let error_code = 0x2002;
2560+
let mut first_hop_error_packet = build_unencrypted_failure_packet(
2561+
outer_onion_keys[0].shared_secret.as_ref(),
2562+
error_code,
2563+
&[0; 0],
2564+
);
2565+
2566+
crypt_failure_packet(
2567+
outer_onion_keys[0].shared_secret.as_ref(),
2568+
&mut first_hop_error_packet,
2569+
);
2570+
2571+
let decrypted_failure =
2572+
process_onion_failure(&secp_ctx, &logger, &htlc_source, first_hop_error_packet);
2573+
assert_eq!(decrypted_failure.onion_error_code, Some(error_code));
2574+
};
2575+
2576+
{
2577+
let error_code = 0x2003;
2578+
let mut trampoline_outer_hop_error_packet = build_unencrypted_failure_packet(
2579+
outer_onion_keys[1].shared_secret.as_ref(),
2580+
error_code,
2581+
&[0; 0],
2582+
);
2583+
2584+
crypt_failure_packet(
2585+
outer_onion_keys[1].shared_secret.as_ref(),
2586+
&mut trampoline_outer_hop_error_packet,
2587+
);
2588+
2589+
crypt_failure_packet(
2590+
outer_onion_keys[0].shared_secret.as_ref(),
2591+
&mut trampoline_outer_hop_error_packet,
2592+
);
2593+
2594+
let decrypted_failure = process_onion_failure(
2595+
&secp_ctx,
2596+
&logger,
2597+
&htlc_source,
2598+
trampoline_outer_hop_error_packet,
2599+
);
2600+
assert_eq!(decrypted_failure.onion_error_code, Some(error_code));
2601+
};
2602+
2603+
{
2604+
let error_code = 0x2004;
2605+
let mut trampoline_inner_hop_error_packet = build_unencrypted_failure_packet(
2606+
trampoline_onion_keys[0].shared_secret.as_ref(),
2607+
error_code,
2608+
&[0; 0],
2609+
);
2610+
2611+
crypt_failure_packet(
2612+
trampoline_onion_keys[0].shared_secret.as_ref(),
2613+
&mut trampoline_inner_hop_error_packet,
2614+
);
2615+
2616+
crypt_failure_packet(
2617+
outer_onion_keys[1].shared_secret.as_ref(),
2618+
&mut trampoline_inner_hop_error_packet,
2619+
);
2620+
2621+
crypt_failure_packet(
2622+
outer_onion_keys[0].shared_secret.as_ref(),
2623+
&mut trampoline_inner_hop_error_packet,
2624+
);
2625+
2626+
let decrypted_failure = process_onion_failure(
2627+
&secp_ctx,
2628+
&logger,
2629+
&htlc_source,
2630+
trampoline_inner_hop_error_packet,
2631+
);
2632+
assert_eq!(decrypted_failure.onion_error_code, Some(error_code));
2633+
}
2634+
}
2635+
}
2636+
24072637
#[test]
24082638
fn test_non_attributable_failure_packet_onion() {
24092639
// Create a failure packet with bogus data.

0 commit comments

Comments
 (0)