X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Foffers%2Finvoice.rs;h=9d83ce899b153d836288facf83b82e7515b1dc90;hb=3880e69237d7f6a33db1912176de5a745ea99b41;hp=1ca2c06cefd03ca7400b6ca7b6fcf707992d400d;hpb=bf1147f23f95fa52f555b28e300d9f6851caef62;p=rust-lightning diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 1ca2c06c..9d83ce89 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -16,7 +16,7 @@ //! The payment recipient must include a [`PaymentHash`], so as to reveal the preimage upon payment //! receipt, and one or more [`BlindedPath`]s for the payer to use when sending the payment. //! -//! ```ignore +//! ``` //! extern crate bitcoin; //! extern crate lightning; //! @@ -45,7 +45,12 @@ //! //! // Invoice for the "offer to be paid" flow. //! InvoiceRequest::try_from(bytes)? -//! .respond_with(payment_paths, payment_hash)? +#![cfg_attr(feature = "std", doc = " + .respond_with(payment_paths, payment_hash)? +")] +#![cfg_attr(not(feature = "std"), doc = " + .respond_with_no_std(payment_paths, payment_hash, core::time::Duration::from_secs(0))? +")] //! .relative_expiry(3600) //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) @@ -69,7 +74,12 @@ //! // Invoice for the "offer for money" flow. //! "lnr1qcp4256ypq" //! .parse::()? -//! .respond_with(payment_paths, payment_hash, pubkey)? +#![cfg_attr(feature = "std", doc = " + .respond_with(payment_paths, payment_hash, pubkey)? +")] +#![cfg_attr(not(feature = "std"), doc = " + .respond_with_no_std(payment_paths, payment_hash, pubkey, core::time::Duration::from_secs(0))? +")] //! .relative_expiry(3600) //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) @@ -136,9 +146,10 @@ impl<'a> InvoiceBuilder<'a> { ) -> Result { let amount_msats = match invoice_request.amount_msats() { Some(amount_msats) => amount_msats, - None => match invoice_request.contents.offer.amount() { + None => match invoice_request.contents.inner.offer.amount() { Some(Amount::Bitcoin { amount_msats }) => { - amount_msats * invoice_request.quantity().unwrap_or(1) + amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1)) + .ok_or(SemanticError::InvalidAmount)? }, Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), None => return Err(SemanticError::MissingAmount), @@ -150,7 +161,7 @@ impl<'a> InvoiceBuilder<'a> { fields: InvoiceFields { payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, fallbacks: None, features: Bolt12InvoiceFeatures::empty(), - signing_pubkey: invoice_request.contents.offer.signing_pubkey(), + signing_pubkey: invoice_request.contents.inner.offer.signing_pubkey(), }, }; @@ -257,6 +268,11 @@ pub struct UnsignedInvoice<'a> { } impl<'a> UnsignedInvoice<'a> { + /// The public key corresponding to the key needed to sign the invoice. + pub fn signing_pubkey(&self) -> PublicKey { + self.invoice.fields().signing_pubkey + } + /// Signs the invoice using the given function. pub fn sign(self, sign: F) -> Result> where @@ -297,6 +313,8 @@ impl<'a> UnsignedInvoice<'a> { /// [`Offer`]: crate::offers::offer::Offer /// [`Refund`]: crate::offers::refund::Refund /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Invoice { bytes: Vec, contents: InvoiceContents, @@ -307,6 +325,8 @@ pub struct Invoice { /// /// [`Offer`]: crate::offers::offer::Offer /// [`Refund`]: crate::offers::refund::Refund +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] enum InvoiceContents { /// Contents for an [`Invoice`] corresponding to an [`Offer`]. /// @@ -325,6 +345,7 @@ enum InvoiceContents { } /// Invoice-specific fields for an `invoice` message. +#[derive(Clone, Debug, PartialEq)] struct InvoiceFields { payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, @@ -338,8 +359,10 @@ struct InvoiceFields { impl Invoice { /// Paths to the recipient originating from publicly reachable nodes, including information - /// needed for routing payments across them. Blinded paths provide recipient privacy by - /// obfuscating its node id. + /// needed for routing payments across them. + /// + /// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this + /// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`]. pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] { &self.contents.fields().payment_paths[..] } @@ -438,16 +461,21 @@ impl Invoice { &self.contents.fields().features } - /// The public key used to sign invoices. + /// The public key corresponding to the key used to sign the invoice. pub fn signing_pubkey(&self) -> PublicKey { self.contents.fields().signing_pubkey } - /// Signature of the invoice using [`Invoice::signing_pubkey`]. + /// Signature of the invoice verified using [`Invoice::signing_pubkey`]. pub fn signature(&self) -> Signature { self.signature } + /// Hash that was used for signing the invoice. + pub fn signable_hash(&self) -> [u8; 32] { + merkle::message_digest(SIGNATURE_TAG, &self.bytes).as_ref().clone() + } + #[cfg(test)] fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) = @@ -465,7 +493,8 @@ impl InvoiceContents { #[cfg(feature = "std")] fn is_offer_or_refund_expired(&self) -> bool { match self { - InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.offer.is_expired(), + InvoiceContents::ForOffer { invoice_request, .. } => + invoice_request.inner.offer.is_expired(), InvoiceContents::ForRefund { refund, .. } => refund.is_expired(), } } @@ -529,6 +558,12 @@ impl Writeable for Invoice { } } +impl Writeable for InvoiceContents { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + self.as_tlv_stream().write(writer) + } +} + impl TryFrom> for Invoice { type Error = ParseError; @@ -563,12 +598,30 @@ type BlindedPayInfoIter<'a> = core::iter::Map< /// Information needed to route a payment across a [`BlindedPath`]. #[derive(Clone, Debug, PartialEq)] pub struct BlindedPayInfo { - fee_base_msat: u32, - fee_proportional_millionths: u32, - cltv_expiry_delta: u16, - htlc_minimum_msat: u64, - htlc_maximum_msat: u64, - features: BlindedHopFeatures, + /// Base fee charged (in millisatoshi) for the entire blinded path. + pub fee_base_msat: u32, + + /// Liquidity fee charged (in millionths of the amount transferred) for the entire blinded path + /// (i.e., 10,000 is 1%). + pub fee_proportional_millionths: u32, + + /// Number of blocks subtracted from an incoming HTLC's `cltv_expiry` for the entire blinded + /// path. + pub cltv_expiry_delta: u16, + + /// The minimum HTLC value (in millisatoshi) that is acceptable to all channel peers on the + /// blinded path from the introduction node to the recipient, accounting for any fees, i.e., as + /// seen by the recipient. + pub htlc_minimum_msat: u64, + + /// The maximum HTLC value (in millisatoshi) that is acceptable to all channel peers on the + /// blinded path from the introduction node to the recipient, accounting for any fees, i.e., as + /// seen by the recipient. + pub htlc_maximum_msat: u64, + + /// Features set in `encrypted_data_tlv` for the `encrypted_recipient_data` TLV record in an + /// onion payload. + pub features: BlindedHopFeatures, } impl_writeable!(BlindedPayInfo, { @@ -581,7 +634,7 @@ impl_writeable!(BlindedPayInfo, { }); /// Wire representation for an on-chain fallback address. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct FallbackAddress { version: u8, program: Vec, @@ -727,117 +780,41 @@ impl TryFrom for InvoiceContents { #[cfg(test)] mod tests { - use super::{DEFAULT_RELATIVE_EXPIRY, BlindedPayInfo, FallbackAddress, Invoice, InvoiceTlvStreamRef, SIGNATURE_TAG}; + use super::{DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, Invoice, InvoiceTlvStreamRef, SIGNATURE_TAG}; use bitcoin::blockdata::script::Script; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; - use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey, XOnlyPublicKey, self}; - use bitcoin::secp256k1::schnorr::Signature; - use bitcoin::util::address::{Address, WitnessVersion}; + use bitcoin::secp256k1::{Message, Secp256k1, XOnlyPublicKey, self}; + use bitcoin::util::address::{Address, Payload, WitnessVersion}; use bitcoin::util::schnorr::TweakedPublicKey; - use core::convert::{Infallible, TryFrom}; + use core::convert::TryFrom; use core::time::Duration; - use crate::ln::PaymentHash; - use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; + use crate::ln::msgs::DecodeError; + use crate::ln::features::Bolt12InvoiceFeatures; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; - use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef}; - use crate::offers::parse::SemanticError; + use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity}; + use crate::offers::parse::{ParseError, SemanticError}; use crate::offers::payer::PayerTlvStreamRef; use crate::offers::refund::RefundBuilder; - use crate::onion_message::{BlindedHop, BlindedPath}; - use crate::util::ser::{Iterable, Writeable}; - - fn payer_keys() -> KeyPair { - let secp_ctx = Secp256k1::new(); - KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()) - } - - fn payer_sign(digest: &Message) -> Result { - let secp_ctx = Secp256k1::new(); - let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); - Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) - } - - fn payer_pubkey() -> PublicKey { - payer_keys().public_key() - } - - fn recipient_keys() -> KeyPair { - let secp_ctx = Secp256k1::new(); - KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()) - } - - fn recipient_sign(digest: &Message) -> Result { - let secp_ctx = Secp256k1::new(); - let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()); - Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) - } - - fn recipient_pubkey() -> PublicKey { - recipient_keys().public_key() - } - - fn pubkey(byte: u8) -> PublicKey { - let secp_ctx = Secp256k1::new(); - PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) - } - - fn privkey(byte: u8) -> SecretKey { - SecretKey::from_slice(&[byte; 32]).unwrap() - } - - fn payment_paths() -> Vec<(BlindedPath, BlindedPayInfo)> { - let paths = vec![ - BlindedPath { - introduction_node_id: 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_id: 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 payinfo = vec![ - BlindedPayInfo { - fee_base_msat: 1, - fee_proportional_millionths: 1_000, - cltv_expiry_delta: 42, - htlc_minimum_msat: 100, - htlc_maximum_msat: 1_000_000_000_000, - features: BlindedHopFeatures::empty(), - }, - BlindedPayInfo { - fee_base_msat: 1, - fee_proportional_millionths: 1_000, - cltv_expiry_delta: 42, - htlc_minimum_msat: 100, - htlc_maximum_msat: 1_000_000_000_000, - features: BlindedHopFeatures::empty(), - }, - ]; - - paths.into_iter().zip(payinfo.into_iter()).collect() - } + use crate::offers::test_utils::*; + use crate::util::ser::{BigSize, Iterable, Writeable}; - fn payment_hash() -> PaymentHash { - PaymentHash([42; 32]) + trait ToBytes { + fn to_bytes(&self) -> Vec; } - fn now() -> Duration { - std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH") + impl<'a> ToBytes for FullInvoiceTlvStreamRef<'a> { + fn to_bytes(&self) -> Vec { + let mut buffer = Vec::new(); + self.0.write(&mut buffer).unwrap(); + self.1.write(&mut buffer).unwrap(); + self.2.write(&mut buffer).unwrap(); + self.3.write(&mut buffer).unwrap(); + self.4.write(&mut buffer).unwrap(); + buffer + } } #[test] @@ -851,7 +828,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths.clone(), payment_hash, now).unwrap() + .respond_with_no_std(payment_paths.clone(), payment_hash, now).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -875,6 +852,11 @@ mod tests { ).is_ok() ); + let digest = Message::from_slice(&invoice.signable_hash()).unwrap(); + let pubkey = recipient_pubkey().into(); + let secp_ctx = Secp256k1::verification_only(); + assert!(secp_ctx.verify_schnorr(&invoice.signature, &digest, &pubkey).is_ok()); + assert_eq!( invoice.as_tlv_stream(), ( @@ -927,7 +909,8 @@ mod tests { let now = now(); let invoice = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap() - .respond_with(payment_paths.clone(), payment_hash, recipient_pubkey(), now).unwrap() + .respond_with_no_std(payment_paths.clone(), payment_hash, recipient_pubkey(), now) + .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -996,6 +979,42 @@ mod tests { } } + #[cfg(feature = "std")] + #[test] + fn builds_invoice_from_offer_with_expiration() { + let future_expiry = Duration::from_secs(u64::max_value()); + let past_expiry = Duration::from_secs(0); + + if let Err(e) = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .absolute_expiry(future_expiry) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with(payment_paths(), payment_hash()) + .unwrap() + .build() + { + panic!("error building invoice: {:?}", e); + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .absolute_expiry(past_expiry) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap() + .respond_with(payment_paths(), payment_hash()) + .unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::AlreadyExpired), + } + } + #[cfg(feature = "std")] #[test] fn builds_invoice_from_refund_with_expiration() { @@ -1005,7 +1024,8 @@ mod tests { if let Err(e) = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(future_expiry) .build().unwrap() - .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap() + .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) + .unwrap() .build() { panic!("error building invoice: {:?}", e); @@ -1014,7 +1034,8 @@ mod tests { match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(past_expiry) .build().unwrap() - .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap() + .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) + .unwrap() .build() { Ok(_) => panic!("expected error"), @@ -1033,7 +1054,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now).unwrap() .relative_expiry(one_hour.as_secs() as u32) .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1049,7 +1070,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now - one_hour).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now - one_hour).unwrap() .relative_expiry(one_hour.as_secs() as u32 - 1) .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1069,7 +1090,7 @@ mod tests { .amount_msats(1001).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); @@ -1077,6 +1098,38 @@ mod tests { assert_eq!(tlv_stream.amount, Some(1001)); } + #[test] + fn builds_invoice_with_quantity_from_request() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(2).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); + assert_eq!(invoice.amount_msats(), 2000); + assert_eq!(tlv_stream.amount, Some(2000)); + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(u64::max_value()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InvalidAmount), + } + } + #[test] fn builds_invoice_with_fallback_address() { let script = Script::new(); @@ -1090,7 +1143,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) .fallback_v1_p2tr_tweaked(&tweaked_pubkey) @@ -1135,7 +1188,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .allow_mpp() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1152,7 +1205,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(|_| Err(())) { @@ -1166,7 +1219,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(payer_sign) { @@ -1174,4 +1227,369 @@ mod tests { Err(e) => assert_eq!(e, SignError::Verification(secp256k1::Error::InvalidSignature)), } } + + #[test] + fn parses_invoice_with_payment_paths() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.paths = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaths)), + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.blindedpay = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidPayInfo)), + } + + let empty_payment_paths = vec![]; + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.paths = Some(Iterable(empty_payment_paths.iter().map(|(path, _)| path))); + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaths)), + } + + let mut payment_paths = payment_paths(); + payment_paths.pop(); + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.blindedpay = Some(Iterable(payment_paths.iter().map(|(_, payinfo)| payinfo))); + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidPayInfo)), + } + } + + #[test] + fn parses_invoice_with_created_at() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.created_at = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingCreationTime)); + }, + } + } + + #[test] + fn parses_invoice_with_relative_expiry() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .relative_expiry(3600) + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(invoice) => assert_eq!(invoice.relative_expiry(), Duration::from_secs(3600)), + Err(e) => panic!("error parsing invoice: {:?}", e), + } + } + + #[test] + fn parses_invoice_with_payment_hash() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.payment_hash = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaymentHash)); + }, + } + } + + #[test] + fn parses_invoice_with_amount() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.amount = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingAmount)), + } + } + + #[test] + fn parses_invoice_with_allow_mpp() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .allow_mpp() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(invoice) => { + let mut features = Bolt12InvoiceFeatures::empty(); + features.set_basic_mpp_optional(); + assert_eq!(invoice.features(), &features); + }, + Err(e) => panic!("error parsing invoice: {:?}", e), + } + } + + #[test] + fn parses_invoice_with_fallback_address() { + let script = Script::new(); + let pubkey = bitcoin::util::key::PublicKey::new(recipient_pubkey()); + let x_only_pubkey = XOnlyPublicKey::from_keypair(&recipient_keys()).0; + let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey); + + let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap(); + let invoice_request = offer + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let mut unsigned_invoice = invoice_request + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .fallback_v0_p2wsh(&script.wscript_hash()) + .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) + .fallback_v1_p2tr_tweaked(&tweaked_pubkey) + .build().unwrap(); + + // Only standard addresses will be included. + let fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap(); + // Non-standard addresses + fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 41] }); + fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 1] }); + fallbacks.push(FallbackAddress { version: 17, program: vec![0u8; 40] }); + // Standard address + fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 33] }); + fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 40] }); + + let invoice = unsigned_invoice.sign(recipient_sign).unwrap(); + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(invoice) => { + assert_eq!( + invoice.fallbacks(), + vec![ + Address::p2wsh(&script, Network::Bitcoin), + Address::p2wpkh(&pubkey, Network::Bitcoin).unwrap(), + Address::p2tr_tweaked(tweaked_pubkey, Network::Bitcoin), + Address { + payload: Payload::WitnessProgram { + version: WitnessVersion::V1, + program: vec![0u8; 33], + }, + network: Network::Bitcoin, + }, + Address { + payload: Payload::WitnessProgram { + version: WitnessVersion::V2, + program: vec![0u8; 40], + }, + network: Network::Bitcoin, + }, + ], + ); + }, + Err(e) => panic!("error parsing invoice: {:?}", e), + } + } + + #[test] + fn parses_invoice_with_node_id() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.node_id = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSigningPubkey)); + }, + } + + let invalid_pubkey = payer_pubkey(); + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.node_id = Some(&invalid_pubkey); + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidSigningPubkey)); + }, + } + } + + #[test] + fn fails_parsing_invoice_without_signature() { + let mut buffer = Vec::new(); + OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .invoice + .write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSignature)), + } + } + + #[test] + fn fails_parsing_invoice_with_invalid_signature() { + let mut invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + let last_signature_byte = invoice.bytes.last_mut().unwrap(); + *last_signature_byte = last_signature_byte.wrapping_add(1); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSignature(secp256k1::Error::InvalidSignature)); + }, + } + } + + #[test] + fn fails_parsing_invoice_with_extra_tlv_records() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + BigSize(1002).write(&mut encoded_invoice).unwrap(); + BigSize(32).write(&mut encoded_invoice).unwrap(); + [42u8; 32].write(&mut encoded_invoice).unwrap(); + + match Invoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)), + } + } }