Skip to content
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

Compact blinded path handling #2961

Merged
merged 10 commits into from
Apr 15, 2024
7 changes: 4 additions & 3 deletions fuzz/src/onion_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use bitcoin::secp256k1::ecdh::SharedSecret;
use bitcoin::secp256k1::ecdsa::RecoverableSignature;
use bitcoin::secp256k1::schnorr;

use lightning::blinded_path::BlindedPath;
use lightning::blinded_path::{BlindedPath, EmptyNodeIdLookUp};
use lightning::ln::features::InitFeatures;
use lightning::ln::msgs::{self, DecodeError, OnionMessageHandler};
use lightning::ln::script::ShutdownScript;
Expand Down Expand Up @@ -36,12 +36,13 @@ pub fn do_test<L: Logger>(data: &[u8], logger: &L) {
node_secret: secret,
counter: AtomicU64::new(0),
};
let node_id_lookup = EmptyNodeIdLookUp {};
let message_router = TestMessageRouter {};
let offers_msg_handler = TestOffersMessageHandler {};
let custom_msg_handler = TestCustomMessageHandler {};
let onion_messenger = OnionMessenger::new(
&keys_manager, &keys_manager, logger, &message_router, &offers_msg_handler,
&custom_msg_handler
&keys_manager, &keys_manager, logger, &node_id_lookup, &message_router,
&offers_msg_handler, &custom_msg_handler
);

let peer_node_id = {
Expand Down
4 changes: 2 additions & 2 deletions fuzz/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use bitcoin::blockdata::constants::ChainHash;
use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::transaction::TxOut;

use lightning::blinded_path::{BlindedHop, BlindedPath};
use lightning::blinded_path::{BlindedHop, BlindedPath, IntroductionNode};
use lightning::chain::transaction::OutPoint;
use lightning::ln::ChannelId;
use lightning::ln::channelmanager::{self, ChannelDetails, ChannelCounterparty};
Expand Down Expand Up @@ -363,7 +363,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
});
}
(payinfo, BlindedPath {
introduction_node_id: hop.src_node_id,
introduction_node: IntroductionNode::NodeId(hop.src_node_id),
blinding_point: dummy_pk,
blinded_hops,
})
Expand Down
53 changes: 39 additions & 14 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
#[allow(unused_imports)]
use crate::prelude::*;

use crate::blinded_path::{BlindedHop, BlindedPath};
use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp};
use crate::blinded_path::utils;
use crate::io;
use crate::io::Cursor;
Expand All @@ -19,8 +19,8 @@ use core::ops::Deref;
/// TLVs to encode in an intermediate onion message packet's hop data. When provided in a blinded
/// route, they are encoded into [`BlindedHop::encrypted_payload`].
pub(crate) struct ForwardTlvs {
/// The node id of the next hop in the onion message's path.
pub(crate) next_node_id: PublicKey,
/// The next hop in the onion message's path.
pub(crate) next_hop: NextHop,
/// Senders to a blinded path use this value to concatenate the route they find to the
/// introduction node with the blinded path.
pub(crate) next_blinding_override: Option<PublicKey>,
Expand All @@ -34,11 +34,25 @@ pub(crate) struct ReceiveTlvs {
pub(crate) path_id: Option<[u8; 32]>,
}

/// The next hop to forward the onion message along its path.
#[derive(Debug)]
pub enum NextHop {
/// The node id of the next hop.
NodeId(PublicKey),
/// The short channel id leading to the next hop.
ShortChannelId(u64),
}

impl Writeable for ForwardTlvs {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
let (next_node_id, short_channel_id) = match self.next_hop {
NextHop::NodeId(pubkey) => (Some(pubkey), None),
NextHop::ShortChannelId(scid) => (None, Some(scid)),
};
// TODO: write padding
encode_tlv_stream!(writer, {
(4, self.next_node_id, required),
(2, short_channel_id, option),
(4, next_node_id, option),
(8, self.next_blinding_override, option)
});
Ok(())
Expand All @@ -61,28 +75,39 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
) -> Result<Vec<BlindedHop>, secp256k1::Error> {
let blinded_tlvs = unblinded_path.iter()
.skip(1) // The first node's TLVs contains the next node's pubkey
.map(|pk| {
ControlTlvs::Forward(ForwardTlvs { next_node_id: *pk, next_blinding_override: None })
})
.map(|pk| ForwardTlvs { next_hop: NextHop::NodeId(*pk), next_blinding_override: None })
.map(|tlvs| ControlTlvs::Forward(tlvs))
.chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { path_id: None })));

utils::construct_blinded_hops(secp_ctx, unblinded_path.iter(), blinded_tlvs, session_priv)
}

// Advance the blinded onion message path by one hop, so make the second hop into the new
// introduction node.
pub(crate) fn advance_path_by_one<NS: Deref, T: secp256k1::Signing + secp256k1::Verification>(
path: &mut BlindedPath, node_signer: &NS, secp_ctx: &Secp256k1<T>
) -> Result<(), ()> where NS::Target: NodeSigner {
pub(crate) fn advance_path_by_one<NS: Deref, NL: Deref, T>(
path: &mut BlindedPath, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1<T>
) -> Result<(), ()>
where
NS::Target: NodeSigner,
NL::Target: NodeIdLookUp,
T: secp256k1::Signing + secp256k1::Verification,
{
let control_tlvs_ss = node_signer.ecdh(Recipient::Node, &path.blinding_point, None)?;
let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes());
let encrypted_control_tlvs = path.blinded_hops.remove(0).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: ControlTlvs::Forward(ForwardTlvs {
mut next_node_id, next_blinding_override,
})}) => {
Ok(ChaChaPolyReadAdapter {
readable: ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override })
}) => {
let next_node_id = match next_hop {
NextHop::NodeId(pubkey) => pubkey,
NextHop::ShortChannelId(scid) => match node_id_lookup.next_node_id(scid) {
Some(pubkey) => pubkey,
None => return Err(()),
},
};
let mut new_blinding_point = match next_blinding_override {
Some(blinding_point) => blinding_point,
None => {
Expand All @@ -91,7 +116,7 @@ pub(crate) fn advance_path_by_one<NS: Deref, T: secp256k1::Signing + secp256k1::
}
};
mem::swap(&mut path.blinding_point, &mut new_blinding_point);
mem::swap(&mut path.introduction_node_id, &mut next_node_id);
path.introduction_node = IntroductionNode::NodeId(next_node_id);
Ok(())
},
_ => Err(())
Expand Down
129 changes: 121 additions & 8 deletions lightning/src/blinded_path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};

use crate::ln::msgs::DecodeError;
use crate::offers::invoice::BlindedPayInfo;
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
use crate::sign::EntropySource;
use crate::util::ser::{Readable, Writeable, Writer};

Expand All @@ -28,11 +29,11 @@ use crate::prelude::*;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct BlindedPath {
/// To send to a blinded path, the sender first finds a route to the unblinded
/// `introduction_node_id`, which can unblind its [`encrypted_payload`] to find out the onion
/// `introduction_node`, which can unblind its [`encrypted_payload`] to find out the onion
/// message or payment's next hop and forward it along.
///
/// [`encrypted_payload`]: BlindedHop::encrypted_payload
pub introduction_node_id: PublicKey,
pub introduction_node: IntroductionNode,
/// Used by the introduction node to decrypt its [`encrypted_payload`] to forward the onion
/// message or payment.
///
Expand All @@ -42,6 +43,52 @@ pub struct BlindedPath {
pub blinded_hops: Vec<BlindedHop>,
}

/// The unblinded node in a [`BlindedPath`].
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum IntroductionNode {
/// The node id of the introduction node.
NodeId(PublicKey),
/// The short channel id of the channel leading to the introduction node. The [`Direction`]
/// identifies which side of the channel is the introduction node.
DirectedShortChannelId(Direction, u64),
}

/// The side of a channel that is the [`IntroductionNode`] in a [`BlindedPath`]. [BOLT 7] defines
/// which nodes is which in the [`ChannelAnnouncement`] message.
///
/// [BOLT 7]: https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_announcement-message
/// [`ChannelAnnouncement`]: crate::ln::msgs::ChannelAnnouncement
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum Direction {
/// The lesser node id when compared lexicographically in ascending order.
NodeOne,
/// The greater node id when compared lexicographically in ascending order.
NodeTwo,
}

/// An interface for looking up the node id of a channel counterparty for the purpose of forwarding
/// an [`OnionMessage`].
///
/// [`OnionMessage`]: crate::ln::msgs::OnionMessage
pub trait NodeIdLookUp {
/// Returns the node id of the forwarding node's channel counterparty with `short_channel_id`.
///
/// Here, the forwarding node is referring to the node of the [`OnionMessenger`] parameterized
/// by the [`NodeIdLookUp`] and the counterparty to one of that node's peers.
///
/// [`OnionMessenger`]: crate::onion_message::messenger::OnionMessenger
fn next_node_id(&self, short_channel_id: u64) -> Option<PublicKey>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Doesn't this need a Direction? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could provide one, but this is only for forwarding so we know the origin (us!). Maybe it would be useful for some remote lookup though? Or if we implement this for ReadOnlyNetworkGraph (see other comment).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, I totally missed the context here. Maybe lets include an "our channel's counterparty" in the docs just so people as dense as me can't miss it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added "forwarding node's " as "our" can be ambiguous, IMO.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I still kinda find the trait and method docs not 100% clear that this is specific to us. Maybe add a second sentence to one of them that says like "ie usually our peers"?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with Matt here
It feels like it isn't immediately clear that the forwarding node in this context means "our" node.
Maybe a small cue in the docs might help that indicates such some way.

}

/// A [`NodeIdLookUp`] that always returns `None`.
pub struct EmptyNodeIdLookUp {}

impl NodeIdLookUp for EmptyNodeIdLookUp {
fn next_node_id(&self, _short_channel_id: u64) -> Option<PublicKey> {
None
}
}

/// An encrypted payload and node id corresponding to a hop in a payment or onion message path, to
/// be encoded in the sender's onion packet. These hops cannot be identified by outside observers
/// and thus can be used to hide the identity of the recipient.
Expand Down Expand Up @@ -74,10 +121,10 @@ impl BlindedPath {
if node_pks.is_empty() { return Err(()) }
let blinding_secret_bytes = entropy_source.get_secure_random_bytes();
let blinding_secret = SecretKey::from_slice(&blinding_secret_bytes[..]).expect("RNG is busted");
let introduction_node_id = node_pks[0];
let introduction_node = IntroductionNode::NodeId(node_pks[0]);

Ok(BlindedPath {
introduction_node_id,
introduction_node,
blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret),
blinded_hops: message::blinded_hops(secp_ctx, node_pks, &blinding_secret).map_err(|_| ())?,
})
Expand Down Expand Up @@ -111,25 +158,59 @@ impl BlindedPath {
payee_tlvs: payment::ReceiveTlvs, htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
entropy_source: &ES, secp_ctx: &Secp256k1<T>
) -> Result<(BlindedPayInfo, Self), ()> {
let introduction_node = IntroductionNode::NodeId(
intermediate_nodes.first().map_or(payee_node_id, |n| n.node_id)
);
let blinding_secret_bytes = entropy_source.get_secure_random_bytes();
let blinding_secret = SecretKey::from_slice(&blinding_secret_bytes[..]).expect("RNG is busted");

let blinded_payinfo = payment::compute_payinfo(
intermediate_nodes, &payee_tlvs, htlc_maximum_msat, min_final_cltv_expiry_delta
)?;
Ok((blinded_payinfo, BlindedPath {
introduction_node_id: intermediate_nodes.first().map_or(payee_node_id, |n| n.node_id),
introduction_node,
blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret),
blinded_hops: payment::blinded_hops(
secp_ctx, intermediate_nodes, payee_node_id, payee_tlvs, &blinding_secret
).map_err(|_| ())?,
}))
}

/// Returns the introduction [`NodeId`] of the blinded path, if it is publicly reachable (i.e.,
/// it is found in the network graph).
pub fn public_introduction_node_id<'a>(
&self, network_graph: &'a ReadOnlyNetworkGraph
) -> Option<&'a NodeId> {
match &self.introduction_node {
IntroductionNode::NodeId(pubkey) => {
let node_id = NodeId::from_pubkey(pubkey);
network_graph.nodes().get_key_value(&node_id).map(|(key, _)| key)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Its super confusing that introduction_node_id could fail spuriously just because the introduction node_id isn't in the graph. Can we rename it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you care if I make it a method on ReadOnlyNetworkGraph instead? I'd like to keep the name short unless you have a better alternative?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not really sure that that solves it. Can we just call it publicly_reachable_introduction_node_id? Or even just public_introduction_node_id.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

Choose a reason for hiding this comment

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

The name sounds good, but honestly, I got a bit confused when I first read its name as it wasn't immediately clear to me what it does.
I think we should update the function docs, to clearly explain that public in this context means "publicly reachable" node.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

},
IntroductionNode::DirectedShortChannelId(direction, scid) => {
network_graph
.channel(*scid)
.map(|c| match direction {
Direction::NodeOne => &c.node_one,
Direction::NodeTwo => &c.node_two,
})
},
}
}
}

impl Writeable for BlindedPath {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
self.introduction_node_id.write(w)?;
match &self.introduction_node {
IntroductionNode::NodeId(pubkey) => pubkey.write(w)?,
IntroductionNode::DirectedShortChannelId(direction, scid) => {
match direction {
Direction::NodeOne => 0u8.write(w)?,
Direction::NodeTwo => 1u8.write(w)?,
}
scid.write(w)?;
},
}

self.blinding_point.write(w)?;
(self.blinded_hops.len() as u8).write(w)?;
for hop in &self.blinded_hops {
Expand All @@ -141,7 +222,17 @@ impl Writeable for BlindedPath {

impl Readable for BlindedPath {
fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> {
let introduction_node_id = Readable::read(r)?;
let mut first_byte: u8 = Readable::read(r)?;
let introduction_node = match first_byte {
0 => IntroductionNode::DirectedShortChannelId(Direction::NodeOne, Readable::read(r)?),
1 => IntroductionNode::DirectedShortChannelId(Direction::NodeTwo, Readable::read(r)?),
2|3 => {
use io::Read;
let mut pubkey_read = core::slice::from_mut(&mut first_byte).chain(r.by_ref());
IntroductionNode::NodeId(Readable::read(&mut pubkey_read)?)
},
_ => return Err(DecodeError::InvalidValue),
};
let blinding_point = Readable::read(r)?;
let num_hops: u8 = Readable::read(r)?;
if num_hops == 0 { return Err(DecodeError::InvalidValue) }
Expand All @@ -150,7 +241,7 @@ impl Readable for BlindedPath {
blinded_hops.push(Readable::read(r)?);
}
Ok(BlindedPath {
introduction_node_id,
introduction_node,
blinding_point,
blinded_hops,
})
Expand All @@ -162,3 +253,25 @@ impl_writeable!(BlindedHop, {
encrypted_payload
});

impl Direction {
/// Returns the [`NodeId`] from the inputs corresponding to the direction.
pub fn select_node_id<'a>(&self, node_a: &'a NodeId, node_b: &'a NodeId) -> &'a NodeId {
match self {
Direction::NodeOne => core::cmp::min(node_a, node_b),
Direction::NodeTwo => core::cmp::max(node_a, node_b),
}
}

/// Returns the [`PublicKey`] from the inputs corresponding to the direction.
pub fn select_pubkey<'a>(&self, node_a: &'a PublicKey, node_b: &'a PublicKey) -> &'a PublicKey {
let (node_one, node_two) = if NodeId::from_pubkey(node_a) < NodeId::from_pubkey(node_b) {
(node_a, node_b)
} else {
(node_b, node_a)
};
match self {
Direction::NodeOne => node_one,
Direction::NodeTwo => node_two,
}
}
}
19 changes: 18 additions & 1 deletion lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use bitcoin::secp256k1::{SecretKey,PublicKey};
use bitcoin::secp256k1::Secp256k1;
use bitcoin::{secp256k1, Sequence};

use crate::blinded_path::BlindedPath;
use crate::blinded_path::{BlindedPath, NodeIdLookUp};
use crate::blinded_path::payment::{PaymentConstraints, ReceiveTlvs};
use crate::chain;
use crate::chain::{Confirm, ChannelMonitorUpdateStatus, Watch, BestBlock};
Expand Down Expand Up @@ -10433,6 +10433,23 @@ where
}
}

impl<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>
NodeIdLookUp for ChannelManager<M, T, ES, NS, SP, F, R, L>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::EcdsaSigner>,
T::Target: BroadcasterInterface,
ES::Target: EntropySource,
NS::Target: NodeSigner,
SP::Target: SignerProvider,
F::Target: FeeEstimator,
R::Target: Router,
L::Target: Logger,
{
fn next_node_id(&self, short_channel_id: u64) -> Option<PublicKey> {
self.short_to_chan_info.read().unwrap().get(&short_channel_id).map(|(pubkey, _)| *pubkey)
}
}

/// Fetches the set of [`NodeFeatures`] flags that are provided by or required by
/// [`ChannelManager`].
pub(crate) fn provided_node_features(config: &UserConfig) -> NodeFeatures {
Expand Down