Skip to content

offer: make the merkle tree signature public #3892

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 105 additions & 3 deletions lightning/src/offers/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ use crate::offers::merkle::{
};
use crate::offers::nonce::Nonce;
use crate::offers::offer::{
Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferTlvStream,
Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferId, OfferTlvStream,
OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES,
};
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
Expand Down Expand Up @@ -686,6 +686,13 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
// Append the experimental bytes after the signature.
$self.bytes.extend_from_slice(&$self.experimental_bytes);

let offer_id = match &$self.contents {
InvoiceContents::ForOffer { .. } => {
Some(OfferId::from_valid_bolt12_tlv_stream(&$self.bytes))
},
InvoiceContents::ForRefund { .. } => None,
};

Ok(Bolt12Invoice {
#[cfg(not(c_bindings))]
bytes: $self.bytes,
Expand All @@ -700,6 +707,7 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
tagged_hash: $self.tagged_hash,
#[cfg(c_bindings)]
tagged_hash: $self.tagged_hash.clone(),
offer_id,
})
}
} }
Expand Down Expand Up @@ -734,6 +742,7 @@ pub struct Bolt12Invoice {
contents: InvoiceContents,
signature: Signature,
tagged_hash: TaggedHash,
offer_id: Option<OfferId>,
}

/// The contents of an [`Bolt12Invoice`] for responding to either an [`Offer`] or a [`Refund`].
Expand Down Expand Up @@ -967,6 +976,13 @@ impl Bolt12Invoice {
self.tagged_hash.as_digest().as_ref().clone()
}

/// Returns the [`OfferId`] if this invoice corresponds to an [`Offer`].
///
/// [`Offer`]: crate::offers::offer::Offer
pub fn offer_id(&self) -> Option<OfferId> {
self.offer_id
}

/// Verifies that the invoice was for a request or refund created using the given key by
/// checking the payer metadata from the invoice request.
///
Expand Down Expand Up @@ -1032,6 +1048,11 @@ impl Bolt12Invoice {
InvoiceContents::ForRefund { .. } => self.message_paths().is_empty(),
}
}

/// Returns the [`TaggedHash`] of the invoice that was signed.
pub fn tagged_hash(&self) -> &TaggedHash {
&self.tagged_hash
}
}

impl PartialEq for Bolt12Invoice {
Expand Down Expand Up @@ -1626,7 +1647,11 @@ impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Bolt12Invoice {
let pubkey = contents.fields().signing_pubkey;
merkle::verify_signature(&signature, &tagged_hash, pubkey)?;

Ok(Bolt12Invoice { bytes, contents, signature, tagged_hash })
let offer_id = match &contents {
InvoiceContents::ForOffer { .. } => Some(OfferId::from_valid_bolt12_tlv_stream(&bytes)),
InvoiceContents::ForRefund { .. } => None,
};
Ok(Bolt12Invoice { bytes, contents, signature, tagged_hash, offer_id })
}
}

Expand Down Expand Up @@ -1785,7 +1810,6 @@ mod tests {
use bitcoin::script::ScriptBuf;
use bitcoin::secp256k1::{self, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey};
use bitcoin::{CompressedPublicKey, WitnessProgram, WitnessVersion};

use core::time::Duration;

use crate::blinded_path::message::BlindedMessagePath;
Expand Down Expand Up @@ -3560,4 +3584,82 @@ mod tests {
),
}
}

#[test]
fn invoice_offer_id_matches_offer_id() {
let expanded_key = ExpandedKey::new([42; 32]);
let entropy = FixedEntropy {};
let nonce = Nonce::from_entropy_source(&entropy);
let secp_ctx = Secp256k1::new();
let payment_id = PaymentId([1; 32]);

let offer = OfferBuilder::new(recipient_pubkey()).amount_msats(1000).build().unwrap();

let offer_id = offer.id();

let invoice_request = offer
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)
.unwrap()
.build_and_sign()
.unwrap();

let invoice = invoice_request
.respond_with_no_std(payment_paths(), payment_hash(), now())
.unwrap()
.build()
.unwrap()
.sign(recipient_sign)
.unwrap();

assert_eq!(invoice.offer_id(), Some(offer_id));
}

#[test]
fn refund_invoice_has_no_offer_id() {
let refund =
RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap().build().unwrap();

let invoice = refund
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
.unwrap()
.build()
.unwrap()
.sign(recipient_sign)
.unwrap();

assert_eq!(invoice.offer_id(), None);
}

#[test]
fn verifies_invoice_signature_with_tagged_hash() {
let secp_ctx = Secp256k1::new();
let expanded_key = ExpandedKey::new([42; 32]);
let entropy = FixedEntropy {};
let nonce = Nonce::from_entropy_source(&entropy);
let node_id = recipient_pubkey();
let payment_paths = payment_paths();
let now = Duration::from_secs(123456);
let payment_id = PaymentId([1; 32]);

let offer = OfferBuilder::new(node_id).amount_msats(1000).build().unwrap();

let invoice_request = offer
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)
.unwrap()
.build_and_sign()
.unwrap();

let invoice = invoice_request
.respond_with_no_std(payment_paths, payment_hash(), now)
.unwrap()
.build()
.unwrap()
.sign(recipient_sign)
.unwrap();

let issuer_sign_pubkey = offer.issuer_signing_pubkey().unwrap();
let tagged_hash = invoice.tagged_hash();
let signature = invoice.signature();
assert!(merkle::verify_signature(&signature, tagged_hash, issuer_sign_pubkey).is_ok());
}
}
8 changes: 3 additions & 5 deletions lightning/src/offers/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub enum SignError {
}

/// A function for signing a [`TaggedHash`].
pub(super) trait SignFn<T: AsRef<TaggedHash>> {
pub trait SignFn<T: AsRef<TaggedHash>> {
/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream.
fn sign(&self, message: &T) -> Result<Signature, ()>;
}
Expand All @@ -117,9 +117,7 @@ where
///
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
pub(super) fn sign_message<F, T>(
f: F, message: &T, pubkey: PublicKey,
) -> Result<Signature, SignError>
pub fn sign_message<F, T>(f: F, message: &T, pubkey: PublicKey) -> Result<Signature, SignError>
where
F: SignFn<T>,
T: AsRef<TaggedHash>,
Expand All @@ -136,7 +134,7 @@ where

/// Verifies the signature with a pubkey over the given message using a tagged hash as the message
/// digest.
pub(super) fn verify_signature(
pub fn verify_signature(
signature: &Signature, message: &TaggedHash, pubkey: PublicKey,
) -> Result<(), secp256k1::Error> {
let digest = message.as_digest();
Expand Down
4 changes: 2 additions & 2 deletions lightning/src/offers/offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ impl OfferId {
Self(tagged_hash.to_bytes())
}

fn from_valid_invreq_tlv_stream(bytes: &[u8]) -> Self {
pub(super) fn from_valid_bolt12_tlv_stream(bytes: &[u8]) -> Self {
let tlv_stream = Offer::tlv_stream_iter(bytes);
let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, tlv_stream);
Self(tagged_hash.to_bytes())
Expand Down Expand Up @@ -987,7 +987,7 @@ impl OfferContents {
secp_ctx,
)?;

let offer_id = OfferId::from_valid_invreq_tlv_stream(bytes);
let offer_id = OfferId::from_valid_bolt12_tlv_stream(bytes);

Ok((offer_id, keys))
},
Expand Down
49 changes: 46 additions & 3 deletions lightning/src/offers/static_invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use crate::offers::merkle::{
use crate::offers::nonce::Nonce;
use crate::offers::offer::{
Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, OfferContents,
OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES,
OfferId, OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES,
};
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
use crate::types::features::{Bolt12InvoiceFeatures, OfferFeatures};
Expand Down Expand Up @@ -70,6 +70,7 @@ pub struct StaticInvoice {
bytes: Vec<u8>,
contents: InvoiceContents,
signature: Signature,
offer_id: OfferId,
}

impl PartialEq for StaticInvoice {
Expand Down Expand Up @@ -347,7 +348,8 @@ impl UnsignedStaticInvoice {
// Append the experimental bytes after the signature.
self.bytes.extend_from_slice(&self.experimental_bytes);

Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature })
let offer_id = OfferId::from_valid_bolt12_tlv_stream(&self.bytes);
Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature, offer_id })
}

invoice_accessors_common!(self, self.contents, UnsignedStaticInvoice);
Expand Down Expand Up @@ -407,6 +409,13 @@ impl StaticInvoice {
self.contents.is_offer_expired_no_std(duration_since_epoch)
}

/// Returns the [`OfferId`] corresponding to the originating [`Offer`].
///
/// [`Offer`]: crate::offers::offer::Offer
pub fn offer_id(&self) -> OfferId {
self.offer_id
}

#[allow(unused)] // TODO: remove this once we remove the `async_payments` cfg flag
pub(crate) fn is_from_same_offer(&self, invreq: &InvoiceRequest) -> bool {
let invoice_offer_tlv_stream =
Expand Down Expand Up @@ -642,7 +651,8 @@ impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for StaticInvoice {
let pubkey = contents.signing_pubkey;
merkle::verify_signature(&signature, &tagged_hash, pubkey)?;

Ok(StaticInvoice { bytes, contents, signature })
let offer_id = OfferId::from_valid_bolt12_tlv_stream(&bytes);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a chunk more hashing on load. Its probably not crazy given we already do a signature validation, but if we assume most folks won't use it, does it make more sense to expose a calculate_offer_id method that does it live instead?

Ok(StaticInvoice { bytes, contents, signature, offer_id })
}
}

Expand Down Expand Up @@ -1666,4 +1676,37 @@ mod tests {
},
}
}

#[test]
fn static_invoice_offer_id_matches_offer_id() {
let node_id = recipient_pubkey();
let payment_paths = payment_paths();
let now = now();
let expanded_key = ExpandedKey::new([42; 32]);
let entropy = FixedEntropy {};
let nonce = Nonce::from_entropy_source(&entropy);
let secp_ctx = Secp256k1::new();

let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
.path(blinded_path())
.build()
.unwrap();

let offer_id = offer.id();

let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys(
&offer,
payment_paths.clone(),
vec![blinded_path()],
now,
&expanded_key,
nonce,
&secp_ctx,
)
.unwrap()
.build_and_sign(&secp_ctx)
.unwrap();

assert_eq!(invoice.offer_id(), offer_id);
}
}
Loading