From: Matt Corallo <649246+TheBlueMatt@users.noreply.github.com> Date: Tue, 31 Aug 2021 22:11:22 +0000 (+0000) Subject: Merge pull request #1057 from TheBlueMatt/2021-08-invoice-fails X-Git-Tag: v0.0.101~26 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=6bd1af4f9f3a4c3f011e812956d5b090dfe166a0;hp=a265f1e97a9eff25b3418daf4b12fc9c6ad4c036;p=rust-lightning Merge pull request #1057 from TheBlueMatt/2021-08-invoice-fails Fix and modernize lightning-invoice API --- diff --git a/lightning-invoice/Cargo.toml b/lightning-invoice/Cargo.toml index 8c6623b8..baa9a79c 100644 --- a/lightning-invoice/Cargo.toml +++ b/lightning-invoice/Cargo.toml @@ -16,4 +16,5 @@ num-traits = "0.2.8" bitcoin_hashes = "0.10" [dev-dependencies] +hex = "0.3" lightning = { version = "0.0.100", path = "../lightning", features = ["_test_utils"] } diff --git a/lightning-invoice/src/de.rs b/lightning-invoice/src/de.rs index dbcb74e0..777ac660 100644 --- a/lightning-invoice/src/de.rs +++ b/lightning-invoice/src/de.rs @@ -77,7 +77,7 @@ mod hrp_sm { } else if ['m', 'u', 'n', 'p'].contains(&read_symbol) { Ok(States::ParseAmountSiPrefix) } else { - Err(super::ParseError::MalformedHRP) + Err(super::ParseError::UnknownSiPrefix) } }, States::ParseAmountSiPrefix => Err(super::ParseError::MalformedHRP), @@ -209,10 +209,18 @@ impl FromStr for SiPrefix { /// ``` /// use lightning_invoice::Invoice; /// -/// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\ -/// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\ -/// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\ -/// ky03ylcqca784w"; +/// +/// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\ +/// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\ +/// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\ +/// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\ +/// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\ +/// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\ +/// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\ +/// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\ +/// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\ +/// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\ +/// j5r6drg6k6zcqj0fcwg"; /// /// assert!(invoice.parse::().is_ok()); /// ``` @@ -228,10 +236,17 @@ impl FromStr for Invoice { /// ``` /// use lightning_invoice::*; /// -/// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\ -/// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\ -/// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\ -/// ky03ylcqca784w"; +/// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\ +/// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\ +/// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\ +/// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\ +/// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\ +/// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\ +/// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\ +/// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\ +/// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\ +/// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\ +/// j5r6drg6k6zcqj0fcwg"; /// /// let parsed_1 = invoice.parse::(); /// @@ -404,7 +419,7 @@ fn parse_tagged_parts(data: &[u5]) -> Result, ParseError> { Ok(field) => { parts.push(RawTaggedField::KnownSemantics(field)) }, - Err(ParseError::Skip) => { + Err(ParseError::Skip)|Err(ParseError::Bech32Error(bech32::Error::InvalidLength)) => { parts.push(RawTaggedField::UnknownSemantics(field.into())) }, Err(e) => {return Err(e)} diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 2ce58f29..61e394da 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -127,6 +127,7 @@ pub fn check_platform() { /// /// ``` /// extern crate secp256k1; +/// extern crate lightning; /// extern crate lightning_invoice; /// extern crate bitcoin_hashes; /// @@ -136,6 +137,8 @@ pub fn check_platform() { /// use secp256k1::Secp256k1; /// use secp256k1::key::SecretKey; /// +/// use lightning::ln::PaymentSecret; +/// /// use lightning_invoice::{Currency, InvoiceBuilder}; /// /// # fn main() { @@ -148,10 +151,12 @@ pub fn check_platform() { /// ).unwrap(); /// /// let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).unwrap(); +/// let payment_secret = PaymentSecret([42u8; 32]); /// /// let invoice = InvoiceBuilder::new(Currency::Bitcoin) /// .description("Coins pls!".into()) /// .payment_hash(payment_hash) +/// .payment_secret(payment_secret) /// .current_timestamp() /// .min_final_cltv_expiry(144) /// .build_signed(|hash| { @@ -321,7 +326,7 @@ impl SiPrefix { } /// Enum representing the crypto currencies (or networks) supported by this library -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub enum Currency { /// Bitcoin mainnet Bitcoin, @@ -342,7 +347,7 @@ pub enum Currency { /// Tagged field which may have an unknown tag /// /// (C-not exported) as we don't currently support TaggedField -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub enum RawTaggedField { /// Parsed tagged field with known tag KnownSemantics(TaggedField), @@ -357,7 +362,7 @@ pub enum RawTaggedField { /// (C-not exported) As we don't yet support enum variants with the same name the struct contained /// in the variant. #[allow(missing_docs)] -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub enum TaggedField { PaymentHash(Sha256), Description(Description), @@ -372,18 +377,18 @@ pub enum TaggedField { } /// SHA-256 hash -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct Sha256(pub sha256::Hash); /// Description string /// /// # Invariants /// The description can be at most 639 __bytes__ long -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct Description(String); /// Payee public key -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct PayeePubKey(pub PublicKey); /// Positive duration that defines when (relatively to the timestamp) in the future the invoice @@ -393,17 +398,17 @@ pub struct PayeePubKey(pub PublicKey); /// The number of seconds this expiry time represents has to be in the range /// `0...(SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME)` to avoid overflows when adding it to a /// timestamp -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct ExpiryTime(Duration); /// `min_final_cltv_expiry` to use for the last HTLC in the route -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct MinFinalCltvExpiry(pub u64); // TODO: better types instead onf byte arrays /// Fallback address in case no LN payment is possible #[allow(missing_docs)] -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub enum Fallback { SegWitProgram { version: u5, @@ -414,7 +419,7 @@ pub enum Fallback { } /// Recoverable signature -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct InvoiceSignature(pub RecoverableSignature); /// Private routing information @@ -422,7 +427,7 @@ pub struct InvoiceSignature(pub RecoverableSignature); /// # Invariants /// The encoded route has to be <1024 5bit characters long (<=639 bytes or <=12 hops) /// -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct PrivateRoute(RouteHint); /// Tag constants as specified in BOLT11 @@ -480,8 +485,9 @@ impl InvoiceBui } } - /// Sets the amount in pico BTC. The optimal SI prefix is choosen automatically. - pub fn amount_pico_btc(mut self, amount: u64) -> Self { + /// Sets the amount in millisatoshis. The optimal SI prefix is chosen automatically. + pub fn amount_milli_satoshis(mut self, amount_msat: u64) -> Self { + let amount = amount_msat * 10; // Invoices are denominated in "pico BTC" let biggest_possible_si_prefix = SiPrefix::values_desc() .iter() .find(|prefix| amount % prefix.multiplier() == 0) @@ -633,7 +639,7 @@ impl InvoiceBuilder InvoiceBuilder { +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. @@ -673,6 +679,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"); + invoice.check_amount().expect("should be ensured by type signature of builder"); Ok(invoice) } @@ -1016,35 +1023,54 @@ impl Invoice { return Err(SemanticError::MultipleDescriptions); } + self.check_payment_secret()?; + 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." + /// Checks that there is exactly one payment secret field + fn check_payment_secret(&self) -> Result<(), SemanticError> { + // "A writer 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 { + if payment_secret_count < 1 { + return Err(SemanticError::NoPaymentSecret); + } else if payment_secret_count > 1 { return Err(SemanticError::MultiplePaymentSecrets); } + Ok(()) + } + + /// Check that amount is a whole number of millisatoshis + fn check_amount(&self) -> Result<(), SemanticError> { + if let Some(amount_pico_btc) = self.amount_pico_btc() { + if amount_pico_btc % 10 != 0 { + return Err(SemanticError::ImpreciseAmount); + } + } + Ok(()) + } + + /// Check that feature bits are set as required + fn check_feature_bits(&self) -> Result<(), SemanticError> { + self.check_payment_secret()?; + // "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; + // (this requirement has been since removed, and we now require the payment secret + // feature bit always). 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(()), + None => Err(SemanticError::InvalidFeatures), Some(TaggedField::Features(features)) => { - if features.supports_payment_secret() && has_payment_secret { - Ok(()) - } else if has_payment_secret { + if features.requires_unknown_bits() { Err(SemanticError::InvalidFeatures) - } else if features.supports_payment_secret() { + } else if !features.supports_payment_secret() { Err(SemanticError::InvalidFeatures) } else { Ok(()) @@ -1059,7 +1085,9 @@ impl Invoice { match self.signed_invoice.recover_payee_pub_key() { Err(secp256k1::Error::InvalidRecoveryId) => return Err(SemanticError::InvalidRecoveryId), - Err(_) => panic!("no other error may occur"), + Err(secp256k1::Error::InvalidSignature) => + return Err(SemanticError::InvalidSignature), + Err(e) => panic!("no other error may occur, got {:?}", e), Ok(_) => {}, } @@ -1074,10 +1102,17 @@ impl Invoice { /// ``` /// use lightning_invoice::*; /// - /// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\ - /// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\ - /// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\ - /// ky03ylcqca784w"; + /// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\ + /// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\ + /// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\ + /// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\ + /// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\ + /// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\ + /// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\ + /// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\ + /// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\ + /// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\ + /// j5r6drg6k6zcqj0fcwg"; /// /// let signed = invoice.parse::().unwrap(); /// @@ -1090,6 +1125,7 @@ impl Invoice { invoice.check_field_counts()?; invoice.check_feature_bits()?; invoice.check_signature()?; + invoice.check_amount()?; Ok(invoice) } @@ -1130,8 +1166,8 @@ impl Invoice { } /// Get the payment secret if one was included in the invoice - pub fn payment_secret(&self) -> Option<&PaymentSecret> { - self.signed_invoice.payment_secret() + pub fn payment_secret(&self) -> &PaymentSecret { + self.signed_invoice.payment_secret().expect("was checked by constructor") } /// Get the invoice features if they were included in the invoice @@ -1388,6 +1424,10 @@ pub enum SemanticError { /// The invoice contains multiple descriptions and/or description hashes which isn't allowed MultipleDescriptions, + /// The invoice is missing the mandatory payment secret, which all modern lightning nodes + /// should provide. + NoPaymentSecret, + /// The invoice contains multiple payment secrets MultiplePaymentSecrets, @@ -1399,6 +1439,9 @@ pub enum SemanticError { /// The invoice's signature is invalid InvalidSignature, + + /// The invoice's amount was not a whole number of millisatoshis + ImpreciseAmount, } impl Display for SemanticError { @@ -1408,10 +1451,12 @@ 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::NoPaymentSecret => f.write_str("The invoice is missing the mandatory payment secret"), 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"), + SemanticError::ImpreciseAmount => f.write_str("The invoice's amount was not a whole number of millisatoshis"), } } } @@ -1623,7 +1668,7 @@ mod test { let invoice = invoice_template.clone(); invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key))) }.unwrap(); - assert!(Invoice::from_signed(invoice).is_ok()); + assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret)); // No payment secret or feature bits let invoice = { @@ -1631,7 +1676,7 @@ mod test { invoice.data.tagged_fields.push(Features(InvoiceFeatures::empty()).into()); invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key))) }.unwrap(); - assert!(Invoice::from_signed(invoice).is_ok()); + assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret)); // Missing payment secret let invoice = { @@ -1639,7 +1684,7 @@ mod test { invoice.data.tagged_fields.push(Features(InvoiceFeatures::known()).into()); invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key))) }.unwrap(); - assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::InvalidFeatures)); + assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret)); // Multiple payment secrets let invoice = { @@ -1661,7 +1706,7 @@ mod test { .current_timestamp(); let invoice = builder.clone() - .amount_pico_btc(15000) + .amount_milli_satoshis(1500) .build_raw() .unwrap(); @@ -1670,7 +1715,7 @@ mod test { let invoice = builder.clone() - .amount_pico_btc(1500) + .amount_milli_satoshis(150) .build_raw() .unwrap(); @@ -1725,6 +1770,7 @@ mod test { let sign_error_res = builder.clone() .description("Test".into()) + .payment_secret(PaymentSecret([0; 32])) .try_build_signed(|_| { Err("ImaginaryError") }); @@ -1801,7 +1847,7 @@ mod test { ]); let builder = InvoiceBuilder::new(Currency::BitcoinTestnet) - .amount_pico_btc(123) + .amount_milli_satoshis(123) .timestamp(UNIX_EPOCH + Duration::from_secs(1234567)) .payee_pub_key(public_key.clone()) .expiry_time(Duration::from_secs(54321)) @@ -1821,7 +1867,7 @@ mod test { assert!(invoice.check_signature().is_ok()); assert_eq!(invoice.tagged_fields().count(), 10); - assert_eq!(invoice.amount_pico_btc(), Some(123)); + assert_eq!(invoice.amount_pico_btc(), Some(1230)); assert_eq!(invoice.currency(), Currency::BitcoinTestnet); assert_eq!( invoice.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs(), @@ -1837,7 +1883,7 @@ mod test { InvoiceDescription::Hash(&Sha256(sha256::Hash::from_slice(&[3;32][..]).unwrap())) ); assert_eq!(invoice.payment_hash(), &sha256::Hash::from_slice(&[21;32][..]).unwrap()); - assert_eq!(invoice.payment_secret(), Some(&PaymentSecret([42; 32]))); + assert_eq!(invoice.payment_secret(), &PaymentSecret([42; 32])); assert_eq!(invoice.features(), Some(&InvoiceFeatures::known())); let raw_invoice = builder.build_raw().unwrap(); @@ -1853,6 +1899,7 @@ mod test { let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin) .description("Test".into()) .payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap()) + .payment_secret(PaymentSecret([0; 32])) .current_timestamp() .build_raw() .unwrap() diff --git a/lightning-invoice/src/utils.rs b/lightning-invoice/src/utils.rs index f419f5f7..df2bbfd8 100644 --- a/lightning-invoice/src/utils.rs +++ b/lightning-invoice/src/utils.rs @@ -68,7 +68,7 @@ where .basic_mpp() .min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into()); if let Some(amt) = amt_msat { - invoice = invoice.amount_pico_btc(amt * 10); + invoice = invoice.amount_milli_satoshis(amt); } for hint in route_hints { invoice = invoice.private_route(hint); @@ -132,7 +132,7 @@ mod test { let payment_event = { let mut payment_hash = PaymentHash([0; 32]); payment_hash.0.copy_from_slice(&invoice.payment_hash().as_ref()[0..32]); - nodes[0].node.send_payment(&route, payment_hash, &Some(invoice.payment_secret().unwrap().clone())).unwrap(); + nodes[0].node.send_payment(&route, payment_hash, &Some(invoice.payment_secret().clone())).unwrap(); let mut added_monitors = nodes[0].chain_monitor.added_monitors.lock().unwrap(); assert_eq!(added_monitors.len(), 1); added_monitors.clear(); diff --git a/lightning-invoice/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs index 442166e7..a2cc0e2e 100644 --- a/lightning-invoice/tests/ser_de.rs +++ b/lightning-invoice/tests/ser_de.rs @@ -1,27 +1,30 @@ +extern crate bech32; extern crate bitcoin_hashes; extern crate lightning; extern crate lightning_invoice; extern crate secp256k1; +extern crate hex; use bitcoin_hashes::hex::FromHex; -use bitcoin_hashes::sha256; +use bitcoin_hashes::{sha256, Hash}; +use bech32::u5; use lightning::ln::PaymentSecret; +use lightning::routing::router::{RouteHint, RouteHintHop}; +use lightning::routing::network_graph::RoutingFees; use lightning_invoice::*; -use secp256k1::Secp256k1; -use secp256k1::key::SecretKey; +use secp256k1::PublicKey; use secp256k1::recovery::{RecoverableSignature, RecoveryId}; +use std::collections::HashSet; use std::time::{Duration, UNIX_EPOCH}; +use std::str::FromStr; -// TODO: add more of the examples from BOLT11 and generate ones causing SemanticErrors - -fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { +fn get_test_tuples() -> Vec<(String, SignedRawInvoice, bool, bool)> { vec![ ( - "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmw\ - wd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9\ - ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w".to_owned(), + "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql".to_owned(), InvoiceBuilder::new(Currency::Bitcoin) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_secret(PaymentSecret([0x11; 32])) .payment_hash(sha256::Hash::from_hex( "0001020304050607080900010203040506070809000102030405060708090102" ).unwrap()) @@ -30,26 +33,19 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { .unwrap() .sign(|_| { 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, - 0x4e, 0x18, 0x45, 0xc8, 0xaf, 0x72, 0x05, 0xaf, 0xcf, 0xcc, 0x7f, - 0x42, 0x5f, 0xcd, 0x14, 0x63, 0xe9, 0x3c, 0x32, 0x88, 0x1e, 0xad, - 0x0d, 0x6e, 0x35, 0x6d, 0x46, 0x7e, 0xc8, 0xc0, 0x25, 0x53, 0xf9, - 0xaa, 0xb1, 0x5e, 0x57, 0x38, 0xb1, 0x1f, 0x12, 0x7f - ], - RecoveryId::from_i32(0).unwrap() + &hex::decode("8d3ce9e28357337f62da0162d9454df827f83cfe499aeb1c1db349d4d81127425e434ca29929406c23bba1ae8ac6ca32880b38d4bf6ff874024cac34ba9625f1").unwrap(), + RecoveryId::from_i32(1).unwrap() ) }).unwrap(), - None + false, // Same features as set in InvoiceBuilder + false, // No unknown fields ), ( - "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3\ - k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch\ - 9zw97j25emudupq63nyw24cg27h2rspfj9srp".to_owned(), + "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh".to_owned(), InvoiceBuilder::new(Currency::Bitcoin) - .amount_pico_btc(2500000000) + .amount_milli_satoshis(250_000_000) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_secret(PaymentSecret([0x11; 32])) .payment_hash(sha256::Hash::from_hex( "0001020304050607080900010203040506070809000102030405060708090102" ).unwrap()) @@ -59,93 +55,341 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { .unwrap() .sign(|_| { RecoverableSignature::from_compact( - & [ - 0xe8, 0x96, 0x39, 0xba, 0x68, 0x14, 0xe3, 0x66, 0x89, 0xd4, 0xb9, 0x1b, - 0xf1, 0x25, 0xf1, 0x03, 0x51, 0xb5, 0x5d, 0xa0, 0x57, 0xb0, 0x06, 0x47, - 0xa8, 0xda, 0xba, 0xeb, 0x8a, 0x90, 0xc9, 0x5f, 0x16, 0x0f, 0x9d, 0x5a, - 0x6e, 0x0f, 0x79, 0xd1, 0xfc, 0x2b, 0x96, 0x42, 0x38, 0xb9, 0x44, 0xe2, - 0xfa, 0x4a, 0xa6, 0x77, 0xc6, 0xf0, 0x20, 0xd4, 0x66, 0x47, 0x2a, 0xb8, - 0x42, 0xbd, 0x75, 0x0e - ], + &hex::decode("e59e3ffbd3945e4334879158d31e89b076dff54f3fa7979ae79df2db9dcaf5896cbfe1a478b8d2307e92c88139464cb7e6ef26e414c4abe33337961ddc5e8ab1").unwrap(), + RecoveryId::from_i32(1).unwrap() + ) + }).unwrap(), + false, // Same features as set in InvoiceBuilder + false, // No unknown fields + ), + ( + "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_milli_satoshis(250_000_000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_secret(PaymentSecret([0x11; 32])) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .description("ナンセンス 1杯".to_owned()) + .expiry_time(Duration::from_secs(60)) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("bae41ef385e0fc972977c7ea42b12cbd76577d2412919da8a8a22f9577b6507710c0e96dd78c821dea16453037f717f44aa7e3d196ebb18fbb97307dcb7336c3").unwrap(), RecoveryId::from_i32(1).unwrap() ) }).unwrap(), - None + false, // Same features as set in InvoiceBuilder + false, // No unknown fields ), ( - "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qq\ - dhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7k\ - hhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7".to_owned(), + "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44".to_owned(), InvoiceBuilder::new(Currency::Bitcoin) - .amount_pico_btc(20000000000) + .amount_milli_satoshis(2_000_000_000) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon")) + .payment_secret(PaymentSecret([0x11; 32])) .payment_hash(sha256::Hash::from_hex( "0001020304050607080900010203040506070809000102030405060708090102" ).unwrap()) - .description_hash(sha256::Hash::from_hex( - "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1" + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("f67a5f696648fa4fb102e1a07b230e54722f8e024cee71e80b4847ac191da3fb2d2cdb28cc32344d7e9a9cf5c9b6a0ee0582ae46e9938b9c81e344a4dbb5289d").unwrap(), + RecoveryId::from_i32(1).unwrap() + ) + }).unwrap(), + false, // Same features as set in InvoiceBuilder + false, // No unknown fields + ), + ( + "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8".to_owned(), + InvoiceBuilder::new(Currency::BitcoinTestnet) + .amount_milli_satoshis(2_000_000_000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon")) + .payment_secret(PaymentSecret([0x11; 32])) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .fallback(Fallback::PubKeyHash([49, 114, 181, 101, 79, 102, 131, 200, 251, 20, 105, 89, 211, 71, 206, 48, 60, 174, 76, 167])) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("6ca95a74dc32e69ced6175b15a5cc56a92bf19f5dace0f134b7d94d464b9f5cf6090a18d48b243f289394d17bdf89466d8e6b37df5981f696bc3dd5986e1bee1").unwrap(), + RecoveryId::from_i32(1).unwrap() + ) + }).unwrap(), + false, // Same features as set in InvoiceBuilder + false, // No unknown fields + ), + ( + "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_milli_satoshis(2_000_000_000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon")) + .payment_secret(PaymentSecret([0x11; 32])) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" ).unwrap()) + .fallback(Fallback::PubKeyHash([4, 182, 31, 125, 193, 234, 13, 201, 148, 36, 70, 76, 196, 6, 77, 197, 100, 217, 30, 137])) + .private_route(RouteHint(vec![RouteHintHop { + src_node_id: PublicKey::from_slice(&hex::decode( + "029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255" + ).unwrap()).unwrap(), + short_channel_id: (66051 << 40) | (263430 << 16) | 1800, + fees: RoutingFees { base_msat: 1, proportional_millionths: 20 }, + cltv_expiry_delta: 3, + htlc_maximum_msat: None, htlc_minimum_msat: None, + }, RouteHintHop { + src_node_id: PublicKey::from_slice(&hex::decode( + "039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255" + ).unwrap()).unwrap(), + short_channel_id: (197637 << 40) | (395016 << 16) | 2314, + fees: RoutingFees { base_msat: 2, proportional_millionths: 30 }, + cltv_expiry_delta: 4, + htlc_maximum_msat: None, htlc_minimum_msat: None, + }])) .build_raw() .unwrap() .sign(|_| { RecoverableSignature::from_compact( - & [ - 0xc6, 0x34, 0x86, 0xe8, 0x1f, 0x8c, 0x87, 0x8a, 0x10, 0x5b, 0xc9, 0xd9, - 0x59, 0xaf, 0x19, 0x73, 0x85, 0x4c, 0x4d, 0xc5, 0x52, 0xc4, 0xf0, 0xe0, - 0xe0, 0xc7, 0x38, 0x96, 0x03, 0xd6, 0xbd, 0xc6, 0x77, 0x07, 0xbf, 0x6b, - 0xe9, 0x92, 0xa8, 0xce, 0x7b, 0xf5, 0x00, 0x16, 0xbb, 0x41, 0xd8, 0xa9, - 0xb5, 0x35, 0x86, 0x52, 0xc4, 0x96, 0x04, 0x45, 0xa1, 0x70, 0xd0, 0x49, - 0xce, 0xd4, 0x55, 0x8c - ], + &hex::decode("6a6586db4e8f6d40e3a5bb92e4df5110c627e9ce493af237e20a046b4e86ea200178c59564ecf892f33a9558bf041b6ad2cb8292d7a6c351fbb7f2ae2d16b54e").unwrap(), RecoveryId::from_i32(0).unwrap() ) }).unwrap(), - None + false, // Same features as set in InvoiceBuilder + false, // No unknown fields ), ( - "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp59g4z52329g4z52329g4z52329g4z52329g4z52329g4z52329g4q9qrsgqzfhag3vsafx4e5qssalvw4rn0phsvpp3e5h2xxyk9l8fxsutvndx9t840dqvdrlu2gqmk0q8apqrgnjy9amc07hmjl9e9yzqjks5w2gqgjnyms".to_owned(), + "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym".to_owned(), InvoiceBuilder::new(Currency::Bitcoin) + .amount_milli_satoshis(2_000_000_000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon")) + .payment_secret(PaymentSecret([0x11; 32])) .payment_hash(sha256::Hash::from_hex( "0001020304050607080900010203040506070809000102030405060708090102" ).unwrap()) - .description("coffee beans".to_string()) - .amount_pico_btc(20000000000) + .fallback(Fallback::ScriptHash([143, 85, 86, 59, 154, 25, 243, 33, 194, 17, 233, 185, 243, 140, 223, 104, 110, 160, 120, 69])) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("16810439d1a9bfd5a65acc61340dc92448bb2d456a80b58ce012b73cb5202438020500c9ab7ef5573a4d174c811f669885ae27f895bb3a3be52c243589f87518").unwrap(), + RecoveryId::from_i32(1).unwrap() + ) + }).unwrap(), + false, // Same features as set in InvoiceBuilder + false, // No unknown fields + ), + ( + "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_milli_satoshis(2_000_000_000) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) - .payment_secret(PaymentSecret([42; 32])) + .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon")) + .payment_secret(PaymentSecret([0x11; 32])) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .fallback(Fallback::SegWitProgram { version: u5::try_from_u8(0).unwrap(), + program: vec![117, 30, 118, 232, 25, 145, 150, 212, 84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214] + }) .build_raw() .unwrap() - .sign::<_, ()>(|msg_hash| { - let privkey = SecretKey::from_slice(&[41; 32]).unwrap(); - let secp_ctx = Secp256k1::new(); - Ok(secp_ctx.sign_recoverable(msg_hash, &privkey)) + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("5a8bd7b97c1cc9055ee60cf2356621f8752248e037a953886a1782b44a58f5ff2d94e6bc89b7b514541a3603bb33722b6c08aa1a3639d34becc549a99fea6eae").unwrap(), + RecoveryId::from_i32(0).unwrap() + ) + }).unwrap(), + false, // Same features as set in InvoiceBuilder + false, // No unknown fields + ), + ( + "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_milli_satoshis(2_000_000_000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon")) + .payment_secret(PaymentSecret([0x11; 32])) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .fallback(Fallback::SegWitProgram { version: u5::try_from_u8(0).unwrap(), + program: vec![24, 99, 20, 60, 20, 197, 22, 104, 4, 189, 25, 32, 51, 86, 218, 19, 108, 152, 86, 120, 205, 77, 39, 161, 184, 198, 50, 150, 4, 144, 50, 98] }) - .unwrap(), - None - ) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("2b3ec248f80301a421817369194f012cdd8af8df1c279981420f9e901e20fa3309d791e11355e609b59ce4a220852a0cd55ab862b1785a83b206c90fa74d01c8").unwrap(), + RecoveryId::from_i32(1).unwrap() + ) + }).unwrap(), + false, // Same features as set in InvoiceBuilder + false, // No unknown fields + ), + ( + "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_milli_satoshis(967878534) + .timestamp(UNIX_EPOCH + Duration::from_secs(1572468703)) + .payment_secret(PaymentSecret([0x11; 32])) + .payment_hash(sha256::Hash::from_hex( + "462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f" + ).unwrap()) + .expiry_time(Duration::from_secs(604800)) + .min_final_cltv_expiry(10) + .description("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items".to_owned()) + .private_route(RouteHint(vec![RouteHintHop { + src_node_id: PublicKey::from_slice(&hex::decode( + "03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7" + ).unwrap()).unwrap(), + short_channel_id: (589390 << 40) | (3312 << 16) | 1, + fees: RoutingFees { base_msat: 1000, proportional_millionths: 2500 }, + cltv_expiry_delta: 40, + htlc_maximum_msat: None, htlc_minimum_msat: None, + }])) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("1b1160cf6186b55722c1ac7ea502086baaccaabdc76b326e666b7f309d972b15069bfca11cd365304b36f48230cc12f3f13a017aab65f7c165a169df32282a58").unwrap(), + RecoveryId::from_i32(1).unwrap() + ) + }).unwrap(), + false, // Same features as set in InvoiceBuilder + false, // No unknown fields + ), + ( + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2a25dxl5hrntdtn6zvydt7d66hyzsyhqs4wdynavys42xgl6sgx9c4g7me86a27t07mdtfry458rtjr0v92cnmswpsjscgt2vcse3sgpz3uapa".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_milli_satoshis(2_500_000_000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_secret(PaymentSecret([0x11; 32])) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .description("coffee beans".to_owned()) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("5755469bf4b8e6b6ae7a1308d5f9bad5c82812e0855cd24fac242aa323fa820c5c551ede4faeabcb7fb6d5a464ad0e35c86f615589ee0e0c250c216a662198c1").unwrap(), + RecoveryId::from_i32(1).unwrap() + ) + }).unwrap(), + true, // Different features than set in InvoiceBuilder + false, // No unknown fields + ), + ( + "LNBC25M1PVJLUEZPP5QQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQYPQDQ5VDHKVEN9V5SXYETPDEESSP5ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYGS9Q5SQQQQQQQQQQQQQQQQSGQ2A25DXL5HRNTDTN6ZVYDT7D66HYZSYHQS4WDYNAVYS42XGL6SGX9C4G7ME86A27T07MDTFRY458RTJR0V92CNMSWPSJSCGT2VCSE3SGPZ3UAPA".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_milli_satoshis(2_500_000_000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_secret(PaymentSecret([0x11; 32])) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .description("coffee beans".to_owned()) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("5755469bf4b8e6b6ae7a1308d5f9bad5c82812e0855cd24fac242aa323fa820c5c551ede4faeabcb7fb6d5a464ad0e35c86f615589ee0e0c250c216a662198c1").unwrap(), + RecoveryId::from_i32(1).unwrap() + ) + }).unwrap(), + true, // Different features than set in InvoiceBuilder + false, // No unknown fields + ), + ( + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqz599y53s3ujmcfjp5xrdap68qxymkqphwsexhmhr8wdz5usdzkzrse33chw6dlp3jhuhge9ley7j2ayx36kawe7kmgg8sv5ugdyusdcqzn8z9x".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_milli_satoshis(2_500_000_000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_secret(PaymentSecret([0x11; 32])) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .description("coffee beans".to_owned()) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + &hex::decode("150a5252308f25bc2641a186de87470189bb003774326beee33b9a2a720d1584386631c5dda6fc3195f97464bfc93d2574868eadd767d6da1078329c4349c837").unwrap(), + RecoveryId::from_i32(0).unwrap() + ) + }).unwrap(), + true, // Different features than set in InvoiceBuilder + true, // Some unknown fields + ), ] } - #[test] -fn serialize() { - for (serialized, deserialized, _) in get_test_tuples() { - assert_eq!(deserialized.to_string(), serialized); - } -} - -#[test] -fn deserialize() { - for (serialized, deserialized, maybe_error) in get_test_tuples() { +fn invoice_deserialize() { + for (serialized, deserialized, ignore_feature_diff, ignore_unknown_fields) in get_test_tuples() { + eprintln!("Testing invoice {}...", serialized); let parsed = serialized.parse::().unwrap(); - assert_eq!(parsed, deserialized); + let (parsed_invoice, _, parsed_sig) = parsed.into_parts(); + let (deserialized_invoice, _, deserialized_sig) = deserialized.into_parts(); - let validated = Invoice::from_signed(parsed); + assert_eq!(deserialized_sig, parsed_sig); + assert_eq!(deserialized_invoice.hrp, parsed_invoice.hrp); + assert_eq!(deserialized_invoice.data.timestamp, parsed_invoice.data.timestamp); - if let Some(error) = maybe_error { - assert_eq!(Err(error), validated); - } else { - assert!(validated.is_ok()); + let mut deserialized_hunks: HashSet<_> = deserialized_invoice.data.tagged_fields.iter().collect(); + let mut parsed_hunks: HashSet<_> = parsed_invoice.data.tagged_fields.iter().collect(); + if ignore_feature_diff { + deserialized_hunks.retain(|h| + if let RawTaggedField::KnownSemantics(TaggedField::Features(_)) = h { false } else { true }); + parsed_hunks.retain(|h| + if let RawTaggedField::KnownSemantics(TaggedField::Features(_)) = h { false } else { true }); + } + if ignore_unknown_fields { + parsed_hunks.retain(|h| + if let RawTaggedField::UnknownSemantics(_) = h { false } else { true }); } + assert_eq!(deserialized_hunks, parsed_hunks); + + Invoice::from_signed(serialized.parse::().unwrap()).unwrap(); } } + +#[test] +fn test_bolt_invalid_invoices() { + // Tests the BOLT 11 invalid invoice test vectors + assert_eq!(Invoice::from_str( + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" + ), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidFeatures))); + assert_eq!(Invoice::from_str( + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt" + ), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::InvalidChecksum)))); + assert_eq!(Invoice::from_str( + "pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny" + ), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::MissingSeparator)))); + assert_eq!(Invoice::from_str( + "LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny" + ), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::MixedCase)))); + assert_eq!(Invoice::from_str( + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2" + ), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidSignature))); + assert_eq!(Invoice::from_str( + "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh" + ), Err(ParseOrSemanticError::ParseError(ParseError::TooShortDataPart))); + assert_eq!(Invoice::from_str( + "lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqrrzc4cvfue4zp3hggxp47ag7xnrlr8vgcmkjxk3j5jqethnumgkpqp23z9jclu3v0a7e0aruz366e9wqdykw6dxhdzcjjhldxq0w6wgqcnu43j" + ), Err(ParseOrSemanticError::ParseError(ParseError::UnknownSiPrefix))); + assert_eq!(Invoice::from_str( + "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x" + ), Err(ParseOrSemanticError::SemanticError(SemanticError::ImpreciseAmount))); +} diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index e78fa3d5..d1f6b89d 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -25,6 +25,7 @@ use io; use prelude::*; use core::{cmp, fmt}; +use core::hash::{Hash, Hasher}; use core::marker::PhantomData; use bitcoin::bech32; @@ -362,6 +363,11 @@ impl Clone for Features { } } } +impl Hash for Features { + fn hash(&self, hasher: &mut H) { + self.flags.hash(hasher); + } +} impl PartialEq for Features { fn eq(&self, o: &Self) -> bool { self.flags.eq(&o.flags) @@ -548,7 +554,9 @@ impl Features { &self.flags } - pub(crate) fn requires_unknown_bits(&self) -> bool { + /// Returns true if this `Features` object contains unknown feature flags which are set as + /// "required". + pub fn requires_unknown_bits(&self) -> bool { // Bitwise AND-ing with all even bits set except for known features will select required // unknown features. let byte_count = T::KNOWN_FEATURE_MASK.len(); diff --git a/lightning/src/routing/network_graph.rs b/lightning/src/routing/network_graph.rs index 486b7157..8accdab6 100644 --- a/lightning/src/routing/network_graph.rs +++ b/lightning/src/routing/network_graph.rs @@ -533,7 +533,7 @@ impl_writeable_tlv_based!(ChannelInfo, { /// Fees for routing via a given channel or a node -#[derive(Eq, PartialEq, Copy, Clone, Debug)] +#[derive(Eq, PartialEq, Copy, Clone, Debug, Hash)] pub struct RoutingFees { /// Flat routing fee in satoshis pub base_msat: u32, diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index d8f26cdb..5030f6aa 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -28,7 +28,7 @@ use core::cmp; use core::ops::Deref; /// A hop in a route -#[derive(Clone, PartialEq)] +#[derive(Clone, Hash, PartialEq, Eq)] pub struct RouteHop { /// The node_id of the node at this hop. pub pubkey: PublicKey, @@ -60,7 +60,7 @@ impl_writeable_tlv_based!(RouteHop, { /// A route directs a payment from the sender (us) to the recipient. If the recipient supports MPP, /// it can take multiple paths. Each path is composed of one or more hops through the network. -#[derive(Clone, PartialEq)] +#[derive(Clone, Hash, PartialEq, Eq)] pub struct Route { /// The list of routes taken for a single (potentially-)multi-part payment. The pubkey of the /// last RouteHop in each path must be the same. @@ -108,11 +108,11 @@ impl Readable for Route { } /// A list of hops along a payment path terminating with a channel to the recipient. -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct RouteHint(pub Vec); /// A channel descriptor for a hop along a payment path. -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct RouteHintHop { /// The node_id of the non-target end of the route pub src_node_id: PublicKey,