Skip to content
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

BOLT 12 invoice encoding and building #1926

Merged
merged 11 commits into from
Jan 23, 2023
1,565 changes: 1,565 additions & 0 deletions lightning/src/offers/invoice.rs

Large diffs are not rendered by default.

54 changes: 48 additions & 6 deletions lightning/src/offers/invoice_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
//!
//! An [`InvoiceRequest`] can be built from a parsed [`Offer`] as an "offer to be paid". It is
//! typically constructed by a customer and sent to the merchant who had published the corresponding
//! offer. The recipient of the request responds with an `Invoice`.
//! offer. The recipient of the request responds with an [`Invoice`].
//!
//! For an "offer for money" (e.g., refund, ATM withdrawal), where an offer doesn't exist as a
//! precursor, see [`Refund`].
//!
//! [`Invoice`]: crate::offers::invoice::Invoice
//! [`Refund`]: crate::offers::refund::Refund
//!
//! ```ignore
Expand Down Expand Up @@ -57,12 +58,15 @@ use bitcoin::secp256k1::{Message, PublicKey};
use bitcoin::secp256k1::schnorr::Signature;
use core::convert::TryFrom;
use crate::io;
use crate::ln::PaymentHash;
use crate::ln::features::InvoiceRequestFeatures;
use crate::ln::msgs::DecodeError;
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self};
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
use crate::onion_message::BlindedPath;
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
use crate::util::string::PrintableString;

Expand Down Expand Up @@ -239,24 +243,27 @@ impl<'a> UnsignedInvoiceRequest<'a> {
}
}

/// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`].
/// An `InvoiceRequest` is a request for an [`Invoice`] formulated from an [`Offer`].
///
/// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request
/// specifies these such that its recipient can send an invoice for payment.
///
/// [`Invoice`]: crate::offers::invoice::Invoice
/// [`Offer`]: crate::offers::offer::Offer
#[derive(Clone, Debug)]
pub struct InvoiceRequest {
pub(super) bytes: Vec<u8>,
contents: InvoiceRequestContents,
pub(super) contents: InvoiceRequestContents,
signature: Signature,
}

/// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`.
/// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`].
///
/// [`Invoice`]: crate::offers::invoice::Invoice
#[derive(Clone, Debug)]
pub(super) struct InvoiceRequestContents {
payer: PayerContents,
offer: OfferContents,
pub(super) offer: OfferContents,
chain: Option<ChainHash>,
amount_msats: Option<u64>,
features: InvoiceRequestFeatures,
Expand Down Expand Up @@ -315,6 +322,41 @@ impl InvoiceRequest {
self.signature
}

/// Creates an [`Invoice`] for the request with the given required fields.
///
/// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after
/// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter
/// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`]
/// is not available.
///
/// The caller is expected to remember the preimage of `payment_hash` in order to claim a payment
Copy link
Contributor

Choose a reason for hiding this comment

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

lightning-invoice has links to ChannelManager::create_inbound_payment*, may want some here too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll wait until the code is used in onion message handling before documenting. For Offers that code will construct the payment hash, though responding to a Refund may need some utility.

/// for the invoice.
///
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this could use more details about why it's required even when you're using a public node id. Maybe could live on Invoice::payment_paths with a docs link here

/// must contain one or more elements.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should also note that these should be specified in order of most preferred to least preferred

///
/// Errors if the request contains unknown required features.
///
/// [`Duration`]: core::time::Duration
/// [`Invoice`]: crate::offers::invoice::Invoice
/// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at
pub fn respond_with(
&self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
#[cfg(any(test, not(feature = "std")))]
created_at: core::time::Duration
) -> Result<InvoiceBuilder, SemanticError> {
if self.features().requires_unknown_bits() {
return Err(SemanticError::UnknownRequiredFeatures);
}

#[cfg(all(not(test), feature = "std"))]
let created_at = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");

InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash)
}

#[cfg(test)]
fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef {
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) =
Expand All @@ -327,7 +369,7 @@ impl InvoiceRequest {
}

impl InvoiceRequestContents {
fn chain(&self) -> ChainHash {
pub(super) fn chain(&self) -> ChainHash {
self.chain.unwrap_or_else(|| self.offer.implied_chain())
}

Expand Down
72 changes: 60 additions & 12 deletions lightning/src/offers/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use bitcoin::hashes::{Hash, HashEngine, sha256};
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
use bitcoin::secp256k1::schnorr::Signature;
use crate::io;
use crate::util::ser::{BigSize, Readable};
use crate::util::ser::{BigSize, Readable, Writeable, Writer};

use crate::prelude::*;

Expand Down Expand Up @@ -75,22 +75,21 @@ fn message_digest(tag: &str, bytes: &[u8]) -> Message {
/// Computes a merkle root hash for the given data, which must be a well-formed TLV stream
/// containing at least one TLV record.
fn root_hash(data: &[u8]) -> sha256::Hash {
let mut tlv_stream = TlvStream::new(&data[..]).peekable();
let nonce_tag = tagged_hash_engine(sha256::Hash::from_engine({
let first_tlv_record = TlvStream::new(&data[..]).next().unwrap();
let mut engine = sha256::Hash::engine();
engine.input("LnNonce".as_bytes());
engine.input(tlv_stream.peek().unwrap().record_bytes);
engine.input(first_tlv_record.record_bytes);
engine
}));
let leaf_tag = tagged_hash_engine(sha256::Hash::hash("LnLeaf".as_bytes()));
let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes()));

let mut leaves = Vec::new();
for record in tlv_stream {
if !SIGNATURE_TYPES.contains(&record.r#type) {
leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record));
leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes));
}
let tlv_stream = TlvStream::new(&data[..]);
for record in tlv_stream.skip_signatures() {
leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record.record_bytes));
leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes));
}

// Calculate the merkle root hash in place.
Expand Down Expand Up @@ -154,6 +153,10 @@ impl<'a> TlvStream<'a> {
data: io::Cursor::new(data),
}
}

fn skip_signatures(self) -> core::iter::Filter<TlvStream<'a>, fn(&TlvRecord) -> bool> {
self.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type))
}
}

/// A slice into a [`TlvStream`] for a record.
Expand All @@ -164,10 +167,6 @@ struct TlvRecord<'a> {
record_bytes: &'a [u8],
}

impl AsRef<[u8]> for TlvRecord<'_> {
fn as_ref(&self) -> &[u8] { &self.record_bytes }
}

impl<'a> Iterator for TlvStream<'a> {
type Item = TlvRecord<'a>;

Expand Down Expand Up @@ -195,14 +194,33 @@ impl<'a> Iterator for TlvStream<'a> {
}
}

/// Encoding for a pre-serialized TLV stream that excludes any signature TLV records.
///
/// Panics if the wrapped bytes are not a well-formed TLV stream.
pub(super) struct WithoutSignatures<'a>(pub &'a Vec<u8>);

impl<'a> Writeable for WithoutSignatures<'a> {
#[inline]
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
let tlv_stream = TlvStream::new(&self.0[..]);
for record in tlv_stream.skip_signatures() {
writer.write_all(record.record_bytes)?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::{TlvStream, WithoutSignatures};

use bitcoin::hashes::{Hash, sha256};
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
use core::convert::Infallible;
use crate::offers::offer::{Amount, OfferBuilder};
use crate::offers::invoice_request::InvoiceRequest;
use crate::offers::parse::Bech32Encode;
use crate::util::ser::Writeable;

#[test]
fn calculates_merkle_root_hash() {
Expand Down Expand Up @@ -254,6 +272,36 @@ mod tests {
);
}

#[test]
fn skips_encoding_signature_tlv_records() {
let secp_ctx = Secp256k1::new();
let recipient_pubkey = {
let secret_key = SecretKey::from_slice(&[41; 32]).unwrap();
KeyPair::from_secret_key(&secp_ctx, &secret_key).public_key()
};
let payer_keys = {
let secret_key = SecretKey::from_slice(&[42; 32]).unwrap();
KeyPair::from_secret_key(&secp_ctx, &secret_key)
};

let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey)
.amount_msats(100)
.build_unchecked()
.request_invoice(vec![0; 8], payer_keys.public_key()).unwrap()
.build_unchecked()
.sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &payer_keys)))
.unwrap();

let mut bytes_without_signature = Vec::new();
WithoutSignatures(&invoice_request.bytes).write(&mut bytes_without_signature).unwrap();

assert_ne!(bytes_without_signature, invoice_request.bytes);
assert_eq!(
TlvStream::new(&bytes_without_signature).count(),
TlvStream::new(&invoice_request.bytes).count() - 1,
);
}

impl AsRef<[u8]> for InvoiceRequest {
fn as_ref(&self) -> &[u8] {
&self.bytes
Expand Down
1 change: 1 addition & 0 deletions lightning/src/offers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//!
//! Offers are a flexible protocol for Lightning payments.

pub mod invoice;
pub mod invoice_request;
mod merkle;
pub mod offer;
Expand Down
31 changes: 21 additions & 10 deletions lightning/src/offers/offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ impl OfferBuilder {
/// An `Offer` is a potentially long-lived proposal for payment of a good or service.
///
/// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a
/// customer may request an `Invoice` for a specific quantity and using an amount sufficient to
/// customer may request an [`Invoice`] for a specific quantity and using an amount sufficient to
/// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`].
///
/// Offers may be denominated in currency other than bitcoin but are ultimately paid using the
Expand All @@ -241,6 +241,7 @@ impl OfferBuilder {
/// Through the use of [`BlindedPath`]s, offers provide recipient privacy.
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
/// [`Invoice`]: crate::offers::invoice::Invoice
#[derive(Clone, Debug)]
pub struct Offer {
// The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
Expand All @@ -249,9 +250,10 @@ pub struct Offer {
pub(super) contents: OfferContents,
}

/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`.
/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an [`Invoice`].
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
/// [`Invoice`]: crate::offers::invoice::Invoice
#[derive(Clone, Debug)]
pub(super) struct OfferContents {
chains: Option<Vec<ChainHash>>,
Expand Down Expand Up @@ -319,13 +321,7 @@ impl Offer {
/// Whether the offer has expired.
#[cfg(feature = "std")]
pub fn is_expired(&self) -> bool {
match self.absolute_expiry() {
Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
Ok(elapsed) => elapsed > seconds_from_epoch,
Err(_) => false,
},
None => false,
}
self.contents.is_expired()
}

/// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be
Expand Down Expand Up @@ -359,7 +355,7 @@ impl Offer {

/// The public key used by the recipient to sign invoices.
pub fn signing_pubkey(&self) -> PublicKey {
self.contents.signing_pubkey
self.contents.signing_pubkey()
}

/// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which
Expand Down Expand Up @@ -410,6 +406,17 @@ impl OfferContents {
self.chains().contains(&chain)
}

#[cfg(feature = "std")]
pub(super) fn is_expired(&self) -> bool {
match self.absolute_expiry {
Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
Ok(elapsed) => elapsed > seconds_from_epoch,
Err(_) => false,
},
None => false,
}
}

pub fn amount(&self) -> Option<&Amount> {
self.amount.as_ref()
}
Expand Down Expand Up @@ -473,6 +480,10 @@ impl OfferContents {
}
}

pub(super) fn signing_pubkey(&self) -> PublicKey {
self.signing_pubkey
}

pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
let (currency, amount) = match &self.amount {
None => (None, None),
Expand Down
10 changes: 10 additions & 0 deletions lightning/src/offers/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ pub enum SemanticError {
MissingDescription,
/// A signing pubkey was not provided.
MissingSigningPubkey,
/// A signing pubkey was provided but a different one was expected.
InvalidSigningPubkey,
/// A signing pubkey was provided but was not expected.
UnexpectedSigningPubkey,
/// A quantity was expected but was missing.
Expand All @@ -159,6 +161,14 @@ pub enum SemanticError {
MissingPayerMetadata,
/// A payer id was expected but was missing.
MissingPayerId,
/// Blinded paths were expected but were missing.
MissingPaths,
/// The blinded payinfo given does not match the number of blinded path hops.
InvalidPayInfo,
/// An invoice creation time was expected but was missing.
MissingCreationTime,
/// An invoice payment hash was expected but was missing.
MissingPaymentHash,
/// A signature was expected but was missing.
MissingSignature,
}
Expand Down
Loading