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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
71 changes: 69 additions & 2 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 @@ -665,6 +665,14 @@ impl UnsignedBolt12Invoice {
pub fn tagged_hash(&self) -> &TaggedHash {
&self.tagged_hash
}

/// Computes the [`OfferId`] if this invoice corresponds to an [`Offer`].
fn compute_offer_id(&self) -> Option<OfferId> {
match &self.contents {
InvoiceContents::ForOffer { .. } => Some(OfferId::from_invoice_bytes(&self.bytes)),
InvoiceContents::ForRefund { .. } => None,
}
}
}

macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $self_mut: tt)?) => {
Expand All @@ -686,6 +694,9 @@ 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);

// Compute offer_id before moving fields
let offer_id = $self.compute_offer_id();

Ok(Bolt12Invoice {
#[cfg(not(c_bindings))]
bytes: $self.bytes,
Expand All @@ -700,6 +711,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 +746,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 +980,11 @@ impl Bolt12Invoice {
self.tagged_hash.as_digest().as_ref().clone()
}

/// Returns the [`OfferId`] if this invoice corresponds to an [`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 @@ -1622,7 +1640,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_invoice_bytes(&bytes)),
InvoiceContents::ForRefund { .. } => None,
};
Ok(Bolt12Invoice { bytes, contents, signature, tagged_hash, offer_id })
}
}

Expand Down Expand Up @@ -3556,4 +3578,49 @@ 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);
}
}
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
9 changes: 9 additions & 0 deletions lightning/src/offers/offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ impl OfferId {
let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, tlv_stream);
Self(tagged_hash.to_bytes())
}

/// Computes the [`OfferId`] from a [`Bolt12Invoice`] bytes.
pub(crate) fn from_invoice_bytes(bytes: &[u8]) -> Self {
let offer_tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES);
let experimental_offer_tlv_stream = TlvStream::new(bytes).range(EXPERIMENTAL_OFFER_TYPES);
let combined_tlv_stream = offer_tlv_stream.chain(experimental_offer_tlv_stream);
let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, combined_tlv_stream);
Self(tagged_hash.to_bytes())
}
}

impl Borrow<[u8]> for OfferId {
Expand Down
Loading