X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning-invoice%2Fsrc%2Flib.rs;h=fa6a8eedb0fb434385ebe25b553a2e03a3543675;hb=294009969aef617df4184ff41ef6daa5a445f213;hp=2aeee2c4535bcc284106fbb1ca77f1ba76859fb9;hpb=0592c52f230db9d2bf82855deaa8a42060381cc8;p=rust-lightning diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 2aeee2c4..fa6a8eed 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -30,7 +30,7 @@ use lightning::ln::PaymentSecret; use lightning::ln::features::InvoiceFeatures; #[cfg(any(doc, test))] use lightning::routing::network_graph::RoutingFees; -use lightning::routing::router::RouteHintHop; +use lightning::routing::router::RouteHint; use secp256k1::key::PublicKey; use secp256k1::{Message, Secp256k1}; @@ -328,11 +328,16 @@ pub enum Currency { /// Bitcoin regtest Regtest, - /// Bitcoin simnet/signet + /// Bitcoin simnet Simnet, + + /// Bitcoin signet + Signet, } /// Tagged field which may have an unknown tag +/// +/// (C-not exported) as we don't currently support TaggedField #[derive(Eq, PartialEq, Debug, Clone)] pub enum RawTaggedField { /// Parsed tagged field with known tag @@ -344,6 +349,9 @@ pub enum RawTaggedField { /// Tagged field with known tag /// /// For descriptions of the enum values please refer to the enclosed type's docs. +/// +/// (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)] pub enum TaggedField { @@ -354,7 +362,7 @@ pub enum TaggedField { ExpiryTime(ExpiryTime), MinFinalCltvExpiry(MinFinalCltvExpiry), Fallback(Fallback), - Route(RouteHint), + PrivateRoute(PrivateRoute), PaymentSecret(PaymentSecret), Features(InvoiceFeatures), } @@ -411,7 +419,7 @@ pub struct InvoiceSignature(pub RecoverableSignature); /// The encoded route has to be <1024 5bit characters long (<=639 bytes or <=12 hops) /// #[derive(Eq, PartialEq, Debug, Clone)] -pub struct RouteHint(Vec); +pub struct PrivateRoute(RouteHint); /// Tag constants as specified in BOLT11 #[allow(missing_docs)] @@ -423,7 +431,7 @@ pub mod constants { pub const TAG_EXPIRY_TIME: u8 = 6; pub const TAG_MIN_FINAL_CLTV_EXPIRY: u8 = 24; pub const TAG_FALLBACK: u8 = 9; - pub const TAG_ROUTE: u8 = 3; + pub const TAG_PRIVATE_ROUTE: u8 = 3; pub const TAG_PAYMENT_SECRET: u8 = 16; pub const TAG_FEATURES: u8 = 5; } @@ -501,9 +509,9 @@ impl InvoiceBui } /// Adds a private route. - pub fn route(mut self, route: Vec) -> Self { - match RouteHint::new(route) { - Ok(r) => self.tagged_fields.push(TaggedField::Route(r)), + pub fn private_route(mut self, hint: RouteHint) -> Self { + match PrivateRoute::new(hint) { + Ok(r) => self.tagged_fields.push(TaggedField::PrivateRoute(r)), Err(e) => self.error = Some(e), } self @@ -739,7 +747,7 @@ impl SignedRawInvoice { /// Finds the first element of an enum stream of a given variant and extracts one member of the /// variant. If no element was found `None` gets returned. /// -/// The following example would extract the first +/// The following example would extract the first B. /// ``` /// use Enum::* /// @@ -753,11 +761,35 @@ impl SignedRawInvoice { /// assert_eq!(find_extract!(elements.iter(), Enum::B(ref x), x), Some(3u16)) /// ``` macro_rules! find_extract { - ($iter:expr, $enm:pat, $enm_var:ident) => { + ($iter:expr, $enm:pat, $enm_var:ident) => { + find_all_extract!($iter, $enm, $enm_var).next() + }; +} + +/// Finds the all elements of an enum stream of a given variant and extracts one member of the +/// variant through an iterator. +/// +/// The following example would extract all A. +/// ``` +/// use Enum::* +/// +/// enum Enum { +/// A(u8), +/// B(u16) +/// } +/// +/// let elements = vec![A(1), A(2), B(3), A(4)] +/// +/// assert_eq!( +/// find_all_extract!(elements.iter(), Enum::A(ref x), x).collect::>(), +/// vec![1u8, 2u8, 4u8]) +/// ``` +macro_rules! find_all_extract { + ($iter:expr, $enm:pat, $enm_var:ident) => { $iter.filter_map(|tf| match *tf { $enm => Some($enm_var), _ => None, - }).next() + }) }; } @@ -878,17 +910,11 @@ impl RawInvoice { /// (C-not exported) as we don't support Vec<&NonOpaqueType> pub fn fallbacks(&self) -> Vec<&Fallback> { - self.known_tagged_fields().filter_map(|tf| match tf { - &TaggedField::Fallback(ref f) => Some(f), - _ => None, - }).collect::>() + find_all_extract!(self.known_tagged_fields(), TaggedField::Fallback(ref x), x).collect() } - pub fn routes(&self) -> Vec<&RouteHint> { - self.known_tagged_fields().filter_map(|tf| match tf { - &TaggedField::Route(ref r) => Some(r), - _ => None, - }).collect::>() + pub fn private_routes(&self) -> Vec<&PrivateRoute> { + find_all_extract!(self.known_tagged_fields(), TaggedField::PrivateRoute(ref x), x).collect() } pub fn amount_pico_btc(&self) -> Option { @@ -1040,7 +1066,7 @@ impl Invoice { Ok(()) } - /// Constructs an `Invoice` from a `SignedInvoice` by checking all its invariants. + /// Constructs an `Invoice` from a `SignedRawInvoice` by checking all its invariants. /// ``` /// use lightning_invoice::*; /// @@ -1137,8 +1163,15 @@ impl Invoice { } /// Returns a list of all routes included in the invoice - pub fn routes(&self) -> Vec<&RouteHint> { - self.signed_invoice.routes() + pub fn private_routes(&self) -> Vec<&PrivateRoute> { + self.signed_invoice.private_routes() + } + + /// Returns a list of all routes included in the invoice as the underlying hints + pub fn route_hints(&self) -> Vec<&RouteHint> { + find_all_extract!( + self.signed_invoice.known_tagged_fields(), TaggedField::PrivateRoute(ref x), x + ).map(|route| &**route).collect() } /// Returns the currency for which the invoice was issued @@ -1169,7 +1202,7 @@ impl TaggedField { TaggedField::ExpiryTime(_) => constants::TAG_EXPIRY_TIME, TaggedField::MinFinalCltvExpiry(_) => constants::TAG_MIN_FINAL_CLTV_EXPIRY, TaggedField::Fallback(_) => constants::TAG_FALLBACK, - TaggedField::Route(_) => constants::TAG_ROUTE, + TaggedField::PrivateRoute(_) => constants::TAG_PRIVATE_ROUTE, TaggedField::PaymentSecret(_) => constants::TAG_PAYMENT_SECRET, TaggedField::Features(_) => constants::TAG_FEATURES, }; @@ -1260,32 +1293,32 @@ impl ExpiryTime { } } -impl RouteHint { - /// Create a new (partial) route from a list of hops - pub fn new(hops: Vec) -> Result { - if hops.len() <= 12 { - Ok(RouteHint(hops)) +impl PrivateRoute { + /// Creates a new (partial) route from a list of hops + pub fn new(hops: RouteHint) -> Result { + if hops.0.len() <= 12 { + Ok(PrivateRoute(hops)) } else { Err(CreationError::RouteTooLong) } } - /// Returrn the underlying vector of hops - pub fn into_inner(self) -> Vec { + /// Returns the underlying list of hops + pub fn into_inner(self) -> RouteHint { self.0 } } -impl Into> for RouteHint { - fn into(self) -> Vec { +impl Into for PrivateRoute { + fn into(self) -> RouteHint { self.into_inner() } } -impl Deref for RouteHint { - type Target = Vec; +impl Deref for PrivateRoute { + type Target = RouteHint; - fn deref(&self) -> &Vec { + fn deref(&self) -> &RouteHint { &self.0 } } @@ -1383,10 +1416,8 @@ impl std::error::Error for SemanticError { } /// When signing using a fallible method either an user-supplied `SignError` or a `CreationError` /// may occur. -/// -/// (C-not exported) As we don't support unbounded generics #[derive(Eq, PartialEq, Debug, Clone)] -pub enum SignOrCreationError { +pub enum SignOrCreationError { /// An error occurred during signing SignError(S), @@ -1525,6 +1556,97 @@ mod test { assert!(new_signed.check_signature()); } + #[test] + fn test_check_feature_bits() { + use TaggedField::*; + use lightning::ln::features::InvoiceFeatures; + use secp256k1::Secp256k1; + use secp256k1::key::SecretKey; + use {RawInvoice, RawHrp, RawDataPart, Currency, Sha256, PositiveTimestamp, Invoice, + SemanticError}; + + let private_key = SecretKey::from_slice(&[42; 32]).unwrap(); + let payment_secret = lightning::ln::PaymentSecret([21; 32]); + let invoice_template = RawInvoice { + hrp: RawHrp { + currency: Currency::Bitcoin, + raw_amount: None, + si_prefix: None, + }, + data: RawDataPart { + timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(), + tagged_fields: vec ! [ + PaymentHash(Sha256(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap())).into(), + Description( + ::Description::new( + "Please consider supporting this project".to_owned() + ).unwrap() + ).into(), + ], + }, + }; + + // Missing features + let invoice = { + let mut invoice = invoice_template.clone(); + invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into()); + invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key))) + }.unwrap(); + assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::InvalidFeatures)); + + // Missing feature bits + let invoice = { + let mut invoice = invoice_template.clone(); + invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into()); + invoice.data.tagged_fields.push(Features(InvoiceFeatures::empty()).into()); + invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key))) + }.unwrap(); + assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::InvalidFeatures)); + + // Including payment secret and feature bits + let invoice = { + let mut invoice = invoice_template.clone(); + invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into()); + invoice.data.tagged_fields.push(Features(InvoiceFeatures::known()).into()); + invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key))) + }.unwrap(); + assert!(Invoice::from_signed(invoice).is_ok()); + + // No payment secret or features + let invoice = { + let invoice = invoice_template.clone(); + invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key))) + }.unwrap(); + assert!(Invoice::from_signed(invoice).is_ok()); + + // No payment secret or feature bits + let invoice = { + let mut invoice = invoice_template.clone(); + 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()); + + // Missing payment secret + let invoice = { + let mut invoice = invoice_template.clone(); + 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)); + + // Multiple payment secrets + let invoice = { + let mut invoice = invoice_template.clone(); + invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into()); + invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into()); + invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key))) + }.unwrap(); + assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::MultiplePaymentSecrets)); + } + #[test] fn test_builder_amount() { use ::*; @@ -1555,6 +1677,7 @@ mod test { #[test] fn test_builder_fail() { use ::*; + use lightning::routing::router::RouteHintHop; use std::iter::FromIterator; use secp256k1::key::PublicKey; @@ -1589,10 +1712,10 @@ mod test { htlc_minimum_msat: None, htlc_maximum_msat: None, }; - let too_long_route = vec![route_hop; 13]; + let too_long_route = RouteHint(vec![route_hop; 13]); let long_route_res = builder.clone() .description("Test".into()) - .route(too_long_route) + .private_route(too_long_route) .build_raw(); assert_eq!(long_route_res, Err(CreationError::RouteTooLong)); @@ -1607,6 +1730,7 @@ mod test { #[test] fn test_builder_ok() { use ::*; + use lightning::routing::router::RouteHintHop; use secp256k1::Secp256k1; use secp256k1::key::{SecretKey, PublicKey}; use std::time::{UNIX_EPOCH, Duration}; @@ -1622,7 +1746,7 @@ mod test { ).unwrap(); let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key); - let route_1 = vec![ + let route_1 = RouteHint(vec![ RouteHintHop { src_node_id: public_key.clone(), short_channel_id: de::parse_int_be(&[123; 8], 256).expect("short chan ID slice too big?"), @@ -1645,9 +1769,9 @@ mod test { htlc_minimum_msat: None, htlc_maximum_msat: None, } - ]; + ]); - let route_2 = vec![ + let route_2 = RouteHint(vec![ RouteHintHop { src_node_id: public_key.clone(), short_channel_id: 0, @@ -1670,7 +1794,7 @@ mod test { htlc_minimum_msat: None, htlc_maximum_msat: None, } - ]; + ]); let builder = InvoiceBuilder::new(Currency::BitcoinTestnet) .amount_pico_btc(123) @@ -1679,8 +1803,8 @@ mod test { .expiry_time(Duration::from_secs(54321)) .min_final_cltv_expiry(144) .fallback(Fallback::PubKeyHash([0;20])) - .route(route_1.clone()) - .route(route_2.clone()) + .private_route(route_1.clone()) + .private_route(route_2.clone()) .description_hash(sha256::Hash::from_slice(&[3;32][..]).unwrap()) .payment_hash(sha256::Hash::from_slice(&[21;32][..]).unwrap()) .payment_secret(PaymentSecret([42; 32])) @@ -1703,7 +1827,7 @@ mod test { assert_eq!(invoice.expiry_time(), Duration::from_secs(54321)); assert_eq!(invoice.min_final_cltv_expiry(), 144); assert_eq!(invoice.fallbacks(), vec![&Fallback::PubKeyHash([0;20])]); - assert_eq!(invoice.routes(), vec![&RouteHint(route_1), &RouteHint(route_2)]); + assert_eq!(invoice.private_routes(), vec![&PrivateRoute(route_1), &PrivateRoute(route_2)]); assert_eq!( invoice.description(), InvoiceDescription::Hash(&Sha256(sha256::Hash::from_slice(&[3;32][..]).unwrap()))