Implement core::hash::Hash more incl invoice::RawTaggedField
[rust-lightning] / lightning-invoice / src / lib.rs
index fa6a8eedb0fb434385ebe25b553a2e03a3543675..bf92dba573c81b04176b0eeb5133bc7262685d2d 100644 (file)
@@ -60,12 +60,16 @@ const MAX_EXPIRY_TIME: u64 = 60 * 60 * 24 * 356;
 /// Default expiry time as defined by [BOLT 11].
 ///
 /// [BOLT 11]: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
-const DEFAULT_EXPIRY_TIME: u64 = 3600;
+pub const DEFAULT_EXPIRY_TIME: u64 = 3600;
 
 /// Default minimum final CLTV expiry as defined by [BOLT 11].
 ///
+/// Note that this is *not* the same value as rust-lightning's minimum CLTV expiry, which is
+/// provided in [`MIN_FINAL_CLTV_EXPIRY`].
+///
 /// [BOLT 11]: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
-const DEFAULT_MIN_FINAL_CLTV_EXPIRY: u64 = 18;
+/// [`MIN_FINAL_CLTV_EXPIRY`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY
+pub const DEFAULT_MIN_FINAL_CLTV_EXPIRY: u64 = 18;
 
 /// This function is used as a static assert for the size of `SystemTime`. If the crate fails to
 /// compile due to it this indicates that your system uses unexpected bounds for `SystemTime`. You
@@ -317,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,
@@ -338,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),
@@ -353,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),
@@ -368,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
@@ -389,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,
@@ -410,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
@@ -418,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
@@ -476,8 +480,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)
@@ -669,6 +674,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)
        }
@@ -1015,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."
@@ -1036,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)
@@ -1055,7 +1073,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(_) => {},
                }
 
@@ -1070,10 +1090,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();
        ///
@@ -1086,6 +1113,7 @@ impl Invoice {
                invoice.check_field_counts()?;
                invoice.check_feature_bits()?;
                invoice.check_signature()?;
+               invoice.check_amount()?;
 
                Ok(invoice)
        }
@@ -1395,6 +1423,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,6 +1439,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"),
                }
        }
 }
@@ -1657,7 +1689,7 @@ mod test {
                        .current_timestamp();
 
                let invoice = builder.clone()
-                       .amount_pico_btc(15000)
+                       .amount_milli_satoshis(1500)
                        .build_raw()
                        .unwrap();
 
@@ -1666,7 +1698,7 @@ mod test {
 
 
                let invoice = builder.clone()
-                       .amount_pico_btc(1500)
+                       .amount_milli_satoshis(150)
                        .build_raw()
                        .unwrap();
 
@@ -1797,7 +1829,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))
@@ -1817,7 +1849,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(),