Skip to content

Commit 8739b06

Browse files
Implement route blinding spec test vectors.
1 parent 9dba36a commit 8739b06

File tree

3 files changed

+270
-10
lines changed

3 files changed

+270
-10
lines changed

lightning/src/blinded_path/payment.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ enum BlindedPaymentTlvsRef<'a> {
237237
/// Parameters for relaying over a given [`BlindedHop`].
238238
///
239239
/// [`BlindedHop`]: crate::blinded_path::BlindedHop
240-
#[derive(Clone, Debug)]
240+
#[derive(Clone, Debug, PartialEq)]
241241
pub struct PaymentRelay {
242242
/// Number of blocks subtracted from an incoming HTLC's `cltv_expiry` for this [`BlindedHop`].
243243
pub cltv_expiry_delta: u16,
@@ -251,7 +251,7 @@ pub struct PaymentRelay {
251251
/// Constraints for relaying over a given [`BlindedHop`].
252252
///
253253
/// [`BlindedHop`]: crate::blinded_path::BlindedHop
254-
#[derive(Clone, Debug)]
254+
#[derive(Clone, Debug, PartialEq)]
255255
pub struct PaymentConstraints {
256256
/// The maximum total CLTV that is acceptable when relaying a payment over this [`BlindedHop`].
257257
pub max_cltv_expiry: u32,

lightning/src/blinded_path/utils.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ where
109109
}
110110

111111
// Panics if `unblinded_tlvs` length is less than `unblinded_pks` length
112-
pub(super) fn construct_blinded_hops<'a, T, I1, I2>(
112+
pub(crate) fn construct_blinded_hops<'a, T, I1, I2>(
113113
secp_ctx: &Secp256k1<T>, unblinded_pks: I1, mut unblinded_tlvs: I2, session_priv: &SecretKey
114114
) -> Result<Vec<BlindedHop>, secp256k1::Error>
115115
where

lightning/src/ln/blinded_payment_tests.rs

+267-7
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,33 @@
77
// You may not use this file except in accordance with one or both of these
88
// licenses.
99

10-
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
10+
use bitcoin::hashes::hex::FromHex;
11+
use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, schnorr};
12+
use bitcoin::secp256k1::ecdh::SharedSecret;
13+
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
14+
use crate::blinded_path;
1115
use crate::blinded_path::payment::{BlindedPaymentPath, ForwardNode, ForwardTlvs, PaymentConstraints, PaymentContext, PaymentRelay, ReceiveTlvs};
1216
use crate::events::{Event, HTLCDestination, MessageSendEvent, MessageSendEventsProvider, PaymentFailureReason};
13-
use crate::ln::types::PaymentSecret;
17+
use crate::ln::types::{ChannelId, PaymentHash, PaymentSecret};
1418
use crate::ln::channelmanager;
15-
use crate::ln::channelmanager::{PaymentId, RecipientOnionFields};
16-
use crate::ln::features::BlindedHopFeatures;
19+
use crate::ln::channelmanager::{HTLCFailureMsg, PaymentId, RecipientOnionFields};
20+
use crate::ln::features::{BlindedHopFeatures, ChannelFeatures, NodeFeatures};
1721
use crate::ln::functional_test_utils::*;
1822
use crate::ln::msgs;
19-
use crate::ln::msgs::ChannelMessageHandler;
23+
use crate::ln::msgs::{ChannelMessageHandler, UnsignedGossipMessage};
24+
use crate::ln::onion_payment;
2025
use crate::ln::onion_utils;
2126
use crate::ln::onion_utils::INVALID_ONION_BLINDING;
2227
use crate::ln::outbound_payment::{Retry, IDEMPOTENCY_TIMEOUT_TICKS};
23-
use crate::offers::invoice::BlindedPayInfo;
28+
use crate::offers::invoice::{BlindedPayInfo, UnsignedBolt12Invoice};
29+
use crate::offers::invoice_request::UnsignedInvoiceRequest;
2430
use crate::prelude::*;
25-
use crate::routing::router::{Payee, PaymentParameters, RouteParameters};
31+
use crate::routing::router::{BlindedTail, Path, Payee, PaymentParameters, RouteHop, RouteParameters};
32+
use crate::sign::{KeyMaterial, NodeSigner, Recipient};
2633
use crate::util::config::UserConfig;
34+
use crate::util::ser::WithoutLength;
2735
use crate::util::test_utils;
36+
use lightning_invoice::RawBolt11Invoice;
2837

2938
fn blinded_payment_path(
3039
payment_secret: PaymentSecret, intro_node_min_htlc: u64, intro_node_max_htlc: u64,
@@ -1332,3 +1341,254 @@ fn custom_tlvs_to_blinded_path() {
13321341
.with_custom_tlvs(recipient_onion_fields.custom_tlvs.clone())
13331342
);
13341343
}
1344+
1345+
fn secret_from_hex(hex: &str) -> SecretKey {
1346+
SecretKey::from_slice(&<Vec<u8>>::from_hex(hex).unwrap()).unwrap()
1347+
}
1348+
1349+
fn bytes_from_hex(hex: &str) -> Vec<u8> {
1350+
<Vec<u8>>::from_hex(hex).unwrap()
1351+
}
1352+
1353+
fn pubkey_from_hex(hex: &str) -> PublicKey {
1354+
PublicKey::from_slice(&<Vec<u8>>::from_hex(hex).unwrap()).unwrap()
1355+
}
1356+
1357+
fn update_add_msg(
1358+
amount_msat: u64, cltv_expiry: u32, blinding_point: Option<PublicKey>,
1359+
onion_routing_packet: msgs::OnionPacket
1360+
) -> msgs::UpdateAddHTLC {
1361+
msgs::UpdateAddHTLC {
1362+
channel_id: ChannelId::from_bytes([0; 32]),
1363+
htlc_id: 0,
1364+
amount_msat,
1365+
cltv_expiry,
1366+
payment_hash: PaymentHash([0; 32]),
1367+
onion_routing_packet,
1368+
skimmed_fee_msat: None,
1369+
blinding_point,
1370+
}
1371+
}
1372+
1373+
#[test]
1374+
fn route_blinding_spec_test_vector() {
1375+
let mut secp_ctx = Secp256k1::new();
1376+
let bob_secret = secret_from_hex("4242424242424242424242424242424242424242424242424242424242424242");
1377+
let bob_node_id = PublicKey::from_secret_key(&secp_ctx, &bob_secret);
1378+
let bob_unblinded_tlvs = bytes_from_hex("011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456");
1379+
let carol_secret = secret_from_hex("4343434343434343434343434343434343434343434343434343434343434343");
1380+
let carol_node_id = PublicKey::from_secret_key(&secp_ctx, &carol_secret);
1381+
let carol_unblinded_tlvs = bytes_from_hex("020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00");
1382+
let dave_secret = secret_from_hex("4444444444444444444444444444444444444444444444444444444444444444");
1383+
let dave_node_id = PublicKey::from_secret_key(&secp_ctx, &dave_secret);
1384+
let dave_unblinded_tlvs = bytes_from_hex("01230000000000000000000000000000000000000000000000000000000000000000000000020800000000000002310a060090000000fa0c06000b699105dc0e00");
1385+
let eve_secret = secret_from_hex("4545454545454545454545454545454545454545454545454545454545454545");
1386+
let eve_node_id = PublicKey::from_secret_key(&secp_ctx, &eve_secret);
1387+
let eve_unblinded_tlvs = bytes_from_hex("011a00000000000000000000000000000000000000000000000000000604deadbeef0c06000b690105dc0e0f020000000000000000000000000000fdffff0206c1");
1388+
1389+
// Eve creates a blinded path to herself through Dave:
1390+
let dave_eve_session_priv = secret_from_hex("0101010101010101010101010101010101010101010101010101010101010101");
1391+
let blinding_override = PublicKey::from_secret_key(&secp_ctx, &dave_eve_session_priv);
1392+
assert_eq!(blinding_override, pubkey_from_hex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"));
1393+
// Can't use the public API here as the encrypted payloads contain unknown TLVs.
1394+
let mut dave_eve_blinded_hops = blinded_path::utils::construct_blinded_hops(
1395+
&secp_ctx, [dave_node_id, eve_node_id].iter(),
1396+
&mut [WithoutLength(&dave_unblinded_tlvs), WithoutLength(&eve_unblinded_tlvs)].iter(),
1397+
&dave_eve_session_priv
1398+
).unwrap();
1399+
1400+
// Concatenate an additional Bob -> Carol blinded path to the Eve -> Dave blinded path.
1401+
let bob_carol_session_priv = secret_from_hex("0202020202020202020202020202020202020202020202020202020202020202");
1402+
let bob_blinding_point = PublicKey::from_secret_key(&secp_ctx, &bob_carol_session_priv);
1403+
let bob_carol_blinded_hops = blinded_path::utils::construct_blinded_hops(
1404+
&secp_ctx, [bob_node_id, carol_node_id].iter(),
1405+
&mut [WithoutLength(&bob_unblinded_tlvs), WithoutLength(&carol_unblinded_tlvs)].iter(),
1406+
&bob_carol_session_priv
1407+
).unwrap();
1408+
1409+
let mut blinded_hops = bob_carol_blinded_hops;
1410+
blinded_hops.append(&mut dave_eve_blinded_hops);
1411+
assert_eq!(
1412+
vec![
1413+
pubkey_from_hex("03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25"),
1414+
pubkey_from_hex("02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7"),
1415+
pubkey_from_hex("036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf"),
1416+
pubkey_from_hex("021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae")
1417+
],
1418+
blinded_hops.iter().map(|bh| bh.blinded_node_id).collect::<Vec<PublicKey>>()
1419+
);
1420+
assert_eq!(
1421+
vec![
1422+
bytes_from_hex("cd4100ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c49982088b49f2e70b99287fdee0aa58aa39913ab405813b999f66783aa2fe637b3cda91ffc0913c30324e2c6ce327e045183e4bffecb"),
1423+
bytes_from_hex("cc0f16524fd7f8bb0b1d8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f57ff62da5aaec5d7b10d59b04d8a9d77e472b9b3ecc2179334e411be22fa4c02b467c7e"),
1424+
bytes_from_hex("0fa0a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9c43815fd4bcebf6f58c546da0cd8a9bf5cebd0d554802f6c0255e28e4a27343f761fe518cd897463187991105"),
1425+
bytes_from_hex("da1a7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60724a2e4d3f0489ad884f7f3f77149209f0df51efd6b276294a02e3949c7254fbc8b5cab58212d9a78983e1cf86fe218b30c4ca8f6d8")
1426+
],
1427+
blinded_hops.iter().map(|bh| bh.encrypted_payload.clone()).collect::<Vec<Vec<u8>>>()
1428+
);
1429+
1430+
let mut amt_msat = 100_000;
1431+
let session_priv = secret_from_hex("0303030303030303030303030303030303030303030303030303030303030303");
1432+
let path = Path {
1433+
hops: vec![RouteHop {
1434+
pubkey: bob_node_id,
1435+
node_features: NodeFeatures::empty(),
1436+
short_channel_id: 42,
1437+
channel_features: ChannelFeatures::empty(),
1438+
fee_msat: 100,
1439+
cltv_expiry_delta: 42,
1440+
maybe_announced_channel: false,
1441+
}],
1442+
blinded_tail: Some(BlindedTail {
1443+
hops: blinded_hops,
1444+
blinding_point: bob_blinding_point,
1445+
excess_final_cltv_expiry_delta: 0,
1446+
final_value_msat: amt_msat
1447+
}),
1448+
};
1449+
let cur_height = 747_000;
1450+
let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, amt_msat, &RecipientOnionFields::spontaneous_empty(), cur_height, &PaymentHash([0; 32]), &None, [0; 32]).unwrap();
1451+
1452+
struct TestEcdhSigner {
1453+
node_secret: SecretKey,
1454+
}
1455+
impl NodeSigner for TestEcdhSigner {
1456+
fn ecdh(
1457+
&self, _recipient: Recipient, other_key: &PublicKey, tweak: Option<&Scalar>,
1458+
) -> Result<SharedSecret, ()> {
1459+
let mut node_secret = self.node_secret.clone();
1460+
if let Some(tweak) = tweak {
1461+
node_secret = self.node_secret.mul_tweak(tweak).map_err(|_| ())?;
1462+
}
1463+
Ok(SharedSecret::new(other_key, &node_secret))
1464+
}
1465+
fn get_inbound_payment_key_material(&self) -> KeyMaterial { unreachable!() }
1466+
fn get_node_id(&self, _recipient: Recipient) -> Result<PublicKey, ()> { unreachable!() }
1467+
fn sign_invoice(
1468+
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient,
1469+
) -> Result<RecoverableSignature, ()> { unreachable!() }
1470+
fn sign_bolt12_invoice_request(
1471+
&self, _invoice_request: &UnsignedInvoiceRequest,
1472+
) -> Result<schnorr::Signature, ()> { unreachable!() }
1473+
fn sign_bolt12_invoice(
1474+
&self, _invoice: &UnsignedBolt12Invoice,
1475+
) -> Result<schnorr::Signature, ()> { unreachable!() }
1476+
fn sign_gossip_message(&self, _msg: UnsignedGossipMessage) -> Result<Signature, ()> { unreachable!() }
1477+
}
1478+
let logger = test_utils::TestLogger::with_id("".to_owned());
1479+
1480+
let bob_update_add = update_add_msg(110_000, 747_500, None, bob_onion);
1481+
let bob_node_signer = TestEcdhSigner { node_secret: bob_secret };
1482+
// Can't use the public API here as we need to avoid the CLTV delta checks (test vector uses
1483+
// < MIN_CLTV_EXPIRY_DELTA).
1484+
let (bob_peeled_onion, _, next_packet_details_opt) =
1485+
match onion_payment::decode_incoming_update_add_htlc_onion(
1486+
&bob_update_add, &&bob_node_signer, &&logger, &secp_ctx
1487+
) {
1488+
Ok(res) => res,
1489+
_ => panic!("Unexpected error")
1490+
};
1491+
let (carol_packet_bytes, carol_hmac) = if let onion_utils::Hop::Forward {
1492+
next_hop_data: msgs::InboundOnionPayload::BlindedForward {
1493+
short_channel_id, payment_relay, payment_constraints, features, intro_node_blinding_point, next_blinding_override
1494+
}, next_hop_hmac, new_packet_bytes
1495+
} = bob_peeled_onion {
1496+
assert_eq!(short_channel_id, 1729);
1497+
assert!(next_blinding_override.is_none());
1498+
assert_eq!(intro_node_blinding_point, Some(bob_blinding_point));
1499+
assert_eq!(payment_relay, PaymentRelay { cltv_expiry_delta: 36, fee_proportional_millionths: 150, fee_base_msat: 10_000 });
1500+
assert_eq!(features, BlindedHopFeatures::empty());
1501+
assert_eq!(payment_constraints, PaymentConstraints { max_cltv_expiry: 748_005, htlc_minimum_msat: 1500 });
1502+
(new_packet_bytes, next_hop_hmac)
1503+
} else { panic!() };
1504+
1505+
let carol_packet_details = next_packet_details_opt.unwrap();
1506+
let carol_onion = msgs::OnionPacket {
1507+
version: 0,
1508+
public_key: carol_packet_details.next_packet_pubkey,
1509+
hop_data: carol_packet_bytes,
1510+
hmac: carol_hmac,
1511+
};
1512+
let carol_update_add = update_add_msg(
1513+
carol_packet_details.outgoing_amt_msat, carol_packet_details.outgoing_cltv_value,
1514+
Some(pubkey_from_hex("034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0")),
1515+
carol_onion
1516+
);
1517+
let carol_node_signer = TestEcdhSigner { node_secret: carol_secret };
1518+
let (carol_peeled_onion, _, next_packet_details_opt) =
1519+
match onion_payment::decode_incoming_update_add_htlc_onion(
1520+
&carol_update_add, &&carol_node_signer, &&logger, &secp_ctx
1521+
) {
1522+
Ok(res) => res,
1523+
_ => panic!("Unexpected error")
1524+
};
1525+
let (dave_packet_bytes, dave_hmac) = if let onion_utils::Hop::Forward {
1526+
next_hop_data: msgs::InboundOnionPayload::BlindedForward {
1527+
short_channel_id, payment_relay, payment_constraints, features, intro_node_blinding_point, next_blinding_override
1528+
}, next_hop_hmac, new_packet_bytes
1529+
} = carol_peeled_onion {
1530+
assert_eq!(short_channel_id, 1105);
1531+
assert_eq!(next_blinding_override, Some(pubkey_from_hex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")));
1532+
assert!(intro_node_blinding_point.is_none());
1533+
assert_eq!(payment_relay, PaymentRelay { cltv_expiry_delta: 48, fee_proportional_millionths: 100, fee_base_msat: 500 });
1534+
assert_eq!(features, BlindedHopFeatures::empty());
1535+
assert_eq!(payment_constraints, PaymentConstraints { max_cltv_expiry: 747_969, htlc_minimum_msat: 1500 });
1536+
(new_packet_bytes, next_hop_hmac)
1537+
} else { panic!() };
1538+
1539+
let dave_packet_details = next_packet_details_opt.unwrap();
1540+
let dave_onion = msgs::OnionPacket {
1541+
version: 0,
1542+
public_key: dave_packet_details.next_packet_pubkey,
1543+
hop_data: dave_packet_bytes,
1544+
hmac: dave_hmac,
1545+
};
1546+
let dave_update_add = update_add_msg(
1547+
dave_packet_details.outgoing_amt_msat, dave_packet_details.outgoing_cltv_value,
1548+
Some(pubkey_from_hex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")),
1549+
dave_onion
1550+
);
1551+
let dave_node_signer = TestEcdhSigner { node_secret: dave_secret };
1552+
let (dave_peeled_onion, _, next_packet_details_opt) =
1553+
match onion_payment::decode_incoming_update_add_htlc_onion(
1554+
&dave_update_add, &&dave_node_signer, &&logger, &secp_ctx
1555+
) {
1556+
Ok(res) => res,
1557+
_ => panic!("Unexpected error")
1558+
};
1559+
let (eve_packet_bytes, eve_hmac) = if let onion_utils::Hop::Forward {
1560+
next_hop_data: msgs::InboundOnionPayload::BlindedForward {
1561+
short_channel_id, payment_relay, payment_constraints, features, intro_node_blinding_point, next_blinding_override
1562+
}, next_hop_hmac, new_packet_bytes
1563+
} = dave_peeled_onion {
1564+
assert_eq!(short_channel_id, 561);
1565+
assert!(next_blinding_override.is_none());
1566+
assert!(intro_node_blinding_point.is_none());
1567+
assert_eq!(payment_relay, PaymentRelay { cltv_expiry_delta: 144, fee_proportional_millionths: 250, fee_base_msat: 0 });
1568+
assert_eq!(features, BlindedHopFeatures::empty());
1569+
assert_eq!(payment_constraints, PaymentConstraints { max_cltv_expiry: 747_921, htlc_minimum_msat: 1500 });
1570+
(new_packet_bytes, next_hop_hmac)
1571+
} else { panic!() };
1572+
1573+
let eve_packet_details = next_packet_details_opt.unwrap();
1574+
let eve_onion = msgs::OnionPacket {
1575+
version: 0,
1576+
public_key: eve_packet_details.next_packet_pubkey,
1577+
hop_data: eve_packet_bytes,
1578+
hmac: eve_hmac,
1579+
};
1580+
let eve_update_add = update_add_msg(
1581+
eve_packet_details.outgoing_amt_msat, eve_packet_details.outgoing_cltv_value,
1582+
Some(pubkey_from_hex("03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a")),
1583+
eve_onion
1584+
);
1585+
let eve_node_signer = TestEcdhSigner { node_secret: eve_secret };
1586+
// We can't decode the final payload because it contains a path_id and is missing some LDK
1587+
// specific fields.
1588+
match onion_payment::decode_incoming_update_add_htlc_onion(
1589+
&eve_update_add, &&eve_node_signer, &&logger, &secp_ctx
1590+
) {
1591+
Err(HTLCFailureMsg::Malformed(msg)) => assert_eq!(msg.failure_code, INVALID_ONION_BLINDING),
1592+
_ => panic!("Unexpected error")
1593+
}
1594+
}

0 commit comments

Comments
 (0)