Skip to content

Commit e0a0add

Browse files
authored
Merge pull request #1926 from jkczyz/2022-12-invoice
BOLT 12 `invoice` encoding and building
2 parents 153b048 + 15f1295 commit e0a0add

File tree

10 files changed

+1814
-43
lines changed

10 files changed

+1814
-43
lines changed

lightning/src/offers/invoice.rs

+1,565
Large diffs are not rendered by default.

lightning/src/offers/invoice_request.rs

+48-6
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
//!
1212
//! An [`InvoiceRequest`] can be built from a parsed [`Offer`] as an "offer to be paid". It is
1313
//! typically constructed by a customer and sent to the merchant who had published the corresponding
14-
//! offer. The recipient of the request responds with an `Invoice`.
14+
//! offer. The recipient of the request responds with an [`Invoice`].
1515
//!
1616
//! For an "offer for money" (e.g., refund, ATM withdrawal), where an offer doesn't exist as a
1717
//! precursor, see [`Refund`].
1818
//!
19+
//! [`Invoice`]: crate::offers::invoice::Invoice
1920
//! [`Refund`]: crate::offers::refund::Refund
2021
//!
2122
//! ```ignore
@@ -57,12 +58,15 @@ use bitcoin::secp256k1::{Message, PublicKey};
5758
use bitcoin::secp256k1::schnorr::Signature;
5859
use core::convert::TryFrom;
5960
use crate::io;
61+
use crate::ln::PaymentHash;
6062
use crate::ln::features::InvoiceRequestFeatures;
6163
use crate::ln::msgs::DecodeError;
64+
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
6265
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self};
6366
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
6467
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
6568
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
69+
use crate::onion_message::BlindedPath;
6670
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
6771
use crate::util::string::PrintableString;
6872

@@ -239,24 +243,27 @@ impl<'a> UnsignedInvoiceRequest<'a> {
239243
}
240244
}
241245

242-
/// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`].
246+
/// An `InvoiceRequest` is a request for an [`Invoice`] formulated from an [`Offer`].
243247
///
244248
/// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request
245249
/// specifies these such that its recipient can send an invoice for payment.
246250
///
251+
/// [`Invoice`]: crate::offers::invoice::Invoice
247252
/// [`Offer`]: crate::offers::offer::Offer
248253
#[derive(Clone, Debug)]
249254
pub struct InvoiceRequest {
250255
pub(super) bytes: Vec<u8>,
251-
contents: InvoiceRequestContents,
256+
pub(super) contents: InvoiceRequestContents,
252257
signature: Signature,
253258
}
254259

255-
/// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`.
260+
/// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`].
261+
///
262+
/// [`Invoice`]: crate::offers::invoice::Invoice
256263
#[derive(Clone, Debug)]
257264
pub(super) struct InvoiceRequestContents {
258265
payer: PayerContents,
259-
offer: OfferContents,
266+
pub(super) offer: OfferContents,
260267
chain: Option<ChainHash>,
261268
amount_msats: Option<u64>,
262269
features: InvoiceRequestFeatures,
@@ -315,6 +322,41 @@ impl InvoiceRequest {
315322
self.signature
316323
}
317324

325+
/// Creates an [`Invoice`] for the request with the given required fields.
326+
///
327+
/// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after
328+
/// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter
329+
/// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`]
330+
/// is not available.
331+
///
332+
/// The caller is expected to remember the preimage of `payment_hash` in order to claim a payment
333+
/// for the invoice.
334+
///
335+
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
336+
/// must contain one or more elements.
337+
///
338+
/// Errors if the request contains unknown required features.
339+
///
340+
/// [`Duration`]: core::time::Duration
341+
/// [`Invoice`]: crate::offers::invoice::Invoice
342+
/// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at
343+
pub fn respond_with(
344+
&self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
345+
#[cfg(any(test, not(feature = "std")))]
346+
created_at: core::time::Duration
347+
) -> Result<InvoiceBuilder, SemanticError> {
348+
if self.features().requires_unknown_bits() {
349+
return Err(SemanticError::UnknownRequiredFeatures);
350+
}
351+
352+
#[cfg(all(not(test), feature = "std"))]
353+
let created_at = std::time::SystemTime::now()
354+
.duration_since(std::time::SystemTime::UNIX_EPOCH)
355+
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
356+
357+
InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash)
358+
}
359+
318360
#[cfg(test)]
319361
fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef {
320362
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) =
@@ -327,7 +369,7 @@ impl InvoiceRequest {
327369
}
328370

329371
impl InvoiceRequestContents {
330-
fn chain(&self) -> ChainHash {
372+
pub(super) fn chain(&self) -> ChainHash {
331373
self.chain.unwrap_or_else(|| self.offer.implied_chain())
332374
}
333375

lightning/src/offers/merkle.rs

+60-12
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use bitcoin::hashes::{Hash, HashEngine, sha256};
1313
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
1414
use bitcoin::secp256k1::schnorr::Signature;
1515
use crate::io;
16-
use crate::util::ser::{BigSize, Readable};
16+
use crate::util::ser::{BigSize, Readable, Writeable, Writer};
1717

1818
use crate::prelude::*;
1919

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

8888
let mut leaves = Vec::new();
89-
for record in tlv_stream {
90-
if !SIGNATURE_TYPES.contains(&record.r#type) {
91-
leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record));
92-
leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes));
93-
}
89+
let tlv_stream = TlvStream::new(&data[..]);
90+
for record in tlv_stream.skip_signatures() {
91+
leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record.record_bytes));
92+
leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes));
9493
}
9594

9695
// Calculate the merkle root hash in place.
@@ -154,6 +153,10 @@ impl<'a> TlvStream<'a> {
154153
data: io::Cursor::new(data),
155154
}
156155
}
156+
157+
fn skip_signatures(self) -> core::iter::Filter<TlvStream<'a>, fn(&TlvRecord) -> bool> {
158+
self.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type))
159+
}
157160
}
158161

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

167-
impl AsRef<[u8]> for TlvRecord<'_> {
168-
fn as_ref(&self) -> &[u8] { &self.record_bytes }
169-
}
170-
171170
impl<'a> Iterator for TlvStream<'a> {
172171
type Item = TlvRecord<'a>;
173172

@@ -195,14 +194,33 @@ impl<'a> Iterator for TlvStream<'a> {
195194
}
196195
}
197196

197+
/// Encoding for a pre-serialized TLV stream that excludes any signature TLV records.
198+
///
199+
/// Panics if the wrapped bytes are not a well-formed TLV stream.
200+
pub(super) struct WithoutSignatures<'a>(pub &'a Vec<u8>);
201+
202+
impl<'a> Writeable for WithoutSignatures<'a> {
203+
#[inline]
204+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
205+
let tlv_stream = TlvStream::new(&self.0[..]);
206+
for record in tlv_stream.skip_signatures() {
207+
writer.write_all(record.record_bytes)?;
208+
}
209+
Ok(())
210+
}
211+
}
212+
198213
#[cfg(test)]
199214
mod tests {
215+
use super::{TlvStream, WithoutSignatures};
216+
200217
use bitcoin::hashes::{Hash, sha256};
201218
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
202219
use core::convert::Infallible;
203220
use crate::offers::offer::{Amount, OfferBuilder};
204221
use crate::offers::invoice_request::InvoiceRequest;
205222
use crate::offers::parse::Bech32Encode;
223+
use crate::util::ser::Writeable;
206224

207225
#[test]
208226
fn calculates_merkle_root_hash() {
@@ -254,6 +272,36 @@ mod tests {
254272
);
255273
}
256274

275+
#[test]
276+
fn skips_encoding_signature_tlv_records() {
277+
let secp_ctx = Secp256k1::new();
278+
let recipient_pubkey = {
279+
let secret_key = SecretKey::from_slice(&[41; 32]).unwrap();
280+
KeyPair::from_secret_key(&secp_ctx, &secret_key).public_key()
281+
};
282+
let payer_keys = {
283+
let secret_key = SecretKey::from_slice(&[42; 32]).unwrap();
284+
KeyPair::from_secret_key(&secp_ctx, &secret_key)
285+
};
286+
287+
let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey)
288+
.amount_msats(100)
289+
.build_unchecked()
290+
.request_invoice(vec![0; 8], payer_keys.public_key()).unwrap()
291+
.build_unchecked()
292+
.sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &payer_keys)))
293+
.unwrap();
294+
295+
let mut bytes_without_signature = Vec::new();
296+
WithoutSignatures(&invoice_request.bytes).write(&mut bytes_without_signature).unwrap();
297+
298+
assert_ne!(bytes_without_signature, invoice_request.bytes);
299+
assert_eq!(
300+
TlvStream::new(&bytes_without_signature).count(),
301+
TlvStream::new(&invoice_request.bytes).count() - 1,
302+
);
303+
}
304+
257305
impl AsRef<[u8]> for InvoiceRequest {
258306
fn as_ref(&self) -> &[u8] {
259307
&self.bytes

lightning/src/offers/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//!
1313
//! Offers are a flexible protocol for Lightning payments.
1414
15+
pub mod invoice;
1516
pub mod invoice_request;
1617
mod merkle;
1718
pub mod offer;

lightning/src/offers/offer.rs

+21-10
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ impl OfferBuilder {
232232
/// An `Offer` is a potentially long-lived proposal for payment of a good or service.
233233
///
234234
/// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a
235-
/// customer may request an `Invoice` for a specific quantity and using an amount sufficient to
235+
/// customer may request an [`Invoice`] for a specific quantity and using an amount sufficient to
236236
/// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`].
237237
///
238238
/// Offers may be denominated in currency other than bitcoin but are ultimately paid using the
@@ -241,6 +241,7 @@ impl OfferBuilder {
241241
/// Through the use of [`BlindedPath`]s, offers provide recipient privacy.
242242
///
243243
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
244+
/// [`Invoice`]: crate::offers::invoice::Invoice
244245
#[derive(Clone, Debug)]
245246
pub struct Offer {
246247
// The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
@@ -249,9 +250,10 @@ pub struct Offer {
249250
pub(super) contents: OfferContents,
250251
}
251252

252-
/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`.
253+
/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an [`Invoice`].
253254
///
254255
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
256+
/// [`Invoice`]: crate::offers::invoice::Invoice
255257
#[derive(Clone, Debug)]
256258
pub(super) struct OfferContents {
257259
chains: Option<Vec<ChainHash>>,
@@ -319,13 +321,7 @@ impl Offer {
319321
/// Whether the offer has expired.
320322
#[cfg(feature = "std")]
321323
pub fn is_expired(&self) -> bool {
322-
match self.absolute_expiry() {
323-
Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
324-
Ok(elapsed) => elapsed > seconds_from_epoch,
325-
Err(_) => false,
326-
},
327-
None => false,
328-
}
324+
self.contents.is_expired()
329325
}
330326

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

360356
/// The public key used by the recipient to sign invoices.
361357
pub fn signing_pubkey(&self) -> PublicKey {
362-
self.contents.signing_pubkey
358+
self.contents.signing_pubkey()
363359
}
364360

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

409+
#[cfg(feature = "std")]
410+
pub(super) fn is_expired(&self) -> bool {
411+
match self.absolute_expiry {
412+
Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
413+
Ok(elapsed) => elapsed > seconds_from_epoch,
414+
Err(_) => false,
415+
},
416+
None => false,
417+
}
418+
}
419+
413420
pub fn amount(&self) -> Option<&Amount> {
414421
self.amount.as_ref()
415422
}
@@ -473,6 +480,10 @@ impl OfferContents {
473480
}
474481
}
475482

483+
pub(super) fn signing_pubkey(&self) -> PublicKey {
484+
self.signing_pubkey
485+
}
486+
476487
pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
477488
let (currency, amount) = match &self.amount {
478489
None => (None, None),

lightning/src/offers/parse.rs

+10
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ pub enum SemanticError {
147147
MissingDescription,
148148
/// A signing pubkey was not provided.
149149
MissingSigningPubkey,
150+
/// A signing pubkey was provided but a different one was expected.
151+
InvalidSigningPubkey,
150152
/// A signing pubkey was provided but was not expected.
151153
UnexpectedSigningPubkey,
152154
/// A quantity was expected but was missing.
@@ -159,6 +161,14 @@ pub enum SemanticError {
159161
MissingPayerMetadata,
160162
/// A payer id was expected but was missing.
161163
MissingPayerId,
164+
/// Blinded paths were expected but were missing.
165+
MissingPaths,
166+
/// The blinded payinfo given does not match the number of blinded path hops.
167+
InvalidPayInfo,
168+
/// An invoice creation time was expected but was missing.
169+
MissingCreationTime,
170+
/// An invoice payment hash was expected but was missing.
171+
MissingPaymentHash,
162172
/// A signature was expected but was missing.
163173
MissingSignature,
164174
}

0 commit comments

Comments
 (0)