-
Notifications
You must be signed in to change notification settings - Fork 427
Introduce Dummy Hop support for Blinded Payment Path #4152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4ce34ba
fe33666
1e94a08
2aeabc5
0dd8c46
61a8d39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| use bitcoin::secp256k1::ecdh::SharedSecret; | ||
| use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; | ||
|
|
||
| use crate::blinded_path::message::MAX_DUMMY_HOPS_COUNT; | ||
| use crate::blinded_path::utils::{self, BlindedPathWithPadding}; | ||
| use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp}; | ||
| use crate::crypto::streams::ChaChaDualPolyReadAdapter; | ||
|
|
@@ -33,7 +34,6 @@ use crate::util::ser::{ | |
| Writeable, Writer, | ||
| }; | ||
|
|
||
| use core::mem; | ||
| use core::ops::Deref; | ||
|
|
||
| #[allow(unused_imports)] | ||
|
|
@@ -121,6 +121,32 @@ impl BlindedPaymentPath { | |
| 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, | ||
| { | ||
| BlindedPaymentPath::new_with_dummy_hops( | ||
| intermediate_nodes, | ||
| payee_node_id, | ||
| 0, | ||
| local_node_receive_key, | ||
| payee_tlvs, | ||
| htlc_maximum_msat, | ||
| min_final_cltv_expiry_delta, | ||
| entropy_source, | ||
| secp_ctx, | ||
| ) | ||
| } | ||
|
|
||
| /// Same as [`BlindedPaymentPath::new`], but allows specifying a number of dummy hops. | ||
| /// | ||
| /// Note: | ||
| /// At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path. | ||
| pub fn new_with_dummy_hops<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>( | ||
| intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, | ||
| dummy_hop_count: usize, 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, | ||
| { | ||
|
|
@@ -145,6 +171,7 @@ impl BlindedPaymentPath { | |
| secp_ctx, | ||
| intermediate_nodes, | ||
| payee_node_id, | ||
| dummy_hop_count, | ||
| payee_tlvs, | ||
| &blinding_secret, | ||
| local_node_receive_key, | ||
|
|
@@ -191,28 +218,31 @@ impl BlindedPaymentPath { | |
| NL::Target: NodeIdLookUp, | ||
| T: secp256k1::Signing + secp256k1::Verification, | ||
| { | ||
| match self.decrypt_intro_payload::<NS>(node_signer) { | ||
| Ok(( | ||
| BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }), | ||
| control_tlvs_ss, | ||
| )) => { | ||
| let next_node_id = match node_id_lookup.next_node_id(short_channel_id) { | ||
| Some(node_id) => node_id, | ||
| None => return Err(()), | ||
| }; | ||
| let mut new_blinding_point = onion_utils::next_hop_pubkey( | ||
| secp_ctx, | ||
| self.inner_path.blinding_point, | ||
| control_tlvs_ss.as_ref(), | ||
| ) | ||
| .map_err(|_| ())?; | ||
| mem::swap(&mut self.inner_path.blinding_point, &mut new_blinding_point); | ||
| self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id); | ||
| self.inner_path.blinded_hops.remove(0); | ||
| Ok(()) | ||
| }, | ||
| _ => Err(()), | ||
| } | ||
| let (next_node_id, control_tlvs_ss) = | ||
| match self.decrypt_intro_payload::<NS>(node_signer).map_err(|_| ())? { | ||
| (BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }), ss) => { | ||
| let node_id = node_id_lookup.next_node_id(short_channel_id).ok_or(())?; | ||
| (node_id, ss) | ||
| }, | ||
| (BlindedPaymentTlvs::Dummy, ss) => { | ||
| let node_id = node_signer.get_node_id(Recipient::Node)?; | ||
| (node_id, ss) | ||
| }, | ||
| _ => return Err(()), | ||
| }; | ||
|
|
||
| let new_blinding_point = onion_utils::next_hop_pubkey( | ||
| secp_ctx, | ||
| self.inner_path.blinding_point, | ||
| control_tlvs_ss.as_ref(), | ||
| ) | ||
| .map_err(|_| ())?; | ||
|
|
||
| self.inner_path.blinding_point = new_blinding_point; | ||
| self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id); | ||
| self.inner_path.blinded_hops.remove(0); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| pub(crate) fn decrypt_intro_payload<NS: Deref>( | ||
|
|
@@ -234,9 +264,9 @@ impl BlindedPaymentPath { | |
| .map_err(|_| ())?; | ||
|
|
||
| match (&readable, used_aad) { | ||
| (BlindedPaymentTlvs::Forward(_), false) | (BlindedPaymentTlvs::Receive(_), true) => { | ||
| Ok((readable, control_tlvs_ss)) | ||
| }, | ||
| (BlindedPaymentTlvs::Forward(_), false) | ||
| | (BlindedPaymentTlvs::Dummy, true) | ||
| | (BlindedPaymentTlvs::Receive(_), true) => Ok((readable, control_tlvs_ss)), | ||
| _ => Err(()), | ||
| } | ||
| } | ||
|
|
@@ -346,6 +376,8 @@ pub struct ReceiveTlvs { | |
| pub(crate) enum BlindedPaymentTlvs { | ||
| /// This blinded payment data is for a forwarding node. | ||
| Forward(ForwardTlvs), | ||
| /// This blinded payment data is dummy and is to be peeled by receiving node. | ||
| Dummy, | ||
| /// This blinded payment data is for the receiving node. | ||
| Receive(ReceiveTlvs), | ||
| } | ||
|
|
@@ -361,8 +393,10 @@ pub(crate) enum BlindedTrampolineTlvs { | |
| } | ||
|
|
||
| // Used to include forward and receive TLVs in the same iterator for encoding. | ||
| #[derive(Clone)] | ||
| enum BlindedPaymentTlvsRef<'a> { | ||
| Forward(&'a ForwardTlvs), | ||
| Dummy, | ||
| Receive(&'a ReceiveTlvs), | ||
| } | ||
|
|
||
|
|
@@ -532,6 +566,11 @@ impl<'a> Writeable for BlindedPaymentTlvsRef<'a> { | |
| fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { | ||
| match self { | ||
| Self::Forward(tlvs) => tlvs.write(w)?, | ||
| Self::Dummy => { | ||
| encode_tlv_stream!(w, { | ||
| (65539, (), required), | ||
| }) | ||
| }, | ||
| Self::Receive(tlvs) => tlvs.write(w)?, | ||
| } | ||
| Ok(()) | ||
|
|
@@ -548,32 +587,48 @@ impl Readable for BlindedPaymentTlvs { | |
| (2, scid, option), | ||
| (8, next_blinding_override, option), | ||
| (10, payment_relay, option), | ||
| (12, payment_constraints, required), | ||
| (12, payment_constraints, option), | ||
| (14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))), | ||
| (65536, payment_secret, option), | ||
| (65537, payment_context, option), | ||
| (65539, is_dummy, option) | ||
| }); | ||
|
|
||
| if let Some(short_channel_id) = scid { | ||
| if payment_secret.is_some() { | ||
| return Err(DecodeError::InvalidValue); | ||
| } | ||
| Ok(BlindedPaymentTlvs::Forward(ForwardTlvs { | ||
| match ( | ||
| scid, | ||
| next_blinding_override, | ||
| payment_relay, | ||
| payment_constraints, | ||
| features, | ||
| payment_secret, | ||
| payment_context, | ||
| is_dummy, | ||
| ) { | ||
| ( | ||
| Some(short_channel_id), | ||
| next_override, | ||
| Some(relay), | ||
| Some(constraints), | ||
| features, | ||
| None, | ||
| None, | ||
| None, | ||
| ) => Ok(BlindedPaymentTlvs::Forward(ForwardTlvs { | ||
| short_channel_id, | ||
| payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?, | ||
| payment_constraints: payment_constraints.0.unwrap(), | ||
| next_blinding_override, | ||
| payment_relay: relay, | ||
| payment_constraints: constraints, | ||
| next_blinding_override: next_override, | ||
| features: features.unwrap_or_else(BlindedHopFeatures::empty), | ||
| })) | ||
| } else { | ||
| if payment_relay.is_some() || features.is_some() { | ||
| return Err(DecodeError::InvalidValue); | ||
| } | ||
| Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs { | ||
| payment_secret: payment_secret.ok_or(DecodeError::InvalidValue)?, | ||
| payment_constraints: payment_constraints.0.unwrap(), | ||
| payment_context: payment_context.ok_or(DecodeError::InvalidValue)?, | ||
| })) | ||
| })), | ||
| (None, None, None, Some(constraints), None, Some(secret), Some(context), None) => { | ||
| Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs { | ||
| payment_secret: secret, | ||
| payment_constraints: constraints, | ||
| payment_context: context, | ||
| })) | ||
| }, | ||
| (None, None, None, None, None, None, None, Some(())) => Ok(BlindedPaymentTlvs::Dummy), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually feel like we should include a This requires enforcing the fees when decoding the onions as well, in addition to reporting it somehow in |
||
| _ => return Err(DecodeError::InvalidValue), | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -620,21 +675,47 @@ 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, local_node_receive_key: ReceiveAuthKey, | ||
| dummy_hop_count: usize, payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, | ||
| local_node_receive_key: ReceiveAuthKey, | ||
| ) -> Vec<BlindedHop> { | ||
| let dummy_count = core::cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT); | ||
| let pks = intermediate_nodes | ||
| .iter() | ||
| .map(|node| (node.node_id, None)) | ||
| .chain(core::iter::repeat((payee_node_id, Some(local_node_receive_key))).take(dummy_count)) | ||
| .chain(core::iter::once((payee_node_id, Some(local_node_receive_key)))); | ||
| let tlvs = intermediate_nodes | ||
| .iter() | ||
| .map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs)) | ||
| .chain(core::iter::repeat(BlindedPaymentTlvsRef::Dummy).take(dummy_count)) | ||
| .chain(core::iter::once(BlindedPaymentTlvsRef::Receive(&payee_tlvs))); | ||
|
|
||
| let path = pks.zip( | ||
| tlvs.map(|tlv| BlindedPathWithPadding { tlvs: tlv, round_off: PAYMENT_PADDING_ROUND_OFF }), | ||
| ); | ||
|
|
||
| // Debug invariant: all non-final hops must have identical serialized size. | ||
| #[cfg(debug_assertions)] | ||
| { | ||
| let mut iter = path.clone(); | ||
| if let Some((_, first)) = iter.next() { | ||
| let remaining = iter.clone().count(); // includes intermediate + final | ||
|
|
||
| // At least one intermediate hop | ||
| if remaining > 1 { | ||
| let expected = first.serialized_length(); | ||
|
|
||
| // skip final hop: take(remaining - 1) | ||
| for (_, hop) in iter.take(remaining - 1) { | ||
| debug_assert!( | ||
| hop.serialized_length() == expected, | ||
| "All intermediate blinded hops must have identical serialized size" | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| utils::construct_blinded_hops(secp_ctx, path, session_priv) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accordingly we'll need to allow specifying the fees/CLTV delta for each dummy hop here, though probably that should wait until a PR where we start enforcing them.