Skip to content

Commit 7002180

Browse files
committed
Generalize BlindedPath::introduction_node_id field
Allow using either a node id or a directed short channel id in blinded paths. This allows for a more compact representation of blinded paths, which is advantageous for reducing offer QR code size. Follow-up commits will implement handling the directed short channel id case in OnionMessenger as it requires resolving the introduction node in MessageRouter.
1 parent e0bc6fa commit 7002180

File tree

11 files changed

+212
-97
lines changed

11 files changed

+212
-97
lines changed

fuzz/src/router.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use bitcoin::blockdata::constants::ChainHash;
1111
use bitcoin::blockdata::script::Builder;
1212
use bitcoin::blockdata::transaction::TxOut;
1313

14-
use lightning::blinded_path::{BlindedHop, BlindedPath};
14+
use lightning::blinded_path::{BlindedHop, BlindedPath, IntroductionNode};
1515
use lightning::chain::transaction::OutPoint;
1616
use lightning::ln::ChannelId;
1717
use lightning::ln::channelmanager::{self, ChannelDetails, ChannelCounterparty};
@@ -363,7 +363,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
363363
});
364364
}
365365
(payinfo, BlindedPath {
366-
introduction_node_id: hop.src_node_id,
366+
introduction_node: IntroductionNode::NodeId(hop.src_node_id),
367367
blinding_point: dummy_pk,
368368
blinded_hops,
369369
})

lightning/src/blinded_path/message.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
33
#[allow(unused_imports)]
44
use crate::prelude::*;
55

6-
use crate::blinded_path::{BlindedHop, BlindedPath};
6+
use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode};
77
use crate::blinded_path::utils;
88
use crate::io;
99
use crate::io::Cursor;
@@ -96,7 +96,7 @@ pub(crate) fn advance_path_by_one<NS: Deref, T: secp256k1::Signing + secp256k1::
9696
Ok(ChaChaPolyReadAdapter {
9797
readable: ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override })
9898
}) => {
99-
let mut next_node_id = match next_hop {
99+
let next_node_id = match next_hop {
100100
NextHop::NodeId(pubkey) => pubkey,
101101
NextHop::ShortChannelId(_) => todo!(),
102102
};
@@ -108,7 +108,7 @@ pub(crate) fn advance_path_by_one<NS: Deref, T: secp256k1::Signing + secp256k1::
108108
}
109109
};
110110
mem::swap(&mut path.blinding_point, &mut new_blinding_point);
111-
mem::swap(&mut path.introduction_node_id, &mut next_node_id);
111+
path.introduction_node = IntroductionNode::NodeId(next_node_id);
112112
Ok(())
113113
},
114114
_ => Err(())

lightning/src/blinded_path/mod.rs

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ use crate::prelude::*;
2929
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
3030
pub struct BlindedPath {
3131
/// To send to a blinded path, the sender first finds a route to the unblinded
32-
/// `introduction_node_id`, which can unblind its [`encrypted_payload`] to find out the onion
32+
/// `introduction_node`, which can unblind its [`encrypted_payload`] to find out the onion
3333
/// message or payment's next hop and forward it along.
3434
///
3535
/// [`encrypted_payload`]: BlindedHop::encrypted_payload
36-
pub introduction_node_id: PublicKey,
36+
pub introduction_node: IntroductionNode,
3737
/// Used by the introduction node to decrypt its [`encrypted_payload`] to forward the onion
3838
/// message or payment.
3939
///
@@ -43,6 +43,29 @@ pub struct BlindedPath {
4343
pub blinded_hops: Vec<BlindedHop>,
4444
}
4545

46+
/// The unblinded node in a [`BlindedPath`].
47+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
48+
pub enum IntroductionNode {
49+
/// The node id of the introduction node.
50+
NodeId(PublicKey),
51+
/// The short channel id of the channel leading to the introduction node. The [`Direction`]
52+
/// identifies which side of the channel is the introduction node.
53+
DirectedShortChannelId(Direction, u64),
54+
}
55+
56+
/// The side of a channel that is the [`IntroductionNode`] in a [`BlindedPath`]. [BOLT 7] defines
57+
/// which nodes is which in the [`ChannelAnnouncement`] message.
58+
///
59+
/// [BOLT 7]: https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_announcement-message
60+
/// [`ChannelAnnouncement`]: crate::ln::msgs::ChannelAnnouncement
61+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
62+
pub enum Direction {
63+
/// The lesser node id when compared lexicographically in ascending order.
64+
NodeOne,
65+
/// The greater node id when compared lexicographically in ascending order.
66+
NodeTwo,
67+
}
68+
4669
/// An encrypted payload and node id corresponding to a hop in a payment or onion message path, to
4770
/// be encoded in the sender's onion packet. These hops cannot be identified by outside observers
4871
/// and thus can be used to hide the identity of the recipient.
@@ -75,10 +98,10 @@ impl BlindedPath {
7598
if node_pks.is_empty() { return Err(()) }
7699
let blinding_secret_bytes = entropy_source.get_secure_random_bytes();
77100
let blinding_secret = SecretKey::from_slice(&blinding_secret_bytes[..]).expect("RNG is busted");
78-
let introduction_node_id = node_pks[0];
101+
let introduction_node = IntroductionNode::NodeId(node_pks[0]);
79102

80103
Ok(BlindedPath {
81-
introduction_node_id,
104+
introduction_node,
82105
blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret),
83106
blinded_hops: message::blinded_hops(secp_ctx, node_pks, &blinding_secret).map_err(|_| ())?,
84107
})
@@ -112,33 +135,59 @@ impl BlindedPath {
112135
payee_tlvs: payment::ReceiveTlvs, htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
113136
entropy_source: &ES, secp_ctx: &Secp256k1<T>
114137
) -> Result<(BlindedPayInfo, Self), ()> {
138+
let introduction_node = IntroductionNode::NodeId(
139+
intermediate_nodes.first().map_or(payee_node_id, |n| n.node_id)
140+
);
115141
let blinding_secret_bytes = entropy_source.get_secure_random_bytes();
116142
let blinding_secret = SecretKey::from_slice(&blinding_secret_bytes[..]).expect("RNG is busted");
117143

118144
let blinded_payinfo = payment::compute_payinfo(
119145
intermediate_nodes, &payee_tlvs, htlc_maximum_msat, min_final_cltv_expiry_delta
120146
)?;
121147
Ok((blinded_payinfo, BlindedPath {
122-
introduction_node_id: intermediate_nodes.first().map_or(payee_node_id, |n| n.node_id),
148+
introduction_node,
123149
blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret),
124150
blinded_hops: payment::blinded_hops(
125151
secp_ctx, intermediate_nodes, payee_node_id, payee_tlvs, &blinding_secret
126152
).map_err(|_| ())?,
127153
}))
128154
}
129155

130-
/// Returns the introduction [`NodeId`] of the blinded path.
156+
/// Returns the introduction [`NodeId`] of the blinded path, if it is publicly reachable (i.e.,
157+
/// it is found in the network graph).
131158
pub fn public_introduction_node_id<'a>(
132159
&self, network_graph: &'a ReadOnlyNetworkGraph
133160
) -> Option<&'a NodeId> {
134-
let node_id = NodeId::from_pubkey(&self.introduction_node_id);
135-
network_graph.nodes().get_key_value(&node_id).map(|(key, _)| key)
161+
match &self.introduction_node {
162+
IntroductionNode::NodeId(pubkey) => {
163+
let node_id = NodeId::from_pubkey(pubkey);
164+
network_graph.nodes().get_key_value(&node_id).map(|(key, _)| key)
165+
},
166+
IntroductionNode::DirectedShortChannelId(direction, scid) => {
167+
network_graph
168+
.channel(*scid)
169+
.map(|c| match direction {
170+
Direction::NodeOne => &c.node_one,
171+
Direction::NodeTwo => &c.node_two,
172+
})
173+
},
174+
}
136175
}
137176
}
138177

139178
impl Writeable for BlindedPath {
140179
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
141-
self.introduction_node_id.write(w)?;
180+
match &self.introduction_node {
181+
IntroductionNode::NodeId(pubkey) => pubkey.write(w)?,
182+
IntroductionNode::DirectedShortChannelId(direction, scid) => {
183+
match direction {
184+
Direction::NodeOne => 0u8.write(w)?,
185+
Direction::NodeTwo => 1u8.write(w)?,
186+
}
187+
scid.write(w)?;
188+
},
189+
}
190+
142191
self.blinding_point.write(w)?;
143192
(self.blinded_hops.len() as u8).write(w)?;
144193
for hop in &self.blinded_hops {
@@ -150,7 +199,17 @@ impl Writeable for BlindedPath {
150199

151200
impl Readable for BlindedPath {
152201
fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> {
153-
let introduction_node_id = Readable::read(r)?;
202+
let mut first_byte: u8 = Readable::read(r)?;
203+
let introduction_node = match first_byte {
204+
0 => IntroductionNode::DirectedShortChannelId(Direction::NodeOne, Readable::read(r)?),
205+
1 => IntroductionNode::DirectedShortChannelId(Direction::NodeTwo, Readable::read(r)?),
206+
2|3 => {
207+
use io::Read;
208+
let mut pubkey_read = core::slice::from_mut(&mut first_byte).chain(r.by_ref());
209+
IntroductionNode::NodeId(Readable::read(&mut pubkey_read)?)
210+
},
211+
_ => return Err(DecodeError::InvalidValue),
212+
};
154213
let blinding_point = Readable::read(r)?;
155214
let num_hops: u8 = Readable::read(r)?;
156215
if num_hops == 0 { return Err(DecodeError::InvalidValue) }
@@ -159,7 +218,7 @@ impl Readable for BlindedPath {
159218
blinded_hops.push(Readable::read(r)?);
160219
}
161220
Ok(BlindedPath {
162-
introduction_node_id,
221+
introduction_node,
163222
blinding_point,
164223
blinded_hops,
165224
})
@@ -171,3 +230,12 @@ impl_writeable!(BlindedHop, {
171230
encrypted_payload
172231
});
173232

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

lightning/src/ln/offers_tests.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
4343
use bitcoin::network::constants::Network;
4444
use core::time::Duration;
45-
use crate::blinded_path::BlindedPath;
45+
use crate::blinded_path::{BlindedPath, IntroductionNode};
4646
use crate::events::{Event, MessageSendEventsProvider, PaymentPurpose};
4747
use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, Retry, self};
4848
use crate::ln::functional_test_utils::*;
@@ -260,8 +260,8 @@ fn prefers_non_tor_nodes_in_blinded_paths() {
260260
assert_ne!(offer.signing_pubkey(), bob_id);
261261
assert!(!offer.paths().is_empty());
262262
for path in offer.paths() {
263-
assert_ne!(path.introduction_node_id, bob_id);
264-
assert_ne!(path.introduction_node_id, charlie_id);
263+
assert_ne!(path.introduction_node, IntroductionNode::NodeId(bob_id));
264+
assert_ne!(path.introduction_node, IntroductionNode::NodeId(charlie_id));
265265
}
266266

267267
// Use a one-hop blinded path when Bob is announced and all his peers are Tor-only.
@@ -275,7 +275,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() {
275275
assert_ne!(offer.signing_pubkey(), bob_id);
276276
assert!(!offer.paths().is_empty());
277277
for path in offer.paths() {
278-
assert_eq!(path.introduction_node_id, bob_id);
278+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id));
279279
}
280280
}
281281

@@ -325,7 +325,7 @@ fn prefers_more_connected_nodes_in_blinded_paths() {
325325
assert_ne!(offer.signing_pubkey(), bob_id);
326326
assert!(!offer.paths().is_empty());
327327
for path in offer.paths() {
328-
assert_eq!(path.introduction_node_id, nodes[4].node.get_our_node_id());
328+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(nodes[4].node.get_our_node_id()));
329329
}
330330
}
331331

@@ -374,7 +374,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
374374
assert_ne!(offer.signing_pubkey(), alice_id);
375375
assert!(!offer.paths().is_empty());
376376
for path in offer.paths() {
377-
assert_eq!(path.introduction_node_id, bob_id);
377+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id));
378378
}
379379

380380
let payment_id = PaymentId([1; 32]);
@@ -395,7 +395,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
395395
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
396396
assert_eq!(invoice_request.amount_msats(), None);
397397
assert_ne!(invoice_request.payer_id(), david_id);
398-
assert_eq!(reply_path.unwrap().introduction_node_id, charlie_id);
398+
assert_eq!(reply_path.unwrap().introduction_node, IntroductionNode::NodeId(charlie_id));
399399

400400
let onion_message = alice.onion_messenger.next_onion_message_for_peer(charlie_id).unwrap();
401401
charlie.onion_messenger.handle_onion_message(&alice_id, &onion_message);
@@ -408,7 +408,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
408408
assert_ne!(invoice.signing_pubkey(), alice_id);
409409
assert!(!invoice.payment_paths().is_empty());
410410
for (_, path) in invoice.payment_paths() {
411-
assert_eq!(path.introduction_node_id, bob_id);
411+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id));
412412
}
413413

414414
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
@@ -469,7 +469,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
469469
assert_ne!(refund.payer_id(), david_id);
470470
assert!(!refund.paths().is_empty());
471471
for path in refund.paths() {
472-
assert_eq!(path.introduction_node_id, charlie_id);
472+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(charlie_id));
473473
}
474474
expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id);
475475

@@ -488,7 +488,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
488488
assert_ne!(invoice.signing_pubkey(), alice_id);
489489
assert!(!invoice.payment_paths().is_empty());
490490
for (_, path) in invoice.payment_paths() {
491-
assert_eq!(path.introduction_node_id, bob_id);
491+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id));
492492
}
493493

494494
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
@@ -522,7 +522,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
522522
assert_ne!(offer.signing_pubkey(), alice_id);
523523
assert!(!offer.paths().is_empty());
524524
for path in offer.paths() {
525-
assert_eq!(path.introduction_node_id, alice_id);
525+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id));
526526
}
527527

528528
let payment_id = PaymentId([1; 32]);
@@ -535,7 +535,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
535535
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
536536
assert_eq!(invoice_request.amount_msats(), None);
537537
assert_ne!(invoice_request.payer_id(), bob_id);
538-
assert_eq!(reply_path.unwrap().introduction_node_id, bob_id);
538+
assert_eq!(reply_path.unwrap().introduction_node, IntroductionNode::NodeId(bob_id));
539539

540540
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
541541
bob.onion_messenger.handle_onion_message(&alice_id, &onion_message);
@@ -545,7 +545,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
545545
assert_ne!(invoice.signing_pubkey(), alice_id);
546546
assert!(!invoice.payment_paths().is_empty());
547547
for (_, path) in invoice.payment_paths() {
548-
assert_eq!(path.introduction_node_id, alice_id);
548+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id));
549549
}
550550

551551
route_bolt12_payment(bob, &[alice], &invoice);
@@ -585,7 +585,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
585585
assert_ne!(refund.payer_id(), bob_id);
586586
assert!(!refund.paths().is_empty());
587587
for path in refund.paths() {
588-
assert_eq!(path.introduction_node_id, bob_id);
588+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id));
589589
}
590590
expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
591591

@@ -599,7 +599,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
599599
assert_ne!(invoice.signing_pubkey(), alice_id);
600600
assert!(!invoice.payment_paths().is_empty());
601601
for (_, path) in invoice.payment_paths() {
602-
assert_eq!(path.introduction_node_id, alice_id);
602+
assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id));
603603
}
604604

605605
route_bolt12_payment(bob, &[alice], &invoice);

lightning/src/offers/invoice.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,7 +1455,7 @@ mod tests {
14551455

14561456
use core::time::Duration;
14571457

1458-
use crate::blinded_path::{BlindedHop, BlindedPath};
1458+
use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode};
14591459
use crate::sign::KeyMaterial;
14601460
use crate::ln::features::{Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures};
14611461
use crate::ln::inbound_payment::ExpandedKey;
@@ -1804,7 +1804,7 @@ mod tests {
18041804
let secp_ctx = Secp256k1::new();
18051805

18061806
let blinded_path = BlindedPath {
1807-
introduction_node_id: pubkey(40),
1807+
introduction_node: IntroductionNode::NodeId(pubkey(40)),
18081808
blinding_point: pubkey(41),
18091809
blinded_hops: vec![
18101810
BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] },

0 commit comments

Comments
 (0)