X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning-invoice%2Fsrc%2Flib.rs;h=9ee8e49b7dcb8d2a2b307f12087b815659764dc0;hb=20e776bc8e2ebe7af3df99227c1f1a5467f42fc3;hp=3d75d4d65dc057c148d11886ae03a702da9205ea;hpb=7af9976261436fe2357a187c2aaa6792fb0506c6;p=rust-lightning diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 3d75d4d6..9ee8e49b 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -3,6 +3,7 @@ #![deny(non_camel_case_types)] #![deny(non_snake_case)] #![deny(unused_mut)] +#![deny(broken_intra_doc_links)] #![cfg_attr(feature = "strict", deny(warnings))] @@ -14,6 +15,7 @@ //! * For parsing use `str::parse::(&self)` (see the docs of `impl FromStr for Invoice`) //! * For constructing invoices use the `InvoiceBuilder` //! * For serializing invoices use the `Display`/`ToString` traits +pub mod utils; extern crate bech32; extern crate bitcoin_hashes; @@ -24,6 +26,7 @@ extern crate secp256k1; use bech32::u5; use bitcoin_hashes::Hash; use bitcoin_hashes::sha256; +use lightning::ln::PaymentSecret; use lightning::ln::features::InvoiceFeatures; #[cfg(any(doc, test))] use lightning::routing::network_graph::RoutingFees; @@ -32,12 +35,12 @@ use lightning::routing::router::RouteHintHop; use secp256k1::key::PublicKey; use secp256k1::{Message, Secp256k1}; use secp256k1::recovery::RecoverableSignature; -use std::ops::Deref; +use std::fmt::{Display, Formatter, self}; use std::iter::FilterMap; +use std::ops::Deref; use std::slice::Iter; use std::time::{SystemTime, Duration, UNIX_EPOCH}; -use std::fmt::{Display, Formatter, self}; mod de; mod ser; @@ -54,6 +57,16 @@ const SYSTEM_TIME_MAX_UNIX_TIMESTAMP: u64 = std::i32::MAX as u64; /// it should be rather low as long as we still have to support 32bit time representations const MAX_EXPIRY_TIME: u64 = 60 * 60 * 24 * 356; +/// Default expiry time as defined by [BOLT 11]. +/// +/// [BOLT 11]: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md +const DEFAULT_EXPIRY_TIME: u64 = 3600; + +/// Default minimum final CLTV expiry as defined by [BOLT 11]. +/// +/// [BOLT 11]: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md +const DEFAULT_MIN_FINAL_CLTV_EXPIRY: u64 = 18; + /// This function is used as a static assert for the size of `SystemTime`. If the crate fails to /// compile due to it this indicates that your system uses unexpected bounds for `SystemTime`. You /// can remove this functions and run the test `test_system_time_bounds_assumptions`. In any case, @@ -136,6 +149,7 @@ pub fn check_platform() { /// .description("Coins pls!".into()) /// .payment_hash(payment_hash) /// .current_timestamp() +/// .min_final_cltv_expiry(144) /// .build_signed(|hash| { /// Secp256k1::new().sign_recoverable(hash, &private_key) /// }) @@ -151,8 +165,10 @@ pub fn check_platform() { /// * `D`: exactly one `Description` or `DescriptionHash` /// * `H`: exactly one `PaymentHash` /// * `T`: the timestamp is set +/// +/// (C-not exported) as we likely need to manually select one set of boolean type parameters. #[derive(Eq, PartialEq, Debug, Clone)] -pub struct InvoiceBuilder { +pub struct InvoiceBuilder { currency: Currency, amount: Option, si_prefix: Option, @@ -163,6 +179,8 @@ pub struct InvoiceBuilder { phantom_d: std::marker::PhantomData, phantom_h: std::marker::PhantomData, phantom_t: std::marker::PhantomData, + phantom_c: std::marker::PhantomData, + phantom_s: std::marker::PhantomData, } /// Represents a syntactically and semantically correct lightning BOLT11 invoice. @@ -178,6 +196,9 @@ pub struct Invoice { /// Represents the description of an invoice which has to be either a directly included string or /// a hash of a description provided out of band. +/// +/// (C-not exported) As we don't have a good way to map the reference lifetimes making this +/// practically impossible to use safely in languages like C. #[derive(Eq, PartialEq, Debug, Clone)] pub enum InvoiceDescription<'f> { /// Reference to the directly supplied description in the invoice @@ -207,7 +228,7 @@ pub struct SignedRawInvoice { hash: [u8; 32], /// signature of the payment request - signature: Signature, + signature: InvoiceSignature, } /// Represents an syntactically correct Invoice for a payment on the lightning network, @@ -225,6 +246,8 @@ pub struct RawInvoice { } /// Data of the `RawInvoice` that is encoded in the human readable part +/// +/// (C-not exported) As we don't yet support Option #[derive(Eq, PartialEq, Debug, Clone)] pub struct RawHrp { /// The currency deferred from the 3rd and 4th character of the bech32 transaction @@ -283,6 +306,9 @@ impl SiPrefix { /// Returns all enum variants of `SiPrefix` sorted in descending order of their associated /// multiplier. + /// + /// (C-not exported) As we don't yet support a slice of enums, and also because this function + /// isn't the most critical to expose. pub fn values_desc() -> &'static [SiPrefix] { use SiPrefix::*; static VALUES: [SiPrefix; 4] = [Milli, Micro, Nano, Pico]; @@ -348,10 +374,6 @@ pub struct Description(String); #[derive(Eq, PartialEq, Debug, Clone)] pub struct PayeePubKey(pub PublicKey); -/// 256-bit payment secret -#[derive(Eq, PartialEq, Debug, Clone)] -pub struct PaymentSecret(pub [u8; 32]); - /// Positive duration that defines when (relatively to the timestamp) in the future the invoice /// expires /// @@ -381,7 +403,7 @@ pub enum Fallback { /// Recoverable signature #[derive(Eq, PartialEq, Debug, Clone)] -pub struct Signature(pub RecoverableSignature); +pub struct InvoiceSignature(pub RecoverableSignature); /// Private routing information /// @@ -406,7 +428,7 @@ pub mod constants { pub const TAG_FEATURES: u8 = 5; } -impl InvoiceBuilder { +impl InvoiceBuilder { /// Construct new, empty `InvoiceBuilder`. All necessary fields have to be filled first before /// `InvoiceBuilder::build(self)` becomes available. pub fn new(currrency: Currency) -> Self { @@ -421,14 +443,16 @@ impl InvoiceBuilder { phantom_d: std::marker::PhantomData, phantom_h: std::marker::PhantomData, phantom_t: std::marker::PhantomData, + phantom_c: std::marker::PhantomData, + phantom_s: std::marker::PhantomData, } } } -impl InvoiceBuilder { +impl InvoiceBuilder { /// Helper function to set the completeness flags. - fn set_flags(self) -> InvoiceBuilder { - InvoiceBuilder:: { + fn set_flags(self) -> InvoiceBuilder { + InvoiceBuilder:: { currency: self.currency, amount: self.amount, si_prefix: self.si_prefix, @@ -439,6 +463,8 @@ impl InvoiceBuilder { phantom_d: std::marker::PhantomData, phantom_h: std::marker::PhantomData, phantom_t: std::marker::PhantomData, + phantom_c: std::marker::PhantomData, + phantom_s: std::marker::PhantomData, } } @@ -459,12 +485,6 @@ impl InvoiceBuilder { self } - /// Sets the payment secret - pub fn payment_secret(mut self, payment_secret: PaymentSecret) -> Self { - self.tagged_fields.push(TaggedField::PaymentSecret(payment_secret)); - self - } - /// Sets the expiry time pub fn expiry_time(mut self, expiry_time: Duration) -> Self { match ExpiryTime::from_duration(expiry_time) { @@ -474,12 +494,6 @@ impl InvoiceBuilder { self } - /// Sets `min_final_cltv_expiry`. - pub fn min_final_cltv_expiry(mut self, min_final_cltv_expiry: u64) -> Self { - self.tagged_fields.push(TaggedField::MinFinalCltvExpiry(MinFinalCltvExpiry(min_final_cltv_expiry))); - self - } - /// Adds a fallback address. pub fn fallback(mut self, fallback: Fallback) -> Self { self.tagged_fields.push(TaggedField::Fallback(fallback)); @@ -494,16 +508,9 @@ impl InvoiceBuilder { } self } - - /// Adds a features field which indicates the set of supported protocol extensions which the - /// origin node supports. - pub fn features(mut self, features: InvoiceFeatures) -> Self { - self.tagged_fields.push(TaggedField::Features(features)); - self - } } -impl InvoiceBuilder { +impl InvoiceBuilder { /// Builds a `RawInvoice` if no `CreationError` occurred while construction any of the fields. pub fn build_raw(self) -> Result { @@ -536,9 +543,9 @@ impl InvoiceBuilder { } } -impl InvoiceBuilder { +impl InvoiceBuilder { /// Set the description. This function is only available if no description (hash) was set. - pub fn description(mut self, description: String) -> InvoiceBuilder { + pub fn description(mut self, description: String) -> InvoiceBuilder { match Description::new(description) { Ok(d) => self.tagged_fields.push(TaggedField::Description(d)), Err(e) => self.error = Some(e), @@ -547,23 +554,23 @@ impl InvoiceBuilder { } /// Set the description hash. This function is only available if no description (hash) was set. - pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder { + pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder { self.tagged_fields.push(TaggedField::DescriptionHash(Sha256(description_hash))); self.set_flags() } } -impl InvoiceBuilder { +impl InvoiceBuilder { /// Set the payment hash. This function is only available if no payment hash was set. - pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder { + pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder { self.tagged_fields.push(TaggedField::PaymentHash(Sha256(hash))); self.set_flags() } } -impl InvoiceBuilder { +impl InvoiceBuilder { /// Sets the timestamp. - pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder { + pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder { match PositiveTimestamp::from_system_time(time) { Ok(t) => self.timestamp = Some(t), Err(e) => self.error = Some(e), @@ -573,14 +580,48 @@ impl InvoiceBuilder { } /// Sets the timestamp to the current UNIX timestamp. - pub fn current_timestamp(mut self) -> InvoiceBuilder { + pub fn current_timestamp(mut self) -> InvoiceBuilder { let now = PositiveTimestamp::from_system_time(SystemTime::now()); self.timestamp = Some(now.expect("for the foreseeable future this shouldn't happen")); self.set_flags() } } -impl InvoiceBuilder { +impl InvoiceBuilder { + /// Sets `min_final_cltv_expiry`. + pub fn min_final_cltv_expiry(mut self, min_final_cltv_expiry: u64) -> InvoiceBuilder { + self.tagged_fields.push(TaggedField::MinFinalCltvExpiry(MinFinalCltvExpiry(min_final_cltv_expiry))); + self.set_flags() + } +} + +impl InvoiceBuilder { + /// Sets the payment secret and relevant features. + pub fn payment_secret(mut self, payment_secret: PaymentSecret) -> InvoiceBuilder { + let features = InvoiceFeatures::empty() + .set_variable_length_onion_required() + .set_payment_secret_required(); + self.tagged_fields.push(TaggedField::PaymentSecret(payment_secret)); + self.tagged_fields.push(TaggedField::Features(features)); + self.set_flags() + } +} + +impl InvoiceBuilder { + /// Sets the `basic_mpp` feature as optional. + pub fn basic_mpp(mut self) -> Self { + self.tagged_fields = self.tagged_fields + .drain(..) + .map(|field| match field { + TaggedField::Features(f) => TaggedField::Features(f.set_basic_mpp_optional()), + _ => field, + }) + .collect(); + self + } +} + +impl InvoiceBuilder { /// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail /// and MUST produce a recoverable signature valid for the given hash and if applicable also for /// the included payee public key. @@ -619,6 +660,7 @@ impl InvoiceBuilder { }; invoice.check_field_counts().expect("should be ensured by type signature of builder"); + invoice.check_feature_bits().expect("should be ensured by type signature of builder"); Ok(invoice) } @@ -630,7 +672,7 @@ impl SignedRawInvoice { /// 1. raw invoice /// 2. hash of the raw invoice /// 3. signature - pub fn into_parts(self) -> (RawInvoice, [u8; 32], Signature) { + pub fn into_parts(self) -> (RawInvoice, [u8; 32], InvoiceSignature) { (self.raw_invoice, self.hash, self.signature) } @@ -644,8 +686,8 @@ impl SignedRawInvoice { &self.hash } - /// Signature for the invoice. - pub fn signature(&self) -> &Signature { + /// InvoiceSignature for the invoice. + pub fn signature(&self) -> &InvoiceSignature { &self.signature } @@ -721,8 +763,8 @@ macro_rules! find_extract { #[allow(missing_docs)] impl RawInvoice { - /// Hash the HRP as bytes and signatureless data part. - fn hash_from_parts(hrp_bytes: &[u8], data_without_signature: &[u5]) -> [u8; 32] { + /// Construct the invoice's HRP and signatureless data into a preimage to be hashed. + pub(crate) fn construct_invoice_preimage(hrp_bytes: &[u8], data_without_signature: &[u5]) -> Vec { use bech32::FromBase32; let mut preimage = Vec::::from(hrp_bytes); @@ -741,7 +783,12 @@ impl RawInvoice { preimage.extend_from_slice(&Vec::::from_base32(&data_part) .expect("No padding error may occur due to appended zero above.")); + preimage + } + /// Hash the HRP as bytes and signatureless data part. + fn hash_from_parts(hrp_bytes: &[u8], data_without_signature: &[u5]) -> [u8; 32] { + let preimage = RawInvoice::construct_invoice_preimage(hrp_bytes, data_without_signature); let mut hash: [u8; 32] = Default::default(); hash.copy_from_slice(&sha256::Hash::hash(&preimage)[..]); hash @@ -760,6 +807,9 @@ impl RawInvoice { /// Signs the invoice using the supplied `sign_function`. This function MAY fail with an error /// of type `E`. Since the signature of a `SignedRawInvoice` is not required to be valid there /// are no constraints regarding the validity of the produced signature. + /// + /// (C-not exported) As we don't currently support passing function pointers into methods + /// explicitly. pub fn sign(self, sign_method: F) -> Result where F: FnOnce(&Message) -> Result { @@ -771,11 +821,13 @@ impl RawInvoice { Ok(SignedRawInvoice { raw_invoice: self, hash: raw_hash, - signature: Signature(signature), + signature: InvoiceSignature(signature), }) } /// Returns an iterator over all tagged fields with known semantics. + /// + /// (C-not exported) As there is not yet a manual mapping for a FilterMap pub fn known_tagged_fields(&self) -> FilterMap, fn(&RawTaggedField) -> Option<&TaggedField>> { @@ -824,6 +876,7 @@ impl RawInvoice { find_extract!(self.known_tagged_fields(), TaggedField::Features(ref x), x) } + /// (C-not exported) as we don't support Vec<&NonOpaqueType> pub fn fallbacks(&self) -> Vec<&Fallback> { self.known_tagged_fields().filter_map(|tf| match tf { &TaggedField::Fallback(ref f) => Some(f), @@ -936,6 +989,41 @@ impl Invoice { Ok(()) } + /// Check that feature bits are set as required + fn check_feature_bits(&self) -> Result<(), SemanticError> { + // "If the payment_secret feature is set, MUST include exactly one s field." + let payment_secret_count = self.tagged_fields().filter(|&tf| match *tf { + TaggedField::PaymentSecret(_) => true, + _ => false, + }).count(); + if payment_secret_count > 1 { + return Err(SemanticError::MultiplePaymentSecrets); + } + + // "A writer MUST set an s field if and only if the payment_secret feature is set." + let has_payment_secret = payment_secret_count == 1; + let features = self.tagged_fields().find(|&tf| match *tf { + TaggedField::Features(_) => true, + _ => false, + }); + match features { + None if has_payment_secret => Err(SemanticError::InvalidFeatures), + None => Ok(()), + Some(TaggedField::Features(features)) => { + if features.supports_payment_secret() && has_payment_secret { + Ok(()) + } else if has_payment_secret { + Err(SemanticError::InvalidFeatures) + } else if features.supports_payment_secret() { + Err(SemanticError::InvalidFeatures) + } else { + Ok(()) + } + }, + Some(_) => unreachable!(), + } + } + /// Check that the invoice is signed correctly and that key recovery works pub fn check_signature(&self) -> Result<(), SemanticError> { match self.signed_invoice.recover_payee_pub_key() { @@ -970,6 +1058,7 @@ impl Invoice { signed_invoice: signed_invoice, }; invoice.check_field_counts()?; + invoice.check_feature_bits()?; invoice.check_signature()?; Ok(invoice) @@ -981,6 +1070,8 @@ impl Invoice { } /// Returns an iterator over all tagged fields of this Invoice. + /// + /// (C-not exported) As there is not yet a manual mapping for a FilterMap pub fn tagged_fields(&self) -> FilterMap, fn(&RawTaggedField) -> Option<&TaggedField>> { self.signed_invoice.raw_invoice().known_tagged_fields() @@ -992,6 +1083,8 @@ impl Invoice { } /// Return the description or a hash of it for longer ones + /// + /// (C-not exported) because we don't yet export InvoiceDescription pub fn description(&self) -> InvoiceDescription { if let Some(ref direct) = self.signed_invoice.description() { return InvoiceDescription::Direct(direct); @@ -1021,19 +1114,24 @@ impl Invoice { self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0 } - /// Returns the invoice's expiry time if present + /// Returns the invoice's expiry time, if present, otherwise [`DEFAULT_EXPIRY_TIME`]. pub fn expiry_time(&self) -> Duration { self.signed_invoice.expiry_time() .map(|x| x.0) - .unwrap_or(Duration::from_secs(3600)) + .unwrap_or(Duration::from_secs(DEFAULT_EXPIRY_TIME)) } - /// Returns the invoice's `min_cltv_expiry` time if present - pub fn min_final_cltv_expiry(&self) -> Option<&u64> { - self.signed_invoice.min_final_cltv_expiry().map(|x| &x.0) + /// Returns the invoice's `min_final_cltv_expiry` time, if present, otherwise + /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY`]. + pub fn min_final_cltv_expiry(&self) -> u64 { + self.signed_invoice.min_final_cltv_expiry() + .map(|x| x.0) + .unwrap_or(DEFAULT_MIN_FINAL_CLTV_EXPIRY) } /// Returns a list of all fallback addresses + /// + /// (C-not exported) as we don't support Vec<&NonOpaqueType> pub fn fallbacks(&self) -> Vec<&Fallback> { self.signed_invoice.fallbacks() } @@ -1192,7 +1290,7 @@ impl Deref for RouteHint { } } -impl Deref for Signature { +impl Deref for InvoiceSignature { type Target = RecoverableSignature; fn deref(&self) -> &RecoverableSignature { @@ -1253,6 +1351,12 @@ pub enum SemanticError { /// The invoice contains multiple descriptions and/or description hashes which isn't allowed MultipleDescriptions, + /// The invoice contains multiple payment secrets + MultiplePaymentSecrets, + + /// The invoice's features are invalid + InvalidFeatures, + /// The recovery id doesn't fit the signature/pub key InvalidRecoveryId, @@ -1267,6 +1371,8 @@ impl Display for SemanticError { SemanticError::MultiplePaymentHashes => f.write_str("The invoice has multiple payment hashes which isn't allowed"), SemanticError::NoDescription => f.write_str("No description or description hash are part of the invoice"), SemanticError::MultipleDescriptions => f.write_str("The invoice contains multiple descriptions and/or description hashes which isn't allowed"), + SemanticError::MultiplePaymentSecrets => f.write_str("The invoice contains multiple payment secrets"), + SemanticError::InvalidFeatures => f.write_str("The invoice's features are invalid"), SemanticError::InvalidRecoveryId => f.write_str("The recovery id doesn't fit the signature/pub key"), SemanticError::InvalidSignature => f.write_str("The invoice's signature is invalid"), } @@ -1277,6 +1383,8 @@ impl std::error::Error for SemanticError { } /// When signing using a fallible method either an user-supplied `SignError` or a `CreationError` /// may occur. +/// +/// (C-not exported) As we don't support unbounded generics #[derive(Eq, PartialEq, Debug, Clone)] pub enum SignOrCreationError { /// An error occurred during signing @@ -1354,7 +1462,7 @@ mod test { use secp256k1::Secp256k1; use secp256k1::recovery::{RecoveryId, RecoverableSignature}; use secp256k1::key::{SecretKey, PublicKey}; - use {SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart, Currency, Sha256, + use {SignedRawInvoice, InvoiceSignature, RawInvoice, RawHrp, RawDataPart, Currency, Sha256, PositiveTimestamp}; let invoice = SignedRawInvoice { @@ -1383,7 +1491,7 @@ mod test { 0x7b, 0x1d, 0x85, 0x8d, 0xb1, 0xd1, 0xf7, 0xab, 0x71, 0x37, 0xdc, 0xb7, 0x83, 0x5d, 0xb2, 0xec, 0xd5, 0x18, 0xe1, 0xc9 ], - signature: Signature(RecoverableSignature::from_compact( + signature: InvoiceSignature(RecoverableSignature::from_compact( & [ 0x38u8, 0xec, 0x68, 0x91, 0x34, 0x5e, 0x20, 0x41, 0x45, 0xbe, 0x8a, 0x3a, 0x99, 0xde, 0x38, 0xe9, 0x8a, 0x39, 0xd6, 0xa5, 0x69, 0x43, @@ -1452,7 +1560,8 @@ mod test { let builder = InvoiceBuilder::new(Currency::Bitcoin) .payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap()) - .current_timestamp(); + .current_timestamp() + .min_final_cltv_expiry(144); let too_long_string = String::from_iter( (0..1024).map(|_| '?') @@ -1569,7 +1678,6 @@ mod test { .payee_pub_key(public_key.clone()) .expiry_time(Duration::from_secs(54321)) .min_final_cltv_expiry(144) - .min_final_cltv_expiry(143) .fallback(Fallback::PubKeyHash([0;20])) .route(route_1.clone()) .route(route_2.clone()) @@ -1581,7 +1689,7 @@ mod test { }).unwrap(); assert!(invoice.check_signature().is_ok()); - assert_eq!(invoice.tagged_fields().count(), 9); + assert_eq!(invoice.tagged_fields().count(), 8); assert_eq!(invoice.amount_pico_btc(), Some(123)); assert_eq!(invoice.currency(), Currency::BitcoinTestnet); @@ -1591,7 +1699,7 @@ mod test { ); assert_eq!(invoice.payee_pub_key(), Some(&public_key)); assert_eq!(invoice.expiry_time(), Duration::from_secs(54321)); - assert_eq!(invoice.min_final_cltv_expiry(), Some(&144)); + assert_eq!(invoice.min_final_cltv_expiry(), 144); assert_eq!(invoice.fallbacks(), vec![&Fallback::PubKeyHash([0;20])]); assert_eq!(invoice.routes(), vec![&RouteHint(route_1), &RouteHint(route_2)]); assert_eq!( @@ -1603,4 +1711,28 @@ mod test { let raw_invoice = builder.build_raw().unwrap(); assert_eq!(raw_invoice, *invoice.into_signed_raw().raw_invoice()) } + + #[test] + fn test_default_values() { + use ::*; + use secp256k1::Secp256k1; + use secp256k1::key::SecretKey; + + let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin) + .description("Test".into()) + .payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap()) + .current_timestamp() + .build_raw() + .unwrap() + .sign::<_, ()>(|hash| { + let privkey = SecretKey::from_slice(&[41; 32]).unwrap(); + let secp_ctx = Secp256k1::new(); + Ok(secp_ctx.sign_recoverable(hash, &privkey)) + }) + .unwrap(); + let invoice = Invoice::from_signed(signed_invoice).unwrap(); + + assert_eq!(invoice.min_final_cltv_expiry(), DEFAULT_MIN_FINAL_CLTV_EXPIRY); + assert_eq!(invoice.expiry_time(), Duration::from_secs(DEFAULT_EXPIRY_TIME)); + } }