Skip to content

Commit a70af2b

Browse files
committed
Bolt12Invoice for Offer without signing_pubkey
When parsing a Bolt12Invoice use both the Offer's signing_pubkey and paths to determine if it is for an Offer or a Refund. Previously, an Offer was required to have a signing_pubkey. But now that it is optional, the Offers paths can be used to make the determination. Additionally, check that the invoice matches one of the blinded node ids from the paths' last hops.
1 parent 8232664 commit a70af2b

File tree

2 files changed

+108
-4
lines changed

2 files changed

+108
-4
lines changed

lightning/src/offers/invoice.rs

+93-4
Original file line numberDiff line numberDiff line change
@@ -1434,8 +1434,8 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
14341434
features, signing_pubkey,
14351435
};
14361436

1437-
match offer_tlv_stream.node_id {
1438-
Some(expected_signing_pubkey) => {
1437+
match (offer_tlv_stream.node_id, &offer_tlv_stream.paths) {
1438+
(Some(expected_signing_pubkey), _) => {
14391439
if fields.signing_pubkey != expected_signing_pubkey {
14401440
return Err(Bolt12SemanticError::InvalidSigningPubkey);
14411441
}
@@ -1445,7 +1445,21 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
14451445
)?;
14461446
Ok(InvoiceContents::ForOffer { invoice_request, fields })
14471447
},
1448-
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) => {
14491463
let refund = RefundContents::try_from(
14501464
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
14511465
)?;
@@ -1463,7 +1477,7 @@ mod tests {
14631477
use bitcoin::blockdata::script::ScriptBuf;
14641478
use bitcoin::hashes::Hash;
14651479
use bitcoin::network::constants::Network;
1466-
use bitcoin::secp256k1::{Message, Secp256k1, XOnlyPublicKey, self};
1480+
use bitcoin::secp256k1::{KeyPair, Message, Secp256k1, SecretKey, XOnlyPublicKey, self};
14671481
use bitcoin::address::{Address, Payload, WitnessProgram, WitnessVersion};
14681482
use bitcoin::key::TweakedPublicKey;
14691483

@@ -2366,6 +2380,81 @@ mod tests {
23662380
}
23672381
}
23682382

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+
23692458
#[test]
23702459
fn fails_parsing_invoice_without_signature() {
23712460
let mut buffer = Vec::new();

lightning/src/offers/invoice_request.rs

+15
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,21 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { (
754754

755755
<$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey)
756756
}
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)
771+
}
757772
} }
758773

759774
macro_rules! invoice_request_verify_method { ($self: ident, $self_type: ty) => {

0 commit comments

Comments
 (0)