Introduce ReceiveAuthKey-based verification in Blinded Payment Paths
What changed, and why it matters
This commit changes how hidden (blinded) Lightning payment paths are created and checked. It adds a new 'receive authentication key' used when encrypting the final hop's details, and it makes the receiving node verify that the final hop was encrypted with that key. This is a defensive hardening change that reduces the data sent in payment paths and makes the verification logic consistent with another similar feature (blinded message paths).
Treat as a hardening/correctness improvement rather than an active vulnerability fix. Review the ChaChaDualPolyReadAdapter implementation separately to confirm the AAD handling and key derivation are sound, and ensure all downstream Router implementations have been updated to supply the receive auth key.
Security signals we found
New cryptographic authentication key (ReceiveAuthKey) added to blinded payment path construction and verification
Final receive hop now requires the auth-key AAD; forward hops must not use it
ChaChaPolyReadAdapter replaced by ChaChaDualPolyReadAdapter with dual-key material (rho + receive_auth_key)
Decoding rejects forward payloads that incorrectly use the auth-key AAD and receive payloads that omit it
Router trait and all callers updated to propagate receive auth key from NodeSigner
Test vectors and mock signers updated to provide deterministic receive auth keys
Evidence from the diff
The patch introduces ReceiveAuthKey-based verification into BlindedPaymentPath construction and decoding. BlindedPaymentPath::new and ::one_hop now take a local_node_receive_key: ReceiveAuthKey. The final hop’s payload is encrypted using a dual-ChaCha adapter (ChaChaDualPolyReadAdapter) keyed by both the existing rho and the receive auth key. On decoding, the node uses ChaChaDualPolyReadAdapter and checks used_aad: Forward payloads must not use the auth-key AAD, while Receive payloads must use it. The Router trait and all implementations are updated to thread the receive auth key from the signer into path creation. This aligns blinded payment paths with the authentication model already used for blinded message paths.
Changed components
lightning/src/blinded_path/payment.rslightning/src/ln/msgs.rslightning/src/routing/router.rslightning/src/offers/flow.rsfuzz targets (chanmon_consistency, full_stack, invoice_request_deser, refund_deser)test utilities and blinded payment testsInspect captured patch +97 / −45
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 9f03de4..17a46ff 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -128,8 +128,9 @@ impl Router for FuzzRouter {
}
fn create_blinded_payment_paths<T: secp256k1::Signing + secp256k1::Verification>(
- &self, _recipient: PublicKey, _first_hops: Vec<ChannelDetails>, _tlvs: ReceiveTlvs,
- _amount_msats: Option<u64>, _secp_ctx: &Secp256k1<T>,
+ &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey,
+ _first_hops: Vec<ChannelDetails>, _tlvs: ReceiveTlvs, _amount_msats: Option<u64>,
+ _secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPaymentPath>, ()> {
unreachable!()
}
diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs
index 97a7487..6ddcd78 100644
--- a/fuzz/src/full_stack.rs
+++ b/fuzz/src/full_stack.rs
@@ -157,8 +157,9 @@ impl Router for FuzzRouter {
}
fn create_blinded_payment_paths<T: secp256k1::Signing + secp256k1::Verification>(
- &self, _recipient: PublicKey, _first_hops: Vec<ChannelDetails>, _tlvs: ReceiveTlvs,
- _amount_msats: Option<u64>, _secp_ctx: &Secp256k1<T>,
+ &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey,
+ _first_hops: Vec<ChannelDetails>, _tlvs: ReceiveTlvs, _amount_msats: Option<u64>,
+ _secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPaymentPath>, ()> {
unreachable!()
}
diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs
index 96d8515..93618d1 100644
--- a/fuzz/src/invoice_request_deser.rs
+++ b/fuzz/src/invoice_request_deser.rs
@@ -21,7 +21,7 @@ use lightning::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields};
use lightning::offers::nonce::Nonce;
use lightning::offers::offer::OfferId;
use lightning::offers::parse::Bolt12SemanticError;
-use lightning::sign::EntropySource;
+use lightning::sign::{EntropySource, ReceiveAuthKey};
use lightning::types::features::BlindedHopFeatures;
use lightning::types::payment::{PaymentHash, PaymentSecret};
use lightning::types::string::UntrustedString;
@@ -85,6 +85,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
let expanded_key = ExpandedKey::new([42; 32]);
let entropy_source = Randomness {};
let nonce = Nonce::from_entropy_source(&entropy_source);
+ let receive_auth_key = ReceiveAuthKey([41; 32]);
let invoice_request_fields =
if let Ok(ver) = invoice_request.clone().verify_using_metadata(&expanded_key, secp_ctx) {
@@ -136,6 +137,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
let payment_path = BlindedPaymentPath::new(
&intermediate_nodes,
pubkey(42),
+ receive_auth_key,
payee_tlvs,
u64::MAX,
MIN_FINAL_CLTV_EXPIRY_DELTA,
diff --git a/fuzz/src/refund_deser.rs b/fuzz/src/refund_deser.rs
index 6151d81..2dea67c 100644
--- a/fuzz/src/refund_deser.rs
+++ b/fuzz/src/refund_deser.rs
@@ -20,7 +20,7 @@ use lightning::offers::invoice::UnsignedBolt12Invoice;
use lightning::offers::nonce::Nonce;
use lightning::offers::parse::Bolt12SemanticError;
use lightning::offers::refund::Refund;
-use lightning::sign::EntropySource;
+use lightning::sign::{EntropySource, ReceiveAuthKey};
use lightning::types::features::BlindedHopFeatures;
use lightning::types::payment::{PaymentHash, PaymentSecret};
use lightning::util::ser::Writeable;
@@ -71,6 +71,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> {
let expanded_key = ExpandedKey::new([42; 32]);
let entropy_source = Randomness {};
+ let receive_auth_key = ReceiveAuthKey([41; 32]);
let nonce = Nonce::from_entropy_source(&entropy_source);
let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {});
let payee_tlvs = UnauthenticatedReceiveTlvs {
@@ -103,6 +104,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
let payment_path = BlindedPaymentPath::new(
&intermediate_nodes,
pubkey(42),
+ receive_auth_key,
payee_tlvs,
u64::MAX,
MIN_FINAL_CLTV_EXPIRY_DELTA,
diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs
index 37d7a1d..ae60aaa 100644
--- a/lightning/src/blinded_path/payment.rs
+++ b/lightning/src/blinded_path/payment.rs
@@ -16,7 +16,7 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
use crate::blinded_path::utils::{self, BlindedPathWithPadding};
use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp};
-use crate::crypto::streams::ChaChaPolyReadAdapter;
+use crate::crypto::streams::ChaChaDualPolyReadAdapter;
use crate::io;
use crate::io::Cursor;
use crate::ln::channel_state::CounterpartyForwardingInfo;
@@ -28,7 +28,7 @@ use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::nonce::Nonce;
use crate::offers::offer::OfferId;
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
-use crate::sign::{EntropySource, NodeSigner, Recipient};
+use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient};
use crate::types::features::BlindedHopFeatures;
use crate::types::payment::PaymentSecret;
use crate::types::routing::RoutingFees;
@@ -93,8 +93,8 @@ pub struct BlindedPaymentPath {
impl BlindedPaymentPath {
/// Create a one-hop blinded path for a payment.
pub fn one_hop<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
- payee_node_id: PublicKey, payee_tlvs: ReceiveTlvs, min_final_cltv_expiry_delta: u16,
- entropy_source: ES, secp_ctx: &Secp256k1<T>,
+ payee_node_id: PublicKey, local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs,
+ min_final_cltv_expiry_delta: u16, entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()>
where
ES::Target: EntropySource,
@@ -105,6 +105,7 @@ impl BlindedPaymentPath {
Self::new(
&[],
payee_node_id,
+ local_node_receive_key,
payee_tlvs,
htlc_maximum_msat,
min_final_cltv_expiry_delta,
@@ -121,8 +122,8 @@ impl BlindedPaymentPath {
// TODO: make all payloads the same size with padding + add dummy hops
pub fn new<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
- payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
- entropy_source: ES, secp_ctx: &Secp256k1<T>,
+ local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64,
+ min_final_cltv_expiry_delta: u16, entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()>
where
ES::Target: EntropySource,
@@ -150,6 +151,7 @@ impl BlindedPaymentPath {
payee_node_id,
payee_tlvs,
&blinding_secret,
+ local_node_receive_key,
),
},
payinfo: blinded_payinfo,
@@ -226,12 +228,19 @@ impl BlindedPaymentPath {
let control_tlvs_ss =
node_signer.ecdh(Recipient::Node, &self.inner_path.blinding_point, None)?;
let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes());
+ let receive_auth_key = node_signer.get_receive_auth_key();
let encrypted_control_tlvs =
&self.inner_path.blinded_hops.get(0).ok_or(())?.encrypted_payload;
let mut s = Cursor::new(encrypted_control_tlvs);
let mut reader = FixedLengthReader::new(&mut s, encrypted_control_tlvs.len() as u64);
- match ChaChaPolyReadAdapter::read(&mut reader, rho) {
- Ok(ChaChaPolyReadAdapter { readable, .. }) => Ok((readable, control_tlvs_ss)),
+ let ChaChaDualPolyReadAdapter { readable, used_aad } =
+ ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0))
+ .map_err(|_| ())?;
+
+ match (&readable, used_aad) {
+ (BlindedPaymentTlvs::Forward(_), false) | (BlindedPaymentTlvs::Receive(_), true) => {
+ Ok((readable, control_tlvs_ss))
+ },
_ => Err(()),
}
}
@@ -660,12 +669,12 @@ pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30;
/// Construct blinded payment hops for the given `intermediate_nodes` and payee info.
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
- payee_tlvs: ReceiveTlvs, session_priv: &SecretKey,
+ payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey,
) -> Vec<BlindedHop> {
let pks = intermediate_nodes
.iter()
.map(|node| (node.node_id, None))
- .chain(core::iter::once((payee_node_id, None)));
+ .chain(core::iter::once((payee_node_id, Some(local_node_receive_key))));
let tlvs = intermediate_nodes
.iter()
.map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs))
diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs
index 8959e34..85be279 100644
--- a/lightning/src/ln/blinded_payment_tests.rs
+++ b/lightning/src/ln/blinded_payment_tests.rs
@@ -86,12 +86,13 @@ pub fn blinded_payment_path(
let nonce = Nonce([42u8; 16]);
let expanded_key = keys_manager.get_expanded_key();
+ let receive_auth_key = keys_manager.get_receive_auth_key();
let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key);
let mut secp_ctx = Secp256k1::new();
BlindedPaymentPath::new(
- &intermediate_nodes[..], *node_ids.last().unwrap(), payee_tlvs,
- intro_node_max_htlc_opt.unwrap_or_else(|| channel_upds.last().unwrap().htlc_maximum_msat),
+ &intermediate_nodes[..], *node_ids.last().unwrap(), receive_auth_key,
+ payee_tlvs, intro_node_max_htlc_opt.unwrap_or_else(|| channel_upds.last().unwrap().htlc_maximum_msat),
TEST_FINAL_CLTV as u16, keys_manager, &secp_ctx
).unwrap()
}
@@ -171,11 +172,13 @@ fn do_one_hop_blinded_path(success: bool) {
};
let nonce = Nonce([42u8; 16]);
let expanded_key = chanmon_cfgs[1].keys_manager.get_expanded_key();
+ let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key();
let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key);
let mut secp_ctx = Secp256k1::new();
let blinded_path = BlindedPaymentPath::new(
- &[], nodes[1].node.get_our_node_id(), payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
+ &[], nodes[1].node.get_our_node_id(), receive_auth_key,
+ payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
&chanmon_cfgs[1].keys_manager, &secp_ctx
).unwrap();
@@ -225,9 +228,11 @@ fn mpp_to_one_hop_blinded_path() {
};
let nonce = Nonce([42u8; 16]);
let expanded_key = chanmon_cfgs[3].keys_manager.get_expanded_key();
+ let receive_auth_key = chanmon_cfgs[3].keys_manager.get_receive_auth_key();
let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key);
let blinded_path = BlindedPaymentPath::new(
- &[], nodes[3].node.get_our_node_id(), payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
+ &[], nodes[3].node.get_our_node_id(), receive_auth_key,
+ payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
&chanmon_cfgs[3].keys_manager, &secp_ctx
).unwrap();
@@ -1335,10 +1340,12 @@ fn custom_tlvs_to_blinded_path() {
};
let nonce = Nonce([42u8; 16]);
let expanded_key = chanmon_cfgs[1].keys_manager.get_expanded_key();
+ let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key();
let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key);
let mut secp_ctx = Secp256k1::new();
let blinded_path = BlindedPaymentPath::new(
- &[], nodes[1].node.get_our_node_id(), payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
+ &[], nodes[1].node.get_our_node_id(), receive_auth_key,
+ payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
&chanmon_cfgs[1].keys_manager, &secp_ctx
).unwrap();
@@ -1389,11 +1396,13 @@ fn fails_receive_tlvs_authentication() {
};
let nonce = Nonce([42u8; 16]);
let expanded_key = chanmon_cfgs[1].keys_manager.get_expanded_key();
+ let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key();
let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key);
let mut secp_ctx = Secp256k1::new();
let blinded_path = BlindedPaymentPath::new(
- &[], nodes[1].node.get_our_node_id(), payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
+ &[], nodes[1].node.get_our_node_id(), receive_auth_key,
+ payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
&chanmon_cfgs[1].keys_manager, &secp_ctx
).unwrap();
@@ -1424,7 +1433,8 @@ fn fails_receive_tlvs_authentication() {
let mut secp_ctx = Secp256k1::new();
let blinded_path = BlindedPaymentPath::new(
- &[], nodes[1].node.get_our_node_id(), payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
+ &[], nodes[1].node.get_our_node_id(), receive_auth_key,
+ payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16,
&chanmon_cfgs[1].keys_manager, &secp_ctx
).unwrap();
@@ -1627,7 +1637,7 @@ fn route_blinding_spec_test_vector() {
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient,
) -> Result<RecoverableSignature, ()> { unreachable!() }
fn get_peer_storage_key(&self) -> PeerStorageKey { unreachable!() }
- fn get_receive_auth_key(&self) -> ReceiveAuthKey { unreachable!() }
+ fn get_receive_auth_key(&self) -> ReceiveAuthKey { ReceiveAuthKey([41; 32]) }
fn sign_bolt12_invoice(
&self, _invoice: &UnsignedBolt12Invoice,
) -> Result<schnorr::Signature, ()> { unreachable!() }
@@ -1940,7 +1950,7 @@ fn test_trampoline_inbound_payment_decoding() {
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient,
) -> Result<RecoverableSignature, ()> { unreachable!() }
fn get_peer_storage_key(&self) -> PeerStorageKey { unreachable!() }
- fn get_receive_auth_key(&self) -> ReceiveAuthKey { unreachable!() }
+ fn get_receive_auth_key(&self) -> ReceiveAuthKey { ReceiveAuthKey([41; 32]) }
fn sign_bolt12_invoice(
&self, _invoice: &UnsignedBolt12Invoice,
) -> Result<schnorr::Signature, ()> { unreachable!() }
@@ -2207,8 +2217,9 @@ fn do_test_trampoline_single_hop_receive(success: bool) {
};
let nonce = Nonce([42u8; 16]);
let expanded_key = nodes[2].keys_manager.get_expanded_key();
+ let receive_auth_key = nodes[2].keys_manager.get_receive_auth_key();
let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key);
- let blinded_path = BlindedPaymentPath::new(&[], carol_node_id, payee_tlvs, u64::MAX, 0, nodes[2].keys_manager, &secp_ctx).unwrap();
+ let blinded_path = BlindedPaymentPath::new(&[], carol_node_id, receive_auth_key, payee_tlvs, u64::MAX, 0, nodes[2].keys_manager, &secp_ctx).unwrap();
let route = Route {
paths: vec![Path {
diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs
index 8425e19..366f54b 100644
--- a/lightning/src/ln/max_payment_path_len_tests.rs
+++ b/lightning/src/ln/max_payment_path_len_tests.rs
@@ -223,11 +223,13 @@ fn one_hop_blinded_path_with_custom_tlv() {
};
let nonce = Nonce([42u8; 16]);
let expanded_key = chanmon_cfgs[2].keys_manager.get_expanded_key();
+ let receive_auth_key = chanmon_cfgs[2].keys_manager.get_receive_auth_key();
let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key);
let mut secp_ctx = Secp256k1::new();
let blinded_path = BlindedPaymentPath::new(
&[],
nodes[2].node.get_our_node_id(),
+ receive_auth_key,
payee_tlvs,
u64::MAX,
TEST_FINAL_CLTV as u16,
diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs
index c0c8239..4462f77 100644
--- a/lightning/src/ln/msgs.rs
+++ b/lightning/src/ln/msgs.rs
@@ -59,7 +59,7 @@ use core::str::FromStr;
#[cfg(feature = "std")]
use std::net::SocketAddr;
-use crate::crypto::streams::ChaChaPolyReadAdapter;
+use crate::crypto::streams::ChaChaDualPolyReadAdapter;
use crate::util::base32;
use crate::util::logger;
use crate::util::ser::{
@@ -3655,10 +3655,11 @@ where
.ecdh(Recipient::Node, &blinding_point, None)
.map_err(|_| DecodeError::InvalidValue)?;
let rho = onion_utils::gen_rho_from_shared_secret(&enc_tlvs_ss.secret_bytes());
+ let receive_auth_key = node_signer.get_receive_auth_key();
let mut s = Cursor::new(&enc_tlvs);
let mut reader = FixedLengthReader::new(&mut s, enc_tlvs.len() as u64);
- match ChaChaPolyReadAdapter::read(&mut reader, rho)? {
- ChaChaPolyReadAdapter {
+ match ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0))? {
+ ChaChaDualPolyReadAdapter {
readable:
BlindedPaymentTlvs::Forward(ForwardTlvs {
short_channel_id,
@@ -3667,11 +3668,13 @@ where
features,
next_blinding_override,
}),
+ used_aad,
} => {
if amt.is_some()
|| cltv_value.is_some() || total_msat.is_some()
|| keysend_preimage.is_some()
|| invoice_request.is_some()
+ || used_aad
{
return Err(DecodeError::InvalidValue);
}
@@ -3684,7 +3687,14 @@ where
next_blinding_override,
}))
},
- ChaChaPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(receive_tlvs) } => {
+ ChaChaDualPolyReadAdapter {
+ readable: BlindedPaymentTlvs::Receive(receive_tlvs),
+ used_aad,
+ } => {
+ if !used_aad {
+ return Err(DecodeError::InvalidValue);
+ }
+
let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs;
let expanded_key = node_signer.get_expanded_key();
if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() {
@@ -3754,6 +3764,7 @@ where
{
fn read<R: Read>(r: &mut R, args: (Option<PublicKey>, NS)) -> Result<Self, DecodeError> {
let (update_add_blinding_point, node_signer) = args;
+ let receive_auth_key = node_signer.get_receive_auth_key();
let mut amt = None;
let mut cltv_value = None;
@@ -3807,8 +3818,8 @@ where
let rho = onion_utils::gen_rho_from_shared_secret(&enc_tlvs_ss.secret_bytes());
let mut s = Cursor::new(&enc_tlvs);
let mut reader = FixedLengthReader::new(&mut s, enc_tlvs.len() as u64);
- match ChaChaPolyReadAdapter::read(&mut reader, rho)? {
- ChaChaPolyReadAdapter {
+ match ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0))? {
+ ChaChaDualPolyReadAdapter {
readable:
BlindedTrampolineTlvs::Forward(TrampolineForwardTlvs {
next_trampoline,
@@ -3817,11 +3828,13 @@ where
features,
next_blinding_override,
}),
+ used_aad,
} => {
if amt.is_some()
|| cltv_value.is_some() || total_msat.is_some()
|| keysend_preimage.is_some()
|| invoice_request.is_some()
+ || used_aad
{
return Err(DecodeError::InvalidValue);
}
@@ -3834,9 +3847,14 @@ where
next_blinding_override,
}))
},
- ChaChaPolyReadAdapter {
+ ChaChaDualPolyReadAdapter {
readable: BlindedTrampolineTlvs::Receive(receive_tlvs),
+ used_aad,
} => {
+ if !used_aad {
+ return Err(DecodeError::InvalidValue);
+ }
+
let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs;
let expanded_key = node_signer.get_expanded_key();
if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() {
diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs
index 88f0cc5..6415d4b 100644
--- a/lightning/src/offers/flow.rs
+++ b/lightning/src/offers/flow.rs
@@ -329,6 +329,7 @@ where
let expanded_key = &self.inbound_payment_key;
let entropy = &*entropy_source;
let secp_ctx = &self.secp_ctx;
+ let receive_auth_key = self.receive_auth_key;
let payee_node_id = self.get_our_node_id();
@@ -349,6 +350,7 @@ where
router.create_blinded_payment_paths(
payee_node_id,
+ receive_auth_key,
usable_channels,
payee_tlvs,
amount_msats,
diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs
index 8434b17..c032868 100644
--- a/lightning/src/routing/router.rs
+++ b/lightning/src/routing/router.rs
@@ -28,7 +28,7 @@ use crate::routing::gossip::{
DirectedChannelInfo, EffectiveCapacity, NetworkGraph, NodeId, ReadOnlyNetworkGraph,
};
use crate::routing::scoring::{ChannelUsage, LockableScore, ScoreLookUp};
-use crate::sign::EntropySource;
+use crate::sign::{EntropySource, ReceiveAuthKey};
use crate::sync::Mutex;
use crate::types::features::{
BlindedHopFeatures, Bolt11InvoiceFeatures, Bolt12InvoiceFeatures, ChannelFeatures, NodeFeatures,
@@ -129,8 +129,8 @@ where
fn create_blinded_payment_paths<
T: secp256k1::Signing + secp256k1::Verification
> (
- &self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
- amount_msats: Option<u64>, secp_ctx: &Secp256k1<T>
+ &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, first_hops: Vec<ChannelDetails>,
+ tlvs: ReceiveTlvs, amount_msats: Option<u64>, secp_ctx: &Secp256k1<T>
) -> Result<Vec<BlindedPaymentPath>, ()> {
// Limit the number of blinded paths that are computed.
const MAX_PAYMENT_PATHS: usize = 3;
@@ -197,7 +197,7 @@ where
})
.map(|forward_node| {
BlindedPaymentPath::new(
- &[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
+ &[forward_node], recipient, local_node_receive_key, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
&*self.entropy_source, secp_ctx
)
})
@@ -209,7 +209,7 @@ where
_ => {
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
BlindedPaymentPath::new(
- &[], recipient, tlvs, u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source,
+ &[], recipient, local_node_receive_key, tlvs, u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source,
secp_ctx
).map(|path| vec![path])
} else {
@@ -243,8 +243,9 @@ impl Router for FixedRouter {
}
fn create_blinded_payment_paths<T: secp256k1::Signing + secp256k1::Verification>(
- &self, _recipient: PublicKey, _first_hops: Vec<ChannelDetails>, _tlvs: ReceiveTlvs,
- _amount_msats: Option<u64>, _secp_ctx: &Secp256k1<T>,
+ &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey,
+ _first_hops: Vec<ChannelDetails>, _tlvs: ReceiveTlvs, _amount_msats: Option<u64>,
+ _secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPaymentPath>, ()> {
// Should be unreachable as this router is only intended to provide a one-time payment route.
debug_assert!(false);
@@ -281,10 +282,11 @@ pub trait Router {
/// Creates [`BlindedPaymentPath`]s for payment to the `recipient` node. The channels in `first_hops`
/// are assumed to be with the `recipient`'s peers. The payment secret and any constraints are
- /// given in `tlvs`.
+ /// given in `tlvs`. The `local_node_receive_key` is required to authenticate the blinded payment paths.
fn create_blinded_payment_paths<T: secp256k1::Signing + secp256k1::Verification>(
- &self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
- amount_msats: Option<u64>, secp_ctx: &Secp256k1<T>,
+ &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey,
+ first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs, amount_msats: Option<u64>,
+ secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPaymentPath>, ()>;
}
diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs
index ad8ea22..c0023a2 100644
--- a/lightning/src/util/test_utils.rs
+++ b/lightning/src/util/test_utils.rs
@@ -289,13 +289,15 @@ impl<'a> Router for TestRouter<'a> {
}
fn create_blinded_payment_paths<T: secp256k1::Signing + secp256k1::Verification>(
- &self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
- amount_msats: Option<u64>, secp_ctx: &Secp256k1<T>,
+ &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey,
+ first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs, amount_msats: Option<u64>,
+ secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPaymentPath>, ()> {
let mut expected_paths = self.next_blinded_payment_paths.lock().unwrap();
if expected_paths.is_empty() {
self.router.create_blinded_payment_paths(
recipient,
+ local_node_receive_key,
first_hops,
tlvs,
amount_msats,
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.