Skip to content

Commit

Permalink
Bolt12Invoice for Offer without signing_pubkey
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jkczyz committed Apr 26, 2024
1 parent 8232664 commit a70af2b
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 4 deletions.
97 changes: 93 additions & 4 deletions lightning/src/offers/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1434,8 +1434,8 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
features, signing_pubkey,
};

match offer_tlv_stream.node_id {
Some(expected_signing_pubkey) => {
match (offer_tlv_stream.node_id, &offer_tlv_stream.paths) {
(Some(expected_signing_pubkey), _) => {
if fields.signing_pubkey != expected_signing_pubkey {
return Err(Bolt12SemanticError::InvalidSigningPubkey);
}
Expand All @@ -1445,7 +1445,21 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
)?;
Ok(InvoiceContents::ForOffer { invoice_request, fields })
},
None => {
(None, Some(paths)) => {
if !paths
.iter()
.filter_map(|path| path.blinded_hops.last())
.any(|last_hop| fields.signing_pubkey == last_hop.blinded_node_id)
{
return Err(Bolt12SemanticError::InvalidSigningPubkey);
}

let invoice_request = InvoiceRequestContents::try_from(
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
)?;
Ok(InvoiceContents::ForOffer { invoice_request, fields })
},
(None, None) => {
let refund = RefundContents::try_from(
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
)?;
Expand All @@ -1463,7 +1477,7 @@ mod tests {
use bitcoin::blockdata::script::ScriptBuf;
use bitcoin::hashes::Hash;
use bitcoin::network::constants::Network;
use bitcoin::secp256k1::{Message, Secp256k1, XOnlyPublicKey, self};
use bitcoin::secp256k1::{KeyPair, Message, Secp256k1, SecretKey, XOnlyPublicKey, self};
use bitcoin::address::{Address, Payload, WitnessProgram, WitnessVersion};
use bitcoin::key::TweakedPublicKey;

Expand Down Expand Up @@ -2366,6 +2380,81 @@ mod tests {
}
}

#[test]
fn parses_invoice_with_node_id_from_blinded_path() {
let paths = vec![
BlindedPath {
introduction_node: IntroductionNode::NodeId(pubkey(40)),
blinding_point: pubkey(41),
blinded_hops: vec![
BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] },
],
},
BlindedPath {
introduction_node: IntroductionNode::NodeId(pubkey(40)),
blinding_point: pubkey(41),
blinded_hops: vec![
BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] },
BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] },
],
},
];

let blinded_node_id_sign = |message: &UnsignedBolt12Invoice| {
let secp_ctx = Secp256k1::new();
let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[46; 32]).unwrap());
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
};

let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
.clear_signing_pubkey()
.amount_msats(1000)
.path(paths[0].clone())
.path(paths[1].clone())
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
.respond_with_no_std_using_signing_pubkey(
payment_paths(), payment_hash(), now(), pubkey(46)
).unwrap()
.build().unwrap()
.sign(blinded_node_id_sign).unwrap();

let mut buffer = Vec::new();
invoice.write(&mut buffer).unwrap();

if let Err(e) = Bolt12Invoice::try_from(buffer) {
panic!("error parsing invoice: {:?}", e);
}

let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
.clear_signing_pubkey()
.amount_msats(1000)
.path(paths[0].clone())
.path(paths[1].clone())
.build().unwrap()
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
.respond_with_no_std_using_signing_pubkey(
payment_paths(), payment_hash(), now(), recipient_pubkey()
).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();

let mut buffer = Vec::new();
invoice.write(&mut buffer).unwrap();

match Bolt12Invoice::try_from(buffer) {
Ok(_) => panic!("expected error"),
Err(e) => {
assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidSigningPubkey));
},
}
}

#[test]
fn fails_parsing_invoice_without_signature() {
let mut buffer = Vec::new();
Expand Down
15 changes: 15 additions & 0 deletions lightning/src/offers/invoice_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,21 @@ macro_rules! invoice_request_respond_with_explicit_signing_pubkey_methods { (

<$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey)
}

#[cfg(test)]
#[allow(dead_code)]
pub(super) fn respond_with_no_std_using_signing_pubkey(
&$self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash,
created_at: core::time::Duration, signing_pubkey: PublicKey
) -> Result<$builder, Bolt12SemanticError> {
debug_assert!($contents.contents.inner.offer.signing_pubkey().is_none());

if $contents.invoice_request_features().requires_unknown_bits() {
return Err(Bolt12SemanticError::UnknownRequiredFeatures);
}

<$builder>::for_offer(&$contents, payment_paths, created_at, payment_hash, signing_pubkey)
}
} }

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

0 comments on commit a70af2b

Please sign in to comment.