From 0cff1f7bb40bac658023551b22dca0a95f0b7cf3 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 22 Aug 2021 22:11:34 +0000 Subject: [PATCH 1/9] [invoice] Update doctest example invoices to real LDK invoices This swaps out our doctest example invoices for real LDK-generated invoices on a real LDK node. --- lightning-invoice/src/de.rs | 31 +++++++++++++++++++++++-------- lightning-invoice/src/lib.rs | 15 +++++++++++---- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/lightning-invoice/src/de.rs b/lightning-invoice/src/de.rs index dbcb74e073a..2ed356dafb8 100644 --- a/lightning-invoice/src/de.rs +++ b/lightning-invoice/src/de.rs @@ -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::(); /// diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 2ce58f296f9..c3805f91d51 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -1074,10 +1074,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(); /// From a80819c9c2562fdf93a835cd30d42d955863c861 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 22 Aug 2021 19:35:15 +0000 Subject: [PATCH 2/9] [invoice] Add the BOLT 11 failure unit tests that we already pass --- lightning-invoice/tests/ser_de.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lightning-invoice/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs index 442166e740e..2da843fd609 100644 --- a/lightning-invoice/tests/ser_de.rs +++ b/lightning-invoice/tests/ser_de.rs @@ -11,6 +11,7 @@ use secp256k1::Secp256k1; use secp256k1::key::SecretKey; use secp256k1::recovery::{RecoverableSignature, RecoveryId}; use std::time::{Duration, UNIX_EPOCH}; +use std::str::FromStr; // TODO: add more of the examples from BOLT11 and generate ones causing SemanticErrors @@ -149,3 +150,20 @@ fn deserialize() { } } } + +#[test] +fn test_bolt_invalid_invoices() { + // Tests the BOLT 11 invalid invoice test vectors + 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( + "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh" + ), Err(ParseOrSemanticError::ParseError(ParseError::TooShortDataPart))); +} From 181cb1103d0a1f21aa1f2e44e2f36a6c68663b2f Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 22 Aug 2021 19:36:01 +0000 Subject: [PATCH 3/9] [invoice] Fix non-recoverable sig handling and bogus SI prefix err This adds two additional tests from the BOLT 11 invalid invoice tests, fixing the two errors that broke them. It fixes a panic on the "nonrecoverable signature" test and makes the error variant more sensible on the bogus SI prefix test. --- lightning-invoice/src/de.rs | 2 +- lightning-invoice/src/lib.rs | 4 +++- lightning-invoice/tests/ser_de.rs | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lightning-invoice/src/de.rs b/lightning-invoice/src/de.rs index 2ed356dafb8..ac7e7e835e0 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), diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index c3805f91d51..199fbe79a7e 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -1059,7 +1059,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(_) => {}, } diff --git a/lightning-invoice/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs index 2da843fd609..807308044d2 100644 --- a/lightning-invoice/tests/ser_de.rs +++ b/lightning-invoice/tests/ser_de.rs @@ -163,7 +163,13 @@ fn test_bolt_invalid_invoices() { assert_eq!(Invoice::from_str( "LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny" ), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::MixedCase)))); + assert_eq!(Invoice::from_str( + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaxtrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspk28uwq" + ), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidSignature))); assert_eq!(Invoice::from_str( "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh" ), Err(ParseOrSemanticError::ParseError(ParseError::TooShortDataPart))); + assert_eq!(Invoice::from_str( + "lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg" + ), Err(ParseOrSemanticError::ParseError(ParseError::UnknownSiPrefix))); } From 0be428eeda30e449b251e74bc330342abe3ef0c5 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 22 Aug 2021 19:42:29 +0000 Subject: [PATCH 4/9] Convert the invoice creation API to millisats and req it for parse The BOLT 11 invalid invoice test vectors suggest failing to parse invoices which have an amount which is not a whole number of millisatoshis. lightning-invoice, however, happily parses such invoices. While we could continue to parse them, failing them makes for one less check on the user code side, so we might as well. In order to keep the invoice creation less likely to fail, we also switch the Builder amount-setting function to use millisatoshis. --- lightning-invoice/src/lib.rs | 29 +++++++++++++++++++++++------ lightning-invoice/src/utils.rs | 2 +- lightning-invoice/tests/ser_de.rs | 9 ++++++--- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 199fbe79a7e..bcb15245d3f 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -480,8 +480,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) @@ -673,6 +674,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) } @@ -1019,6 +1021,16 @@ impl Invoice { 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> { // "If the payment_secret feature is set, MUST include exactly one s field." @@ -1099,6 +1111,7 @@ impl Invoice { invoice.check_field_counts()?; invoice.check_feature_bits()?; invoice.check_signature()?; + invoice.check_amount()?; Ok(invoice) } @@ -1408,6 +1421,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 { @@ -1421,6 +1437,7 @@ impl Display for SemanticError { 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"), } } } @@ -1670,7 +1687,7 @@ mod test { .current_timestamp(); let invoice = builder.clone() - .amount_pico_btc(15000) + .amount_milli_satoshis(1500) .build_raw() .unwrap(); @@ -1679,7 +1696,7 @@ mod test { let invoice = builder.clone() - .amount_pico_btc(1500) + .amount_milli_satoshis(150) .build_raw() .unwrap(); @@ -1810,7 +1827,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)) @@ -1830,7 +1847,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(), diff --git a/lightning-invoice/src/utils.rs b/lightning-invoice/src/utils.rs index f419f5f7f24..c491538aa59 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); diff --git a/lightning-invoice/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs index 807308044d2..c2f82b0990c 100644 --- a/lightning-invoice/tests/ser_de.rs +++ b/lightning-invoice/tests/ser_de.rs @@ -49,7 +49,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch\ 9zw97j25emudupq63nyw24cg27h2rspfj9srp".to_owned(), InvoiceBuilder::new(Currency::Bitcoin) - .amount_pico_btc(2500000000) + .amount_milli_satoshis(250_000_000) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) .payment_hash(sha256::Hash::from_hex( "0001020304050607080900010203040506070809000102030405060708090102" @@ -78,7 +78,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { dhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7k\ hhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7".to_owned(), InvoiceBuilder::new(Currency::Bitcoin) - .amount_pico_btc(20000000000) + .amount_milli_satoshis(2_000_000_000) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) .payment_hash(sha256::Hash::from_hex( "0001020304050607080900010203040506070809000102030405060708090102" @@ -110,7 +110,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { "0001020304050607080900010203040506070809000102030405060708090102" ).unwrap()) .description("coffee beans".to_string()) - .amount_pico_btc(20000000000) + .amount_milli_satoshis(2_000_000_000) .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) .payment_secret(PaymentSecret([42; 32])) .build_raw() @@ -172,4 +172,7 @@ fn test_bolt_invalid_invoices() { assert_eq!(Invoice::from_str( "lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg" ), Err(ParseOrSemanticError::ParseError(ParseError::UnknownSiPrefix))); + assert_eq!(Invoice::from_str( + "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu7hqtk93pkf7sw55rdv4k9z2vj050rxdr6za9ekfs3nlt5lr89jqpdmxsmlj9urqumg0h9wzpqecw7th56tdms40p2ny9q4ddvjsedzcplva53s" + ), Err(ParseOrSemanticError::SemanticError(SemanticError::ImpreciseAmount))); } From a4a54ed9dfd8e6c725c2fdc5961cfe6b815fa958 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 22 Aug 2021 19:54:08 +0000 Subject: [PATCH 5/9] Check if invoices contain unknown required features This adds the final missing BOLT 11 failure test, checking for unknown required feature flags before accepting an invoice. --- lightning-invoice/src/lib.rs | 4 +++- lightning-invoice/tests/ser_de.rs | 3 +++ lightning/src/ln/features.rs | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index bcb15245d3f..75aac549151 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -1052,7 +1052,9 @@ impl Invoice { None if has_payment_secret => Err(SemanticError::InvalidFeatures), None => Ok(()), Some(TaggedField::Features(features)) => { - if features.supports_payment_secret() && has_payment_secret { + if features.requires_unknown_bits() { + Err(SemanticError::InvalidFeatures) + } else if features.supports_payment_secret() && has_payment_secret { Ok(()) } else if has_payment_secret { Err(SemanticError::InvalidFeatures) diff --git a/lightning-invoice/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs index c2f82b0990c..94e75cbc480 100644 --- a/lightning-invoice/tests/ser_de.rs +++ b/lightning-invoice/tests/ser_de.rs @@ -154,6 +154,9 @@ fn deserialize() { #[test] fn test_bolt_invalid_invoices() { // Tests the BOLT 11 invalid invoice test vectors + assert_eq!(Invoice::from_str( + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" + ), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidFeatures))); assert_eq!(Invoice::from_str( "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt" ), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::InvalidChecksum)))); diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index e78fa3d50d2..c0eb8f68ac5 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -548,7 +548,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(); From 75f7af64f35e58bc8d2746119b3e2b8c44baff88 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 24 Aug 2021 21:00:17 +0000 Subject: [PATCH 6/9] Implement core::hash::Hash more incl invoice::RawTaggedField --- lightning-invoice/src/lib.rs | 22 +++++++++++----------- lightning/src/ln/features.rs | 6 ++++++ lightning/src/routing/network_graph.rs | 2 +- lightning/src/routing/router.rs | 8 ++++---- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 75aac549151..bf92dba573c 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -321,7 +321,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 +342,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 +357,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 +372,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 +393,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 +414,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 +422,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 diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index c0eb8f68ac5..d1f6b89db4f 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) diff --git a/lightning/src/routing/network_graph.rs b/lightning/src/routing/network_graph.rs index 486b71578f3..8accdab6088 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 13356cc1c74..ea55ce1a9d0 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, From c7cf5011bebc01702bd5540645e757c79fa6d265 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 24 Aug 2021 23:15:07 +0000 Subject: [PATCH 7/9] [invoice] Ignore InvalidLength fields BOLT 11 states that a reader "MUST skip over...`p`, `h`, `s` or `n` fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively." Here we do so by simply ignoring any invalid-length field. --- lightning-invoice/src/de.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning-invoice/src/de.rs b/lightning-invoice/src/de.rs index ac7e7e835e0..777ac660f8e 100644 --- a/lightning-invoice/src/de.rs +++ b/lightning-invoice/src/de.rs @@ -419,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)} From a906c498fbe9234d5088cbec6e5dba7f1e6317af Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 24 Aug 2021 23:22:55 +0000 Subject: [PATCH 8/9] Use new BOLT 11 test vectors with payment_secrets and feature flags This pulls the BOLT 11 test vectors from https://github.com/lightningnetwork/lightning-rfc/pull/898, tweaking our tests to properly handle them. --- lightning-invoice/Cargo.toml | 1 + lightning-invoice/tests/ser_de.rs | 364 ++++++++++++++++++++++++------ 2 files changed, 290 insertions(+), 75 deletions(-) diff --git a/lightning-invoice/Cargo.toml b/lightning-invoice/Cargo.toml index 8c6623b8fa5..baa9a79c5d5 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/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs index 94e75cbc480..a2cc0e2e2b6 100644 --- a/lightning-invoice/tests/ser_de.rs +++ b/lightning-invoice/tests/ser_de.rs @@ -1,28 +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()) @@ -31,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_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()) @@ -60,94 +55,313 @@ 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_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( - & [ - 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("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( + &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()) + .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(); } } @@ -155,7 +369,7 @@ fn deserialize() { fn test_bolt_invalid_invoices() { // Tests the BOLT 11 invalid invoice test vectors assert_eq!(Invoice::from_str( - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" ), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidFeatures))); assert_eq!(Invoice::from_str( "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt" @@ -167,15 +381,15 @@ fn test_bolt_invalid_invoices() { "LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny" ), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::MixedCase)))); assert_eq!(Invoice::from_str( - "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaxtrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspk28uwq" + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2" ), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidSignature))); assert_eq!(Invoice::from_str( "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh" ), Err(ParseOrSemanticError::ParseError(ParseError::TooShortDataPart))); assert_eq!(Invoice::from_str( - "lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg" + "lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqrrzc4cvfue4zp3hggxp47ag7xnrlr8vgcmkjxk3j5jqethnumgkpqp23z9jclu3v0a7e0aruz366e9wqdykw6dxhdzcjjhldxq0w6wgqcnu43j" ), Err(ParseOrSemanticError::ParseError(ParseError::UnknownSiPrefix))); assert_eq!(Invoice::from_str( - "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu7hqtk93pkf7sw55rdv4k9z2vj050rxdr6za9ekfs3nlt5lr89jqpdmxsmlj9urqumg0h9wzpqecw7th56tdms40p2ny9q4ddvjsedzcplva53s" + "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x" ), Err(ParseOrSemanticError::SemanticError(SemanticError::ImpreciseAmount))); } From 68793485307ade442e6f6594ce67e62ee1639bc1 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 27 Aug 2021 02:21:32 +0000 Subject: [PATCH 9/9] Require payment secrets when building and reading invoices --- lightning-invoice/src/lib.rs | 65 ++++++++++++++++++++++------------ lightning-invoice/src/utils.rs | 2 +- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index bf92dba573c..61e394da6db 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| { @@ -634,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. @@ -1018,6 +1023,24 @@ impl Invoice { return Err(SemanticError::MultipleDescriptions); } + self.check_payment_secret()?; + + Ok(()) + } + + /// 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 { + return Err(SemanticError::NoPaymentSecret); + } else if payment_secret_count > 1 { + return Err(SemanticError::MultiplePaymentSecrets); + } + Ok(()) } @@ -1033,32 +1056,21 @@ impl Invoice { /// 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); - } + 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.requires_unknown_bits() { Err(SemanticError::InvalidFeatures) - } else if features.supports_payment_secret() && has_payment_secret { - Ok(()) - } else if has_payment_secret { - Err(SemanticError::InvalidFeatures) - } else if features.supports_payment_secret() { + } else if !features.supports_payment_secret() { Err(SemanticError::InvalidFeatures) } else { Ok(()) @@ -1154,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 @@ -1412,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, @@ -1435,6 +1451,7 @@ 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"), @@ -1651,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 = { @@ -1659,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 = { @@ -1667,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 = { @@ -1753,6 +1770,7 @@ mod test { let sign_error_res = builder.clone() .description("Test".into()) + .payment_secret(PaymentSecret([0; 32])) .try_build_signed(|_| { Err("ImaginaryError") }); @@ -1865,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(); @@ -1881,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 c491538aa59..df2bbfd8f12 100644 --- a/lightning-invoice/src/utils.rs +++ b/lightning-invoice/src/utils.rs @@ -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();