Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lightning-dns-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ mod test {
use lightning::onion_message::messenger::{
AOnionMessenger, Destination, MessageRouter, OnionMessagePath, OnionMessenger,
};
use lightning::routing::router::DEFAULT_PAYMENT_DUMMY_HOPS;
use lightning::sign::{KeysManager, NodeSigner, ReceiveAuthKey, Recipient};
use lightning::types::features::InitFeatures;
use lightning::types::payment::PaymentHash;
Expand Down Expand Up @@ -419,6 +420,11 @@ mod test {
let updates = get_htlc_update_msgs(&nodes[0], &payee_id);
nodes[1].node.handle_update_add_htlc(payer_id, &updates.update_add_htlcs[0]);
do_commitment_signed_dance(&nodes[1], &nodes[0], &updates.commitment_signed, false, false);

for _ in 0..DEFAULT_PAYMENT_DUMMY_HOPS {
nodes[1].node.process_pending_htlc_forwards();
}

expect_and_process_pending_htlcs(&nodes[1], false);

let claimable_events = nodes[1].node.get_and_clear_pending_events();
Expand Down
173 changes: 127 additions & 46 deletions lightning/src/blinded_path/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,7 +34,6 @@ use crate::util::ser::{
Writeable, Writer,
};

use core::mem;
use core::ops::Deref;

#[allow(unused_imports)]
Expand Down Expand Up @@ -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.
Copy link
Collaborator

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.

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,
{
Expand All @@ -145,6 +171,7 @@ impl BlindedPaymentPath {
secp_ctx,
intermediate_nodes,
payee_node_id,
dummy_hop_count,
payee_tlvs,
&blinding_secret,
local_node_receive_key,
Expand Down Expand Up @@ -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>(
Expand All @@ -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(()),
}
}
Expand Down Expand Up @@ -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),
}
Expand All @@ -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),
}

Expand Down Expand Up @@ -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(())
Expand All @@ -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),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually feel like we should include a payment_relay in dummy payment hops. The biggest issue with padded payment hops is that the sender can (likely) identify the path actually taken based on feerate and CLTV-delta information. The use of payment_relay allows us to request that nodes require/take additional fee/CLTV delta on top of their published feerates/CLTV deltas as a way to add additional privacy. But, if we're gonna ask the payer for additional fees and CLTV, why are we "giving it" to other nodes? We should just require it on the dummy hops (where we get it, though I think this also addresses some off-by-ones in the fee enforcement logic which may be a privacy leak as well).

This requires enforcing the fees when decoding the onions as well, in addition to reporting it somehow in PaymentClaimable/PaymentClaimed (maybe just as a part of the total amount, though might be nice to break it out). I'm certainly fine with pushing that enforcement to another PR given this one is getting somewhat long in the tooth, but we should at least support deserializing the payment_relay field imo.

_ => return Err(DecodeError::InvalidValue),
}
}
}
Expand Down Expand Up @@ -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)
}

Expand Down
9 changes: 8 additions & 1 deletion lightning/src/ln/async_payments_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use crate::onion_message::messenger::{
use crate::onion_message::offers::OffersMessage;
use crate::onion_message::packet::ParsedOnionMessageContents;
use crate::prelude::*;
use crate::routing::router::{Payee, PaymentParameters};
use crate::routing::router::{Payee, PaymentParameters, DEFAULT_PAYMENT_DUMMY_HOPS};
use crate::sign::NodeSigner;
use crate::sync::Mutex;
use crate::types::features::Bolt12InvoiceFeatures;
Expand Down Expand Up @@ -1858,6 +1858,13 @@ fn expired_static_invoice_payment_path() {
blinded_path
.advance_path_by_one(&nodes[1].keys_manager, &nodes[1].node, &secp_ctx)
.unwrap();

for _ in 0..DEFAULT_PAYMENT_DUMMY_HOPS {
blinded_path
.advance_path_by_one(&nodes[2].keys_manager, &nodes[2].node, &secp_ctx)
.unwrap();
}

match blinded_path.decrypt_intro_payload(&nodes[2].keys_manager).unwrap().0 {
BlindedPaymentTlvs::Receive(tlvs) => tlvs.payment_constraints.max_cltv_expiry,
_ => panic!(),
Expand Down
66 changes: 66 additions & 0 deletions lightning/src/ln/blinded_payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,72 @@ fn do_one_hop_blinded_path(success: bool) {
}
}

#[test]
fn one_hop_blinded_path_with_dummy_hops() {
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
let chan_upd =
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0).0.contents;

let amt_msat = 5000;
let (payment_preimage, payment_hash, payment_secret) =
get_payment_preimage_hash(&nodes[1], Some(amt_msat), None);
let payee_tlvs = ReceiveTlvs {
payment_secret,
payment_constraints: PaymentConstraints {
max_cltv_expiry: u32::max_value(),
htlc_minimum_msat: chan_upd.htlc_minimum_msat,
},
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
};
let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key();
let dummy_hops = 2;

let mut secp_ctx = Secp256k1::new();
let blinded_path = BlindedPaymentPath::new_with_dummy_hops(
&[],
nodes[1].node.get_our_node_id(),
dummy_hops,
receive_auth_key,
payee_tlvs,
u64::MAX,
TEST_FINAL_CLTV as u16,
&chanmon_cfgs[1].keys_manager,
&secp_ctx,
)
.unwrap();

let route_params = RouteParameters::from_payment_params_and_value(
PaymentParameters::blinded(vec![blinded_path]),
amt_msat,
);
nodes[0]
.node
.send_payment(
payment_hash,
RecipientOnionFields::spontaneous_empty(),
PaymentId(payment_hash.0),
route_params,
Retry::Attempts(0),
)
.unwrap();
check_added_monitors(&nodes[0], 1);

let mut events = nodes[0].node.get_and_clear_pending_msg_events();
assert_eq!(events.len(), 1);
let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events);

let path = &[&nodes[1]];
let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev)
.with_dummy_override(dummy_hops)
.with_payment_secret(payment_secret);

do_pass_along_path(args);
claim_payment(&nodes[0], &[&nodes[1]], payment_preimage);
}

#[test]
#[rustfmt::skip]
fn mpp_to_one_hop_blinded_path() {
Expand Down
Loading