Skip to content

Commit 3497b59

Browse files
authored
Merge pull request #4335 from TheBlueMatt/2025-01-phantom-bolt12
Add support for "phantom" BOLT 12 offers, up to the invoice_request step
2 parents 5ec66dc + 8022d1e commit 3497b59

File tree

16 files changed

+427
-104
lines changed

16 files changed

+427
-104
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@
201201
generated for inclusion in BOLT 12 `Offer`s will no longer be accepted. As
202202
most blinded message paths are ephemeral, this should only invalidate issued
203203
BOLT 12 `Refund`s in practice (#3917).
204+
* Blinded message paths included in BOLT 12 `Offer`s generated by LDK 0.2 will
205+
not be accepted by prior versions of LDK after downgrade (#3917).
204206
* Once a channel has been spliced, LDK can no longer be downgraded.
205207
`UserConfig::reject_inbound_splices` can be set to block inbound ones (#4150)
206208
* Downgrading after setting `UserConfig::enable_htlc_hold` is not supported

ext-functional-test-demo/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod tests {
1717
impl TestSignerFactory for BrokenSignerFactory {
1818
fn make_signer(
1919
&self, _seed: &[u8; 32], _now: Duration, _v2_remote_key_derivation: bool,
20+
_phantom_seed: Option<&[u8; 32]>,
2021
) -> Box<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>> {
2122
panic!()
2223
}

fuzz/src/onion_message.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ impl NodeSigner for KeyProvider {
260260
}
261261

262262
fn get_expanded_key(&self) -> ExpandedKey {
263-
unreachable!()
263+
ExpandedKey::new([42; 32])
264264
}
265265

266266
fn sign_invoice(

lightning/src/blinded_path/payment.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
1414

1515
use crate::blinded_path::utils::{self, BlindedPathWithPadding};
1616
use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp};
17-
use crate::crypto::streams::ChaChaDualPolyReadAdapter;
17+
use crate::crypto::streams::{ChaChaTriPolyReadAdapter, TriPolyAADUsed};
1818
use crate::io;
1919
use crate::io::Cursor;
2020
use crate::ln::channel_state::CounterpartyForwardingInfo;
@@ -268,18 +268,20 @@ impl BlindedPaymentPath {
268268
node_signer.ecdh(Recipient::Node, &self.inner_path.blinding_point, None)?;
269269
let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes());
270270
let receive_auth_key = node_signer.get_receive_auth_key();
271+
let phantom_auth_key = node_signer.get_expanded_key().phantom_node_blinded_path_key;
272+
let read_arg = (rho, receive_auth_key.0, phantom_auth_key);
273+
271274
let encrypted_control_tlvs =
272275
&self.inner_path.blinded_hops.get(0).ok_or(())?.encrypted_payload;
273276
let mut s = Cursor::new(encrypted_control_tlvs);
274277
let mut reader = FixedLengthReader::new(&mut s, encrypted_control_tlvs.len() as u64);
275-
let ChaChaDualPolyReadAdapter { readable, used_aad } =
276-
ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0))
277-
.map_err(|_| ())?;
278-
279-
match (&readable, used_aad) {
280-
(BlindedPaymentTlvs::Forward(_), false)
281-
| (BlindedPaymentTlvs::Dummy(_), true)
282-
| (BlindedPaymentTlvs::Receive(_), true) => Ok((readable, control_tlvs_ss)),
278+
let ChaChaTriPolyReadAdapter { readable, used_aad } =
279+
ChaChaTriPolyReadAdapter::read(&mut reader, read_arg).map_err(|_| ())?;
280+
281+
match (&readable, used_aad == TriPolyAADUsed::None) {
282+
(BlindedPaymentTlvs::Forward(_), true)
283+
| (BlindedPaymentTlvs::Dummy(_), false)
284+
| (BlindedPaymentTlvs::Receive(_), false) => Ok((readable, control_tlvs_ss)),
283285
_ => Err(()),
284286
}
285287
}

lightning/src/crypto/streams.rs

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ impl<'a, T: Writeable> Writeable for ChaChaPolyWriteAdapter<'a, T> {
5858
}
5959

6060
/// Encrypts the provided plaintext with the given key using ChaCha20Poly1305 in the modified
61-
/// with-AAD form used in [`ChaChaDualPolyReadAdapter`].
61+
/// with-AAD form used in [`ChaChaTriPolyReadAdapter`].
6262
pub(crate) fn chachapoly_encrypt_with_swapped_aad(
6363
mut plaintext: Vec<u8>, key: [u8; 32], aad: [u8; 32],
6464
) -> Vec<u8> {
@@ -84,34 +84,48 @@ pub(crate) fn chachapoly_encrypt_with_swapped_aad(
8484
plaintext
8585
}
8686

87+
#[derive(PartialEq, Eq)]
88+
pub(crate) enum TriPolyAADUsed {
89+
/// No AAD was used.
90+
///
91+
/// The HMAC validated with standard ChaCha20Poly1305.
92+
None,
93+
/// The HMAC vlidated using the first AAD provided.
94+
First,
95+
/// The HMAC vlidated using the second AAD provided.
96+
Second,
97+
}
98+
8799
/// Enables the use of the serialization macros for objects that need to be simultaneously decrypted
88100
/// and deserialized. This allows us to avoid an intermediate Vec allocation.
89101
///
90-
/// This variant of [`ChaChaPolyReadAdapter`] calculates Poly1305 tags twice, once using the given
91-
/// key and once with the given 32-byte AAD appended after the encrypted stream, accepting either
92-
/// being correct as sufficient.
102+
/// This variant of [`ChaChaPolyReadAdapter`] calculates Poly1305 tags thrice, once using the given
103+
/// key and once each for the two given 32-byte AADs appended after the encrypted stream, accepting
104+
/// any being correct as sufficient.
93105
///
94-
/// Note that we do *not* use the provided AAD as the standard ChaCha20Poly1305 AAD as that would
106+
/// Note that we do *not* use the provided AADs as the standard ChaCha20Poly1305 AAD as that would
95107
/// require placing it first and prevent us from avoiding redundant Poly1305 rounds. Instead, the
96108
/// ChaCha20Poly1305 MAC check is tweaked to move the AAD to *after* the the contents being
97109
/// checked, effectively treating the contents as the AAD for the AAD-containing MAC but behaving
98110
/// like classic ChaCha20Poly1305 for the non-AAD-containing MAC.
99-
pub(crate) struct ChaChaDualPolyReadAdapter<R: Readable> {
111+
pub(crate) struct ChaChaTriPolyReadAdapter<R: Readable> {
100112
pub readable: R,
101-
pub used_aad: bool,
113+
pub used_aad: TriPolyAADUsed,
102114
}
103115

104-
impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyReadAdapter<T> {
116+
impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32], [u8; 32])>
117+
for ChaChaTriPolyReadAdapter<T>
118+
{
105119
// Simultaneously read and decrypt an object from a LengthLimitedRead storing it in
106120
// Self::readable. LengthLimitedRead must be used instead of std::io::Read because we need the
107121
// total length to separate out the tag at the end.
108122
fn read<R: LengthLimitedRead>(
109-
r: &mut R, params: ([u8; 32], [u8; 32]),
123+
r: &mut R, params: ([u8; 32], [u8; 32], [u8; 32]),
110124
) -> Result<Self, DecodeError> {
111125
if r.remaining_bytes() < 16 {
112126
return Err(DecodeError::InvalidValue);
113127
}
114-
let (key, aad) = params;
128+
let (key, aad_a, aad_b) = params;
115129

116130
let mut chacha = ChaCha20::new(&key[..], &[0; 12]);
117131
let mut mac_key = [0u8; 64];
@@ -125,7 +139,7 @@ impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyRea
125139
let decrypted_len = r.remaining_bytes() - 16;
126140
let s = FixedLengthReader::new(r, decrypted_len);
127141
let mut chacha_stream =
128-
ChaChaDualPolyReader { chacha: &mut chacha, poly: &mut mac, read_len: 0, read: s };
142+
ChaChaTriPolyReader { chacha: &mut chacha, poly: &mut mac, read_len: 0, read: s };
129143

130144
let readable: T = Readable::read(&mut chacha_stream)?;
131145
while chacha_stream.read.bytes_remain() {
@@ -142,14 +156,18 @@ impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyRea
142156
mac.input(&[0; 16][0..16 - (read_len % 16)]);
143157
}
144158

145-
let mut mac_aad = mac;
159+
let mut mac_aad_a = mac;
160+
let mut mac_aad_b = mac;
146161

147-
mac_aad.input(&aad[..]);
162+
mac_aad_a.input(&aad_a[..]);
163+
mac_aad_b.input(&aad_b[..]);
148164
// Note that we don't need to pad the AAD since its a multiple of 16 bytes
149165

150166
// For the AAD-containing MAC, swap the AAD and the read data, effectively.
151-
mac_aad.input(&(read_len as u64).to_le_bytes());
152-
mac_aad.input(&32u64.to_le_bytes());
167+
mac_aad_a.input(&(read_len as u64).to_le_bytes());
168+
mac_aad_b.input(&(read_len as u64).to_le_bytes());
169+
mac_aad_a.input(&32u64.to_le_bytes());
170+
mac_aad_b.input(&32u64.to_le_bytes());
153171

154172
// For the non-AAD-containing MAC, leave the data and AAD where they belong.
155173
mac.input(&0u64.to_le_bytes());
@@ -158,23 +176,25 @@ impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyRea
158176
let mut tag = [0 as u8; 16];
159177
r.read_exact(&mut tag)?;
160178
if fixed_time_eq(&mac.result(), &tag) {
161-
Ok(Self { readable, used_aad: false })
162-
} else if fixed_time_eq(&mac_aad.result(), &tag) {
163-
Ok(Self { readable, used_aad: true })
179+
Ok(Self { readable, used_aad: TriPolyAADUsed::None })
180+
} else if fixed_time_eq(&mac_aad_a.result(), &tag) {
181+
Ok(Self { readable, used_aad: TriPolyAADUsed::First })
182+
} else if fixed_time_eq(&mac_aad_b.result(), &tag) {
183+
Ok(Self { readable, used_aad: TriPolyAADUsed::Second })
164184
} else {
165185
return Err(DecodeError::InvalidValue);
166186
}
167187
}
168188
}
169189

170-
struct ChaChaDualPolyReader<'a, R: Read> {
190+
struct ChaChaTriPolyReader<'a, R: Read> {
171191
chacha: &'a mut ChaCha20,
172192
poly: &'a mut Poly1305,
173193
read_len: usize,
174194
pub read: R,
175195
}
176196

177-
impl<'a, R: Read> Read for ChaChaDualPolyReader<'a, R> {
197+
impl<'a, R: Read> Read for ChaChaTriPolyReader<'a, R> {
178198
// Decrypts bytes from Self::read into `dest`.
179199
// After all reads complete, the caller must compare the expected tag with
180200
// the result of `Poly1305::result()`.
@@ -349,15 +369,15 @@ mod tests {
349369
}
350370

351371
#[test]
352-
fn short_read_chacha_dual_read_adapter() {
353-
// Previously, if we attempted to read from a ChaChaDualPolyReadAdapter but the object
372+
fn short_read_chacha_tri_read_adapter() {
373+
// Previously, if we attempted to read from a ChaChaTriPolyReadAdapter but the object
354374
// being read is shorter than the available buffer while the buffer passed to
355-
// ChaChaDualPolyReadAdapter itself always thinks it has room, we'd end up
375+
// ChaChaTriPolyReadAdapter itself always thinks it has room, we'd end up
356376
// infinite-looping as we didn't handle `Read::read`'s 0 return values at EOF.
357377
let mut stream = &[0; 1024][..];
358378
let mut too_long_stream = FixedLengthReader::new(&mut stream, 2048);
359-
let keys = ([42; 32], [99; 32]);
360-
let res = super::ChaChaDualPolyReadAdapter::<u8>::read(&mut too_long_stream, keys);
379+
let keys = ([42; 32], [98; 32], [99; 32]);
380+
let res = super::ChaChaTriPolyReadAdapter::<u8>::read(&mut too_long_stream, keys);
361381
match res {
362382
Ok(_) => panic!(),
363383
Err(e) => assert_eq!(e, DecodeError::ShortRead),

lightning/src/crypto/utils.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ macro_rules! hkdf_extract_expand {
2222
let (k1, k2, _) = hkdf_extract_expand!($salt, $ikm);
2323
(k1, k2)
2424
}};
25-
($salt: expr, $ikm: expr, 6) => {{
25+
($salt: expr, $ikm: expr, 7) => {{
2626
let (k1, k2, prk) = hkdf_extract_expand!($salt, $ikm);
2727

2828
let mut hmac = HmacEngine::<Sha256>::new(&prk[..]);
@@ -45,18 +45,23 @@ macro_rules! hkdf_extract_expand {
4545
hmac.input(&[6; 1]);
4646
let k6 = Hmac::from_engine(hmac).to_byte_array();
4747

48-
(k1, k2, k3, k4, k5, k6)
48+
let mut hmac = HmacEngine::<Sha256>::new(&prk[..]);
49+
hmac.input(&k6);
50+
hmac.input(&[7; 1]);
51+
let k7 = Hmac::from_engine(hmac).to_byte_array();
52+
53+
(k1, k2, k3, k4, k5, k6, k7)
4954
}};
5055
}
5156

5257
pub fn hkdf_extract_expand_twice(salt: &[u8], ikm: &[u8]) -> ([u8; 32], [u8; 32]) {
5358
hkdf_extract_expand!(salt, ikm, 2)
5459
}
5560

56-
pub fn hkdf_extract_expand_6x(
61+
pub fn hkdf_extract_expand_7x(
5762
salt: &[u8], ikm: &[u8],
58-
) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) {
59-
hkdf_extract_expand!(salt, ikm, 6)
63+
) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) {
64+
hkdf_extract_expand!(salt, ikm, 7)
6065
}
6166

6267
#[inline]

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,7 +1696,7 @@ fn route_blinding_spec_test_vector() {
16961696
}
16971697
Ok(SharedSecret::new(other_key, &node_secret))
16981698
}
1699-
fn get_expanded_key(&self) -> ExpandedKey { unreachable!() }
1699+
fn get_expanded_key(&self) -> ExpandedKey { ExpandedKey::new([42; 32]) }
17001700
fn get_node_id(&self, _recipient: Recipient) -> Result<PublicKey, ()> { unreachable!() }
17011701
fn sign_invoice(
17021702
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient,
@@ -2011,7 +2011,7 @@ fn test_trampoline_inbound_payment_decoding() {
20112011
}
20122012
Ok(SharedSecret::new(other_key, &node_secret))
20132013
}
2014-
fn get_expanded_key(&self) -> ExpandedKey { unreachable!() }
2014+
fn get_expanded_key(&self) -> ExpandedKey { ExpandedKey::new([42; 32]) }
20152015
fn get_node_id(&self, _recipient: Recipient) -> Result<PublicKey, ()> { unreachable!() }
20162016
fn sign_invoice(
20172017
&self, _invoice: &RawBolt11Invoice, _recipient: Recipient,

lightning/src/ln/channelmanager.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13568,6 +13568,47 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => {
1356813568

1356913569
Ok(builder.into())
1357013570
}
13571+
13572+
/// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by any
13573+
/// [`ChannelManager`] (or [`OffersMessageFlow`]) using the same [`ExpandedKey`] (as returned
13574+
/// from [`NodeSigner::get_expanded_key`]). This allows any nodes participating in a BOLT 11
13575+
/// "phantom node" cluster to also receive BOLT 12 payments.
13576+
///
13577+
/// Note that, unlike with BOLT 11 invoices, BOLT 12 "phantom" offers do not in fact have any
13578+
/// "phantom node" appended to receiving paths. Instead, multiple blinded paths are simply
13579+
/// included which terminate at different final nodes.
13580+
///
13581+
/// `other_nodes_channels` must be set to a list of each participating node's `node_id` (from
13582+
/// [`NodeSigner::get_node_id`] with a [`Recipient::Node`]) and its channels.
13583+
///
13584+
/// `path_count_limit` is used to limit the number of blinded paths included in the resulting
13585+
/// [`Offer`]. Note that if this is less than the number of participating nodes (i.e.
13586+
/// `other_nodes_channels.len() + 1`) not all nodes will participate in receiving funds.
13587+
/// Because the parameterized [`MessageRouter`] will only get a chance to limit the number of
13588+
/// paths *per-node*, it is important to set this for offers that will be included in a QR
13589+
/// code.
13590+
///
13591+
/// See [`Self::create_offer_builder`] for more details on the blinded path construction.
13592+
///
13593+
/// [`ExpandedKey`]: inbound_payment::ExpandedKey
13594+
pub fn create_phantom_offer_builder(
13595+
&$self, other_nodes_channels: Vec<(PublicKey, Vec<ChannelDetails>)>,
13596+
path_count_limit: usize,
13597+
) -> Result<$builder, Bolt12SemanticError> {
13598+
let mut peers = Vec::with_capacity(other_nodes_channels.len() + 1);
13599+
if !other_nodes_channels.iter().any(|(node_id, _)| *node_id == $self.get_our_node_id()) {
13600+
peers.push(($self.get_our_node_id(), $self.get_peers_for_blinded_path()));
13601+
}
13602+
for (node_id, peer_chans) in other_nodes_channels {
13603+
peers.push((node_id, Self::channel_details_to_forward_nodes(peer_chans)));
13604+
}
13605+
13606+
let builder = $self.flow.create_phantom_offer_builder(
13607+
&$self.entropy_source, peers, path_count_limit
13608+
)?;
13609+
13610+
Ok(builder.into())
13611+
}
1357113612
} }
1357213613

1357313614
macro_rules! create_refund_builder { ($self: ident, $builder: ty) => {
@@ -14184,6 +14225,43 @@ impl<
1418414225
now
1418514226
}
1418614227

14228+
/// Converts a list of channels to a list of peers which may be suitable to receive onion
14229+
/// messages through.
14230+
fn channel_details_to_forward_nodes(
14231+
mut channel_list: Vec<ChannelDetails>,
14232+
) -> Vec<MessageForwardNode> {
14233+
channel_list.sort_unstable_by_key(|chan| chan.counterparty.node_id);
14234+
let mut res = Vec::new();
14235+
// TODO: When MSRV reaches 1.77 use chunk_by
14236+
let mut start = 0;
14237+
while start < channel_list.len() {
14238+
let counterparty_node_id = channel_list[start].counterparty.node_id;
14239+
let end = channel_list[start..]
14240+
.iter()
14241+
.position(|chan| chan.counterparty.node_id != counterparty_node_id)
14242+
.map(|pos| start + pos)
14243+
.unwrap_or(channel_list.len());
14244+
14245+
let peer_chans = &channel_list[start..end];
14246+
if peer_chans.iter().any(|chan| chan.is_usable)
14247+
&& peer_chans.iter().any(|c| c.counterparty.features.supports_onion_messages())
14248+
{
14249+
res.push(MessageForwardNode {
14250+
node_id: peer_chans[0].counterparty.node_id,
14251+
short_channel_id: peer_chans
14252+
.iter()
14253+
.filter(|chan| chan.is_usable)
14254+
// Select the channel which has the highest local balance. We assume this
14255+
// channel is the most likely to stick around.
14256+
.max_by_key(|chan| chan.inbound_capacity_msat)
14257+
.and_then(|chan| chan.get_inbound_payment_scid()),
14258+
})
14259+
}
14260+
start = end;
14261+
}
14262+
res
14263+
}
14264+
1418714265
fn get_peers_for_blinded_path(&self) -> Vec<MessageForwardNode> {
1418814266
let per_peer_state = self.per_peer_state.read().unwrap();
1418914267
per_peer_state
@@ -14198,7 +14276,9 @@ impl<
1419814276
.iter()
1419914277
.filter(|(_, channel)| channel.context().is_usable())
1420014278
.filter_map(|(_, channel)| channel.as_funded())
14201-
.min_by_key(|funded_channel| funded_channel.context.channel_creation_height)
14279+
// Select the channel which has the highest local balance. We assume this
14280+
// channel is the most likely to stick around.
14281+
.max_by_key(|funded_channel| funded_channel.funding.get_value_to_self_msat())
1420214282
.and_then(|funded_channel| funded_channel.get_inbound_scid()),
1420314283
})
1420414284
.collect::<Vec<_>>()

0 commit comments

Comments
 (0)