Skip to content

Commit 2b14cc4

Browse files
authored
Merge pull request #3017 from jkczyz/2024-04-optional-pubkey
Sending to `Offer` without `signing_pubkey`
2 parents d00e550 + b7635c4 commit 2b14cc4

File tree

7 files changed

+227
-56
lines changed

7 files changed

+227
-56
lines changed

lightning/src/ln/channelmanager.rs

+11-8
Original file line numberDiff line numberDiff line change
@@ -8757,14 +8757,7 @@ where
87578757
.map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?;
87588758

87598759
let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap();
8760-
if offer.paths().is_empty() {
8761-
let message = new_pending_onion_message(
8762-
OffersMessage::InvoiceRequest(invoice_request),
8763-
Destination::Node(offer.signing_pubkey()),
8764-
Some(reply_path),
8765-
);
8766-
pending_offers_messages.push(message);
8767-
} else {
8760+
if !offer.paths().is_empty() {
87688761
// Send as many invoice requests as there are paths in the offer (with an upper bound).
87698762
// Using only one path could result in a failure if the path no longer exists. But only
87708763
// one invoice for a given payment id will be paid, even if more than one is received.
@@ -8777,6 +8770,16 @@ where
87778770
);
87788771
pending_offers_messages.push(message);
87798772
}
8773+
} else if let Some(signing_pubkey) = offer.signing_pubkey() {
8774+
let message = new_pending_onion_message(
8775+
OffersMessage::InvoiceRequest(invoice_request),
8776+
Destination::Node(signing_pubkey),
8777+
Some(reply_path),
8778+
);
8779+
pending_offers_messages.push(message);
8780+
} else {
8781+
debug_assert!(false);
8782+
return Err(Bolt12SemanticError::MissingSigningPubkey);
87808783
}
87818784

87828785
Ok(())

lightning/src/ln/offers_tests.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() {
270270
.create_offer_builder("coffee".to_string()).unwrap()
271271
.amount_msats(10_000_000)
272272
.build().unwrap();
273-
assert_ne!(offer.signing_pubkey(), bob_id);
273+
assert_ne!(offer.signing_pubkey(), Some(bob_id));
274274
assert!(!offer.paths().is_empty());
275275
for path in offer.paths() {
276276
assert_ne!(path.introduction_node, IntroductionNode::NodeId(bob_id));
@@ -285,7 +285,7 @@ fn prefers_non_tor_nodes_in_blinded_paths() {
285285
.create_offer_builder("coffee".to_string()).unwrap()
286286
.amount_msats(10_000_000)
287287
.build().unwrap();
288-
assert_ne!(offer.signing_pubkey(), bob_id);
288+
assert_ne!(offer.signing_pubkey(), Some(bob_id));
289289
assert!(!offer.paths().is_empty());
290290
for path in offer.paths() {
291291
assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id));
@@ -335,7 +335,7 @@ fn prefers_more_connected_nodes_in_blinded_paths() {
335335
.create_offer_builder("coffee".to_string()).unwrap()
336336
.amount_msats(10_000_000)
337337
.build().unwrap();
338-
assert_ne!(offer.signing_pubkey(), bob_id);
338+
assert_ne!(offer.signing_pubkey(), Some(bob_id));
339339
assert!(!offer.paths().is_empty());
340340
for path in offer.paths() {
341341
assert_eq!(path.introduction_node, IntroductionNode::NodeId(nodes[4].node.get_our_node_id()));
@@ -385,7 +385,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
385385
.unwrap()
386386
.amount_msats(10_000_000)
387387
.build().unwrap();
388-
assert_ne!(offer.signing_pubkey(), alice_id);
388+
assert_ne!(offer.signing_pubkey(), Some(alice_id));
389389
assert!(!offer.paths().is_empty());
390390
for path in offer.paths() {
391391
assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id));
@@ -544,7 +544,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
544544
.create_offer_builder("coffee".to_string()).unwrap()
545545
.amount_msats(10_000_000)
546546
.build().unwrap();
547-
assert_ne!(offer.signing_pubkey(), alice_id);
547+
assert_ne!(offer.signing_pubkey(), Some(alice_id));
548548
assert!(!offer.paths().is_empty());
549549
for path in offer.paths() {
550550
assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id));
@@ -667,7 +667,7 @@ fn pays_for_offer_without_blinded_paths() {
667667
.clear_paths()
668668
.amount_msats(10_000_000)
669669
.build().unwrap();
670-
assert_eq!(offer.signing_pubkey(), alice_id);
670+
assert_eq!(offer.signing_pubkey(), Some(alice_id));
671671
assert!(offer.paths().is_empty());
672672

673673
let payment_id = PaymentId([1; 32]);

lightning/src/offers/invoice.rs

+97-7
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,9 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods { ($self: ident, $s
210210
#[cfg_attr(c_bindings, allow(dead_code))]
211211
pub(super) fn for_offer(
212212
invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
213-
created_at: Duration, payment_hash: PaymentHash
213+
created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey
214214
) -> Result<Self, Bolt12SemanticError> {
215215
let amount_msats = Self::amount_msats(invoice_request)?;
216-
let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey();
217216
let contents = InvoiceContents::ForOffer {
218217
invoice_request: invoice_request.contents.clone(),
219218
fields: Self::fields(
@@ -272,7 +271,7 @@ macro_rules! invoice_derived_signing_pubkey_builder_methods { ($self: ident, $se
272271
created_at: Duration, payment_hash: PaymentHash, keys: KeyPair
273272
) -> Result<Self, Bolt12SemanticError> {
274273
let amount_msats = Self::amount_msats(invoice_request)?;
275-
let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey();
274+
let signing_pubkey = keys.public_key();
276275
let contents = InvoiceContents::ForOffer {
277276
invoice_request: invoice_request.contents.clone(),
278277
fields: Self::fields(
@@ -1435,8 +1434,8 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
14351434
features, signing_pubkey,
14361435
};
14371436

1438-
match offer_tlv_stream.node_id {
1439-
Some(expected_signing_pubkey) => {
1437+
match (offer_tlv_stream.node_id, &offer_tlv_stream.paths) {
1438+
(Some(expected_signing_pubkey), _) => {
14401439
if fields.signing_pubkey != expected_signing_pubkey {
14411440
return Err(Bolt12SemanticError::InvalidSigningPubkey);
14421441
}
@@ -1446,7 +1445,21 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
14461445
)?;
14471446
Ok(InvoiceContents::ForOffer { invoice_request, fields })
14481447
},
1449-
None => {
1448+
(None, Some(paths)) => {
1449+
if !paths
1450+
.iter()
1451+
.filter_map(|path| path.blinded_hops.last())
1452+
.any(|last_hop| fields.signing_pubkey == last_hop.blinded_node_id)
1453+
{
1454+
return Err(Bolt12SemanticError::InvalidSigningPubkey);
1455+
}
1456+
1457+
let invoice_request = InvoiceRequestContents::try_from(
1458+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
1459+
)?;
1460+
Ok(InvoiceContents::ForOffer { invoice_request, fields })
1461+
},
1462+
(None, None) => {
14501463
let refund = RefundContents::try_from(
14511464
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
14521465
)?;
@@ -1464,7 +1477,7 @@ mod tests {
14641477
use bitcoin::blockdata::script::ScriptBuf;
14651478
use bitcoin::hashes::Hash;
14661479
use bitcoin::network::constants::Network;
1467-
use bitcoin::secp256k1::{Message, Secp256k1, XOnlyPublicKey, self};
1480+
use bitcoin::secp256k1::{KeyPair, Message, Secp256k1, SecretKey, XOnlyPublicKey, self};
14681481
use bitcoin::address::{Address, Payload, WitnessProgram, WitnessVersion};
14691482
use bitcoin::key::TweakedPublicKey;
14701483

@@ -1633,6 +1646,7 @@ mod tests {
16331646
quantity: None,
16341647
payer_id: Some(&payer_pubkey()),
16351648
payer_note: None,
1649+
paths: None,
16361650
},
16371651
InvoiceTlvStreamRef {
16381652
paths: Some(Iterable(payment_paths.iter().map(|(_, path)| path))),
@@ -1725,6 +1739,7 @@ mod tests {
17251739
quantity: None,
17261740
payer_id: Some(&payer_pubkey()),
17271741
payer_note: None,
1742+
paths: None,
17281743
},
17291744
InvoiceTlvStreamRef {
17301745
paths: Some(Iterable(payment_paths.iter().map(|(_, path)| path))),
@@ -2365,6 +2380,81 @@ mod tests {
23652380
}
23662381
}
23672382

2383+
#[test]
2384+
fn parses_invoice_with_node_id_from_blinded_path() {
2385+
let paths = vec![
2386+
BlindedPath {
2387+
introduction_node: IntroductionNode::NodeId(pubkey(40)),
2388+
blinding_point: pubkey(41),
2389+
blinded_hops: vec![
2390+
BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
2391+
BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] },
2392+
],
2393+
},
2394+
BlindedPath {
2395+
introduction_node: IntroductionNode::NodeId(pubkey(40)),
2396+
blinding_point: pubkey(41),
2397+
blinded_hops: vec![
2398+
BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] },
2399+
BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] },
2400+
],
2401+
},
2402+
];
2403+
2404+
let blinded_node_id_sign = |message: &UnsignedBolt12Invoice| {
2405+
let secp_ctx = Secp256k1::new();
2406+
let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[46; 32]).unwrap());
2407+
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
2408+
};
2409+
2410+
let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
2411+
.clear_signing_pubkey()
2412+
.amount_msats(1000)
2413+
.path(paths[0].clone())
2414+
.path(paths[1].clone())
2415+
.build().unwrap()
2416+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
2417+
.build().unwrap()
2418+
.sign(payer_sign).unwrap()
2419+
.respond_with_no_std_using_signing_pubkey(
2420+
payment_paths(), payment_hash(), now(), pubkey(46)
2421+
).unwrap()
2422+
.build().unwrap()
2423+
.sign(blinded_node_id_sign).unwrap();
2424+
2425+
let mut buffer = Vec::new();
2426+
invoice.write(&mut buffer).unwrap();
2427+
2428+
if let Err(e) = Bolt12Invoice::try_from(buffer) {
2429+
panic!("error parsing invoice: {:?}", e);
2430+
}
2431+
2432+
let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
2433+
.clear_signing_pubkey()
2434+
.amount_msats(1000)
2435+
.path(paths[0].clone())
2436+
.path(paths[1].clone())
2437+
.build().unwrap()
2438+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
2439+
.build().unwrap()
2440+
.sign(payer_sign).unwrap()
2441+
.respond_with_no_std_using_signing_pubkey(
2442+
payment_paths(), payment_hash(), now(), recipient_pubkey()
2443+
).unwrap()
2444+
.build().unwrap()
2445+
.sign(recipient_sign).unwrap();
2446+
2447+
let mut buffer = Vec::new();
2448+
invoice.write(&mut buffer).unwrap();
2449+
2450+
match Bolt12Invoice::try_from(buffer) {
2451+
Ok(_) => panic!("expected error"),
2452+
Err(e) => {
2453+
assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidSigningPubkey));
2454+
},
2455+
}
2456+
}
2457+
23682458
#[test]
23692459
fn fails_parsing_invoice_without_signature() {
23702460
let mut buffer = Vec::new();

lightning/src/offers/invoice_request.rs

+42-5
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,27 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { (
747747
return Err(Bolt12SemanticError::UnknownRequiredFeatures);
748748
}
749749

750-
<$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash)
750+
let signing_pubkey = match $contents.contents.inner.offer.signing_pubkey() {
751+
Some(signing_pubkey) => signing_pubkey,
752+
None => return Err(Bolt12SemanticError::MissingSigningPubkey),
753+
};
754+
755+
<$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey)
756+
}
757+
758+
#[cfg(test)]
759+
#[allow(dead_code)]
760+
pub(super) fn respond_with_no_std_using_signing_pubkey(
761+
&$self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash,
762+
created_at: core::time::Duration, signing_pubkey: PublicKey
763+
) -> Result<$builder, Bolt12SemanticError> {
764+
debug_assert!($contents.contents.inner.offer.signing_pubkey().is_none());
765+
766+
if $contents.invoice_request_features().requires_unknown_bits() {
767+
return Err(Bolt12SemanticError::UnknownRequiredFeatures);
768+
}
769+
770+
<$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey)
751771
}
752772
} }
753773

@@ -855,6 +875,11 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { (
855875
Some(keys) => keys,
856876
};
857877

878+
match $contents.contents.inner.offer.signing_pubkey() {
879+
Some(signing_pubkey) => debug_assert_eq!(signing_pubkey, keys.public_key()),
880+
None => return Err(Bolt12SemanticError::MissingSigningPubkey),
881+
}
882+
858883
<$builder>::for_offer_using_keys(
859884
&$self.inner, payment_paths, created_at, payment_hash, keys
860885
)
@@ -959,6 +984,7 @@ impl InvoiceRequestContentsWithoutPayerId {
959984
quantity: self.quantity,
960985
payer_id: None,
961986
payer_note: self.payer_note.as_ref(),
987+
paths: None,
962988
};
963989

964990
(payer, offer, invoice_request)
@@ -991,13 +1017,17 @@ pub(super) const INVOICE_REQUEST_TYPES: core::ops::Range<u64> = 80..160;
9911017
/// [`Refund::payer_id`]: crate::offers::refund::Refund::payer_id
9921018
pub(super) const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
9931019

1020+
// This TLV stream is used for both InvoiceRequest and Refund, but not all TLV records are valid for
1021+
// InvoiceRequest as noted below.
9941022
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
9951023
(80, chain: ChainHash),
9961024
(82, amount: (u64, HighZeroBytesDroppedBigSize)),
9971025
(84, features: (InvoiceRequestFeatures, WithoutLength)),
9981026
(86, quantity: (u64, HighZeroBytesDroppedBigSize)),
9991027
(INVOICE_REQUEST_PAYER_ID_TYPE, payer_id: PublicKey),
10001028
(89, payer_note: (String, WithoutLength)),
1029+
// Only used for Refund since the onion message of an InvoiceRequest has a reply path.
1030+
(90, paths: (Vec<BlindedPath>, WithoutLength)),
10011031
});
10021032

10031033
type FullInvoiceRequestTlvStream =
@@ -1080,7 +1110,9 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
10801110
let (
10811111
PayerTlvStream { metadata },
10821112
offer_tlv_stream,
1083-
InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note },
1113+
InvoiceRequestTlvStream {
1114+
chain, amount, features, quantity, payer_id, payer_note, paths,
1115+
},
10841116
) = tlv_stream;
10851117

10861118
let payer = match metadata {
@@ -1107,6 +1139,10 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
11071139
Some(payer_id) => payer_id,
11081140
};
11091141

1142+
if paths.is_some() {
1143+
return Err(Bolt12SemanticError::UnexpectedPaths);
1144+
}
1145+
11101146
Ok(InvoiceRequestContents {
11111147
inner: InvoiceRequestContentsWithoutPayerId {
11121148
payer, offer, chain, amount_msats: amount, features, quantity, payer_note,
@@ -1218,7 +1254,7 @@ mod tests {
12181254
assert_eq!(unsigned_invoice_request.paths(), &[]);
12191255
assert_eq!(unsigned_invoice_request.issuer(), None);
12201256
assert_eq!(unsigned_invoice_request.supported_quantity(), Quantity::One);
1221-
assert_eq!(unsigned_invoice_request.signing_pubkey(), recipient_pubkey());
1257+
assert_eq!(unsigned_invoice_request.signing_pubkey(), Some(recipient_pubkey()));
12221258
assert_eq!(unsigned_invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin));
12231259
assert_eq!(unsigned_invoice_request.amount_msats(), None);
12241260
assert_eq!(unsigned_invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty());
@@ -1250,7 +1286,7 @@ mod tests {
12501286
assert_eq!(invoice_request.paths(), &[]);
12511287
assert_eq!(invoice_request.issuer(), None);
12521288
assert_eq!(invoice_request.supported_quantity(), Quantity::One);
1253-
assert_eq!(invoice_request.signing_pubkey(), recipient_pubkey());
1289+
assert_eq!(invoice_request.signing_pubkey(), Some(recipient_pubkey()));
12541290
assert_eq!(invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin));
12551291
assert_eq!(invoice_request.amount_msats(), None);
12561292
assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty());
@@ -1285,6 +1321,7 @@ mod tests {
12851321
quantity: None,
12861322
payer_id: Some(&payer_pubkey()),
12871323
payer_note: None,
1324+
paths: None,
12881325
},
12891326
SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) },
12901327
),
@@ -2245,7 +2282,7 @@ mod tests {
22452282
.amount_msats(1000)
22462283
.supported_quantity(Quantity::Unbounded)
22472284
.build().unwrap();
2248-
assert_eq!(offer.signing_pubkey(), node_id);
2285+
assert_eq!(offer.signing_pubkey(), Some(node_id));
22492286

22502287
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
22512288
.chain(Network::Testnet).unwrap()

0 commit comments

Comments
 (0)