Merge pull request #1057 from TheBlueMatt/2021-08-invoice-fails
authorMatt Corallo <649246+TheBlueMatt@users.noreply.github.com>
Tue, 31 Aug 2021 22:11:22 +0000 (22:11 +0000)
committerGitHub <noreply@github.com>
Tue, 31 Aug 2021 22:11:22 +0000 (22:11 +0000)
Fix and modernize lightning-invoice API

lightning-invoice/Cargo.toml
lightning-invoice/src/de.rs
lightning-invoice/src/lib.rs
lightning-invoice/src/utils.rs
lightning-invoice/tests/ser_de.rs
lightning/src/ln/features.rs
lightning/src/routing/network_graph.rs
lightning/src/routing/router.rs

index 8c6623b8fa5a0c17148210e6491eee1f5dacc20c..baa9a79c5d5af3a055fdbc1d7894dccb47338e32 100644 (file)
@@ -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"] }
index dbcb74e073aeaec450a3333159cfb6ace5232115..777ac660f8e6b764d6ac5a191af3bcbb3636bc1d 100644 (file)
@@ -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::<Invoice>().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::<Invoice>();
 ///
@@ -404,7 +419,7 @@ fn parse_tagged_parts(data: &[u5]) -> Result<Vec<RawTaggedField>, 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)}
index 2ce58f296f9b26b06ec4c92d8b634a57f680d5b3..61e394da6dba4ef70979a5dae1d673ce2b483732 100644 (file)
@@ -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<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> 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<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T,
        }
 }
 
-impl<S: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, S> {
+impl InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, tb::True> {
        /// 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<S: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, S> {
 
                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::<SignedRawInvoice>().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()
index f419f5f7f24077aae76d6048d0935aac50cf3d6f..df2bbfd8f12459381d3296dc44499cffa2030d07 100644 (file)
@@ -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();
index 442166e740e7568f0a25b24883c1f3ad0e016f2e..a2cc0e2e2b62f70aa883946e45fcc123a510dc9e 100644 (file)
@@ -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<SemanticError>)> {
+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<SemanticError>)> {
                                .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<SemanticError>)> {
                                .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::<SignedRawInvoice>().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::<SignedRawInvoice>().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)));
+}
index e78fa3d50d2d05c5bfcafe1fd58eb1b654c167f7..d1f6b89db4f87e968e3624289331c00c476875fe 100644 (file)
@@ -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<T: sealed::Context> Clone for Features<T> {
                }
        }
 }
+impl<T: sealed::Context> Hash for Features<T> {
+       fn hash<H: Hasher>(&self, hasher: &mut H) {
+               self.flags.hash(hasher);
+       }
+}
 impl<T: sealed::Context> PartialEq for Features<T> {
        fn eq(&self, o: &Self) -> bool {
                self.flags.eq(&o.flags)
@@ -548,7 +554,9 @@ impl<T: sealed::Context> Features<T> {
                &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();
index 486b71578f3fde8775e1d7fe86bd0283c5ef7d8e..8accdab60882321dea801497643478b702a21b47 100644 (file)
@@ -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,
index d8f26cdbf64156ec28c47c21cd86b3b15ef2cf10..5030f6aaacbf9569a0ff5e2d26a20a31104c0c9b 100644 (file)
@@ -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<RouteHintHop>);
 
 /// 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,