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 creation #3011

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion fuzz/src/chanmon_consistency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hash_types::{BlockHash, WPubkeyHash};

use lightning::blinded_path::BlindedPath;
use lightning::blinded_path::message::ForwardNode;
use lightning::blinded_path::payment::ReceiveTlvs;
use lightning::chain;
use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, chainmonitor, channelmonitor, Confirm, Watch};
Expand Down Expand Up @@ -119,7 +120,7 @@ impl MessageRouter for FuzzRouter {
}

fn create_blinded_paths<T: secp256k1::Signing + secp256k1::Verification>(
&self, _recipient: PublicKey, _peers: Vec<PublicKey>, _secp_ctx: &Secp256k1<T>,
&self, _recipient: PublicKey, _peers: Vec<ForwardNode>, _secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPath>, ()> {
unreachable!()
}
Expand Down
3 changes: 2 additions & 1 deletion fuzz/src/full_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hash_types::{Txid, BlockHash, WPubkeyHash};

use lightning::blinded_path::BlindedPath;
use lightning::blinded_path::message::ForwardNode;
use lightning::blinded_path::payment::ReceiveTlvs;
use lightning::chain;
use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen};
Expand Down Expand Up @@ -157,7 +158,7 @@ impl MessageRouter for FuzzRouter {
}

fn create_blinded_paths<T: secp256k1::Signing + secp256k1::Verification>(
&self, _recipient: PublicKey, _peers: Vec<PublicKey>, _secp_ctx: &Secp256k1<T>,
&self, _recipient: PublicKey, _peers: Vec<ForwardNode>, _secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPath>, ()> {
unreachable!()
}
Expand Down
15 changes: 13 additions & 2 deletions fuzz/src/invoice_request_deser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use bitcoin::secp256k1::{KeyPair, Parity, PublicKey, Secp256k1, SecretKey, self}
use crate::utils::test_logger;
use core::convert::TryFrom;
use lightning::blinded_path::BlindedPath;
use lightning::blinded_path::message::ForwardNode;
use lightning::sign::EntropySource;
use lightning::ln::PaymentHash;
use lightning::ln::features::BlindedHopFeatures;
Expand Down Expand Up @@ -73,9 +74,19 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
invoice_request: &InvoiceRequest, secp_ctx: &Secp256k1<T>
) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> {
let entropy_source = Randomness {};
let intermediate_nodes = [
[
ForwardNode { node_id: pubkey(43), short_channel_id: None },
ForwardNode { node_id: pubkey(44), short_channel_id: None },
],
[
ForwardNode { node_id: pubkey(45), short_channel_id: None },
ForwardNode { node_id: pubkey(46), short_channel_id: None },
],
];
let paths = vec![
BlindedPath::new_for_message(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(),
BlindedPath::new_for_message(&[pubkey(45), pubkey(46), pubkey(42)], &entropy_source, secp_ctx).unwrap(),
BlindedPath::new_for_message(&intermediate_nodes[0], pubkey(42), &entropy_source, secp_ctx).unwrap(),
BlindedPath::new_for_message(&intermediate_nodes[1], pubkey(42), &entropy_source, secp_ctx).unwrap(),
];

let payinfo = vec![
Expand Down
3 changes: 2 additions & 1 deletion fuzz/src/onion_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use bitcoin::secp256k1::ecdsa::RecoverableSignature;
use bitcoin::secp256k1::schnorr;

use lightning::blinded_path::{BlindedPath, EmptyNodeIdLookUp};
use lightning::blinded_path::message::ForwardNode;
use lightning::ln::features::InitFeatures;
use lightning::ln::msgs::{self, DecodeError, OnionMessageHandler};
use lightning::ln::script::ShutdownScript;
Expand Down Expand Up @@ -88,7 +89,7 @@ impl MessageRouter for TestMessageRouter {
}

fn create_blinded_paths<T: secp256k1::Signing + secp256k1::Verification>(
&self, _recipient: PublicKey, _peers: Vec<PublicKey>, _secp_ctx: &Secp256k1<T>,
&self, _recipient: PublicKey, _peers: Vec<ForwardNode>, _secp_ctx: &Secp256k1<T>,
) -> Result<Vec<BlindedPath>, ()> {
unreachable!()
}
Expand Down
15 changes: 13 additions & 2 deletions fuzz/src/refund_deser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey, self};
use crate::utils::test_logger;
use core::convert::TryFrom;
use lightning::blinded_path::BlindedPath;
use lightning::blinded_path::message::ForwardNode;
use lightning::sign::EntropySource;
use lightning::ln::PaymentHash;
use lightning::ln::features::BlindedHopFeatures;
Expand Down Expand Up @@ -62,9 +63,19 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
refund: &Refund, signing_pubkey: PublicKey, secp_ctx: &Secp256k1<T>
) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> {
let entropy_source = Randomness {};
let intermediate_nodes = [
[
ForwardNode { node_id: pubkey(43), short_channel_id: None },
ForwardNode { node_id: pubkey(44), short_channel_id: None },
],
[
ForwardNode { node_id: pubkey(45), short_channel_id: None },
ForwardNode { node_id: pubkey(46), short_channel_id: None },
],
];
let paths = vec![
BlindedPath::new_for_message(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(),
BlindedPath::new_for_message(&[pubkey(45), pubkey(46), pubkey(42)], &entropy_source, secp_ctx).unwrap(),
BlindedPath::new_for_message(&intermediate_nodes[0], pubkey(42), &entropy_source, secp_ctx).unwrap(),
BlindedPath::new_for_message(&intermediate_nodes[1], pubkey(42), &entropy_source, secp_ctx).unwrap(),
];

let payinfo = vec![
Expand Down
43 changes: 37 additions & 6 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
// This file is Copyright its original authors, visible in version control
// history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

//! Data structures and methods for constructing [`BlindedPath`]s to send a message over.
jkczyz marked this conversation as resolved.
Show resolved Hide resolved
//!
//! [`BlindedPath`]: crate::blinded_path::BlindedPath

use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};

#[allow(unused_imports)]
Expand All @@ -16,6 +29,17 @@ use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Writeable, Writer}
use core::mem;
use core::ops::Deref;

/// An intermediate node, and possibly a short channel id leading to the next node.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct ForwardNode {
jkczyz marked this conversation as resolved.
Show resolved Hide resolved
/// This node's pubkey.
pub node_id: PublicKey,
/// The channel between `node_id` and the next hop. If set, the constructed [`BlindedHop`]'s
/// `encrypted_payload` will use this instead of the next [`ForwardNode::node_id`] for a more
/// compact representation.
pub short_channel_id: Option<u64>,
}

/// 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 {
Expand Down Expand Up @@ -60,17 +84,24 @@ impl Writeable for ReceiveTlvs {
}
}

/// Construct blinded onion message hops for the given `unblinded_path`.
/// Construct blinded onion message hops for the given `intermediate_nodes` and `recipient_node_id`.
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, unblinded_path: &[PublicKey], session_priv: &SecretKey
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[ForwardNode], recipient_node_id: PublicKey,
session_priv: &SecretKey
) -> Result<Vec<BlindedHop>, secp256k1::Error> {
let blinded_tlvs = unblinded_path.iter()
let pks = intermediate_nodes.iter().map(|node| &node.node_id)
.chain(core::iter::once(&recipient_node_id));
let tlvs = pks.clone()
.skip(1) // The first node's TLVs contains the next node's pubkey
.map(|pk| ForwardTlvs { next_hop: NextMessageHop::NodeId(*pk), next_blinding_override: None })
.map(|tlvs| ControlTlvs::Forward(tlvs))
.zip(intermediate_nodes.iter().map(|node| node.short_channel_id))
.map(|(pubkey, scid)| match scid {
Some(scid) => NextMessageHop::ShortChannelId(scid),
None => NextMessageHop::NodeId(*pubkey),
})
.map(|next_hop| ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None }))
.chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { path_id: None })));

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

// Advance the blinded onion message path by one hop, so make the second hop into the new
Expand Down
46 changes: 40 additions & 6 deletions lightning/src/blinded_path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! Creating blinded paths and related utilities live here.

pub mod payment;
pub(crate) mod message;
pub mod message;
pub(crate) mod utils;

use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
Expand All @@ -21,6 +21,7 @@ use crate::offers::invoice::BlindedPayInfo;
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
use crate::sign::EntropySource;
use crate::util::ser::{Readable, Writeable, Writer};
use crate::util::scid_utils;
jkczyz marked this conversation as resolved.
Show resolved Hide resolved

use crate::io;
use crate::prelude::*;
Expand Down Expand Up @@ -124,7 +125,7 @@ impl BlindedPath {
pub fn one_hop_for_message<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
recipient_node_id: PublicKey, entropy_source: ES, secp_ctx: &Secp256k1<T>
) -> Result<Self, ()> where ES::Target: EntropySource {
Self::new_for_message(&[recipient_node_id], entropy_source, secp_ctx)
Self::new_for_message(&[], recipient_node_id, entropy_source, secp_ctx)
}

/// Create a blinded path for an onion message, to be forwarded along `node_pks`. The last node
Expand All @@ -133,17 +134,21 @@ impl BlindedPath {
/// Errors if no hops are provided or if `node_pk`(s) are invalid.
// TODO: make all payloads the same size with padding + add dummy hops
pub fn new_for_message<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
node_pks: &[PublicKey], entropy_source: ES, secp_ctx: &Secp256k1<T>
intermediate_nodes: &[message::ForwardNode], recipient_node_id: PublicKey,
entropy_source: ES, secp_ctx: &Secp256k1<T>
) -> Result<Self, ()> where ES::Target: EntropySource {
if node_pks.is_empty() { return Err(()) }
let introduction_node = IntroductionNode::NodeId(
intermediate_nodes.first().map_or(recipient_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 introduction_node = IntroductionNode::NodeId(node_pks[0]);

Ok(BlindedPath {
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(|_| ())?,
blinded_hops: message::blinded_hops(
secp_ctx, intermediate_nodes, recipient_node_id, &blinding_secret,
).map_err(|_| ())?,
})
}

Expand Down Expand Up @@ -213,6 +218,35 @@ impl BlindedPath {
},
}
}

/// Attempts to a use a compact representation for the [`IntroductionNode`] by using a directed
/// short channel id from a channel in `network_graph` leading to the introduction node.
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should probably have some kind of discussion of how this makes paths shorter but if a channel closes will invalidate it.

///
/// While this may result in a smaller encoding, there is a trade off in that the path may
/// become invalid if the channel is closed or hasn't been propagated via gossip. Therefore,
/// calling this may not be suitable for long-lived blinded paths.
pub fn use_compact_introduction_node(&mut self, network_graph: &ReadOnlyNetworkGraph) {
if let IntroductionNode::NodeId(pubkey) = &self.introduction_node {
let node_id = NodeId::from_pubkey(pubkey);
if let Some(node_info) = network_graph.node(&node_id) {
if let Some((scid, channel_info)) = node_info
.channels
.iter()
.filter_map(|scid| network_graph.channel(*scid).map(|info| (*scid, info)))
.min_by_key(|(scid, _)| scid_utils::block_from_scid(*scid))
{
let direction = if node_id == channel_info.node_one {
Direction::NodeOne
} else {
debug_assert_eq!(node_id, channel_info.node_two);
Direction::NodeTwo
};
self.introduction_node =
IntroductionNode::DirectedShortChannelId(direction, scid);
}
}
}
}
}

impl Writeable for BlindedPath {
Expand Down
9 changes: 9 additions & 0 deletions lightning/src/blinded_path/payment.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
// This file is Copyright its original authors, visible in version control
// history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

//! Data structures and methods for constructing [`BlindedPath`]s to send a payment over.
//!
//! [`BlindedPath`]: crate::blinded_path::BlindedPath
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1403,7 +1403,7 @@ pub(super) struct ChannelContext<SP: Deref> where SP::Target: SignerProvider {
/// Either the height at which this channel was created or the height at which it was last
/// serialized if it was serialized by versions prior to 0.0.103.
/// We use this to close if funding is never broadcasted.
channel_creation_height: u32,
pub(super) channel_creation_height: u32,

counterparty_dust_limit_satoshis: u64,

Expand Down
13 changes: 11 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use bitcoin::secp256k1::Secp256k1;
use bitcoin::{secp256k1, Sequence};

use crate::blinded_path::{BlindedPath, NodeIdLookUp};
use crate::blinded_path::message::ForwardNode;
use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentConstraints, PaymentContext, ReceiveTlvs};
use crate::chain;
use crate::chain::{Confirm, ChannelMonitorUpdateStatus, Watch, BestBlock};
Expand Down Expand Up @@ -8996,8 +8997,16 @@ where

let peers = self.per_peer_state.read().unwrap()
.iter()
.filter(|(_, peer)| peer.lock().unwrap().latest_features.supports_onion_messages())
.map(|(node_id, _)| *node_id)
.map(|(node_id, peer_state)| (node_id, peer_state.lock().unwrap()))
.filter(|(_, peer)| peer.latest_features.supports_onion_messages())
.map(|(node_id, peer)| ForwardNode {
node_id: *node_id,
short_channel_id: peer.channel_by_id
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we make setting this optional somehow? I feel like if I'm building a super long-term offer I may have a different preference from something being scanned right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah... arguably we shouldn't bother with it for reply paths, either. Just not sure exactly how we want to convey it through the MessageRouter trait. Currently, the caller makes the decision for the penultimate hop using ForwardNode, but when adding more hops the MessageRouter makes the decision since it needs a NetworkGraph to find more hops. Similarly for the introduction node.

So right now it's partly an implementation concern given you need a NetworkGraph. I guess we can just add a bool parameter and it say it is best effort? Any other ideas?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see what you mean... if there's an obvious "default behavior" that stands out, we could have a separate method, e.g. create_long_term_blinded_paths, or have a Config struct. That way we could also have a config setting for compact offers vs offers that don't need to be QR-scanned, or privacy-oriented offers that want longer blinded paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Opened #3080 with separate MessageRouter methods, using compact paths for Offer::paths and Refund::paths and non-compact paths for onion message reply paths. Basically, the trait allows either for the caller to decide.

As for how this is exposed in ChannelManager utilities, I'm not sure how we should handle short- vs long-lived offers. One thought was to chose the type of path based on the expiry. But for offers, the expiry is set by the user after the builder is returned and path already set. For refunds created via ChannelManager, we require an expiration (even though the spec does not), though, so we could infer there.

Alternatives would be:

  • making a special purpose create_offer_builder for long-lived offers
  • adding a parameter to create_offer_builder indicating if short- or long-lived
  • adding a Optional absolute expiry parameter to create_offer_builder used to infer which type of path to create

Any preferences other alternatives?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm good with whichever of those options you think is best!

.iter()
.filter(|(_, channel)| channel.context().is_usable())
.min_by_key(|(_, channel)| channel.context().channel_creation_height)
.and_then(|(_, channel)| channel.context().get_short_channel_id()),
})
.collect::<Vec<_>>();

self.router
Expand Down