Merge pull request #3063 from jirijakes/upgrade-bitcoin-031
[rust-lightning] / lightning / src / offers / offer.rs
index baac949515a3de0f180814fa3d25b31b46b35501..762bc1f3306555947f8a44aff9799547965753f5 100644 (file)
@@ -24,7 +24,7 @@
 //! use core::num::NonZeroU64;
 //! use core::time::Duration;
 //!
-//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
+//! use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey};
 //! use lightning::offers::offer::{Offer, OfferBuilder, Quantity};
 //! use lightning::offers::parse::Bolt12ParseError;
 //! use lightning::util::ser::{Readable, Writeable};
@@ -39,7 +39,7 @@
 //! # #[cfg(feature = "std")]
 //! # fn build() -> Result<(), Bolt12ParseError> {
 //! let secp_ctx = Secp256k1::new();
-//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
+//! let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
 //! let pubkey = PublicKey::from(keys);
 //!
 //! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
@@ -78,8 +78,8 @@
 //! [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder
 
 use bitcoin::blockdata::constants::ChainHash;
-use bitcoin::network::constants::Network;
-use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self};
+use bitcoin::network::Network;
+use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, self};
 use core::hash::{Hash, Hasher};
 use core::num::NonZeroU64;
 use core::ops::Deref;
@@ -163,10 +163,9 @@ pub struct OfferBuilder<'a, M: MetadataStrategy, T: secp256k1::Signing> {
 ///
 /// See [module-level documentation] for usage.
 ///
-/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
-///
 /// [module-level documentation]: self
 #[cfg(c_bindings)]
+#[derive(Clone)]
 pub struct OfferWithExplicitMetadataBuilder<'a> {
        offer: OfferContents,
        metadata_strategy: core::marker::PhantomData<ExplicitMetadata>,
@@ -177,10 +176,9 @@ pub struct OfferWithExplicitMetadataBuilder<'a> {
 ///
 /// See [module-level documentation] for usage.
 ///
-/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
-///
 /// [module-level documentation]: self
 #[cfg(c_bindings)]
+#[derive(Clone)]
 pub struct OfferWithDerivedMetadataBuilder<'a> {
        offer: OfferContents,
        metadata_strategy: core::marker::PhantomData<DerivedMetadata>,
@@ -209,9 +207,8 @@ impl MetadataStrategy for DerivedMetadata {}
 macro_rules! offer_explicit_metadata_builder_methods { (
        $self: ident, $self_type: ty, $return_type: ty, $return_value: expr
 ) => {
-       /// Creates a new builder for an offer setting an empty [`Offer::description`] and using the
-       /// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered
-       /// while the offer is valid.
+       /// Creates a new builder for an offer using the [`Offer::signing_pubkey`] for signing invoices.
+       /// The associated secret key must be remembered while the offer is valid.
        ///
        /// Use a different pubkey per offer to avoid correlating offers.
        ///
@@ -225,7 +222,7 @@ macro_rules! offer_explicit_metadata_builder_methods { (
        pub fn new(signing_pubkey: PublicKey) -> Self {
                Self {
                        offer: OfferContents {
-                               chains: None, metadata: None, amount: None, description: String::new(),
+                               chains: None, metadata: None, amount: None, description: None,
                                features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
                                supported_quantity: Quantity::One, signing_pubkey: Some(signing_pubkey),
                        },
@@ -264,7 +261,7 @@ macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => {
                let metadata = Metadata::DerivedSigningPubkey(derivation_material);
                Self {
                        offer: OfferContents {
-                               chains: None, metadata: Some(metadata), amount: None, description: String::new(),
+                               chains: None, metadata: Some(metadata), amount: None, description: None,
                                features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
                                supported_quantity: Quantity::One, signing_pubkey: Some(node_id),
                        },
@@ -330,7 +327,7 @@ macro_rules! offer_builder_methods { (
        ///
        /// Successive calls to this method will override the previous setting.
        pub fn description($($self_mut)* $self: $self_type, description: String) -> $return_type {
-               $self.offer.description = description;
+               $self.offer.description = Some(description);
                $return_value
        }
 
@@ -373,6 +370,10 @@ macro_rules! offer_builder_methods { (
                        None => {},
                }
 
+               if $self.offer.amount.is_some() && $self.offer.description.is_none() {
+                       $self.offer.description = Some(String::new());
+               }
+
                if let Some(chains) = &$self.offer.chains {
                        if chains.len() == 1 && chains[0] == $self.offer.implied_chain() {
                                $self.offer.chains = None;
@@ -551,7 +552,7 @@ pub(super) struct OfferContents {
        chains: Option<Vec<ChainHash>>,
        metadata: Option<Metadata>,
        amount: Option<Amount>,
-       description: String,
+       description: Option<String>,
        features: OfferFeatures,
        absolute_expiry: Option<Duration>,
        issuer: Option<String>,
@@ -579,13 +580,13 @@ macro_rules! offer_accessors { ($self: ident, $contents: expr) => {
        }
 
        /// The minimum amount required for a successful payment of a single item.
-       pub fn amount(&$self) -> Option<&$crate::offers::offer::Amount> {
+       pub fn amount(&$self) -> Option<$crate::offers::offer::Amount> {
                $contents.amount()
        }
 
        /// A complete description of the purpose of the payment. Intended to be displayed to the user
        /// but with the caveat that it has not been verified in any way.
-       pub fn description(&$self) -> $crate::util::string::PrintableString {
+       pub fn description(&$self) -> Option<$crate::util::string::PrintableString> {
                $contents.description()
        }
 
@@ -805,12 +806,12 @@ impl OfferContents {
                self.metadata.as_ref().and_then(|metadata| metadata.as_bytes())
        }
 
-       pub fn amount(&self) -> Option<&Amount> {
-               self.amount.as_ref()
+       pub fn amount(&self) -> Option<Amount> {
+               self.amount
        }
 
-       pub fn description(&self) -> PrintableString {
-               PrintableString(&self.description)
+       pub fn description(&self) -> Option<PrintableString> {
+               self.description.as_ref().map(|description| PrintableString(description))
        }
 
        pub fn features(&self) -> &OfferFeatures {
@@ -908,7 +909,7 @@ impl OfferContents {
        /// Verifies that the offer metadata was produced from the offer in the TLV stream.
        pub(super) fn verify<T: secp256k1::Signing>(
                &self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1<T>
-       ) -> Result<(OfferId, Option<KeyPair>), ()> {
+       ) -> Result<(OfferId, Option<Keypair>), ()> {
                match self.metadata() {
                        Some(metadata) => {
                                let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| {
@@ -954,7 +955,7 @@ impl OfferContents {
                        metadata: self.metadata(),
                        currency,
                        amount,
-                       description: Some(&self.description),
+                       description: self.description.as_ref(),
                        features,
                        absolute_expiry: self.absolute_expiry.map(|duration| duration.as_secs()),
                        paths: self.paths.as_ref(),
@@ -979,7 +980,7 @@ impl Writeable for OfferContents {
 
 /// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or
 /// another currency.
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Copy, Debug, PartialEq)]
 pub enum Amount {
        /// An amount of bitcoin.
        Bitcoin {
@@ -1092,10 +1093,9 @@ impl TryFrom<OfferTlvStream> for OfferContents {
                        (Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }),
                };
 
-               let description = match description {
-                       None => return Err(Bolt12SemanticError::MissingDescription),
-                       Some(description) => description,
-               };
+               if amount.is_some() && description.is_none() {
+                       return Err(Bolt12SemanticError::MissingDescription);
+               }
 
                let features = features.unwrap_or_else(OfferFeatures::empty);
 
@@ -1140,7 +1140,7 @@ mod tests {
        };
 
        use bitcoin::blockdata::constants::ChainHash;
-       use bitcoin::network::constants::Network;
+       use bitcoin::network::Network;
        use bitcoin::secp256k1::Secp256k1;
        use core::num::NonZeroU64;
        use core::time::Duration;
@@ -1166,7 +1166,7 @@ mod tests {
                assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin)));
                assert_eq!(offer.metadata(), None);
                assert_eq!(offer.amount(), None);
-               assert_eq!(offer.description(), PrintableString(""));
+               assert_eq!(offer.description(), None);
                assert_eq!(offer.offer_features(), &OfferFeatures::empty());
                assert_eq!(offer.absolute_expiry(), None);
                #[cfg(feature = "std")]
@@ -1174,6 +1174,7 @@ mod tests {
                assert_eq!(offer.paths(), &[]);
                assert_eq!(offer.issuer(), None);
                assert_eq!(offer.supported_quantity(), Quantity::One);
+               assert!(!offer.expects_quantity());
                assert_eq!(offer.signing_pubkey(), Some(pubkey(42)));
 
                assert_eq!(
@@ -1183,7 +1184,7 @@ mod tests {
                                metadata: None,
                                currency: None,
                                amount: None,
-                               description: Some(&String::from("")),
+                               description: None,
                                features: None,
                                absolute_expiry: None,
                                paths: None,
@@ -1379,7 +1380,7 @@ mod tests {
                        .build()
                        .unwrap();
                let tlv_stream = offer.as_tlv_stream();
-               assert_eq!(offer.amount(), Some(&bitcoin_amount));
+               assert_eq!(offer.amount(), Some(bitcoin_amount));
                assert_eq!(tlv_stream.amount, Some(1000));
                assert_eq!(tlv_stream.currency, None);
 
@@ -1421,7 +1422,7 @@ mod tests {
                        .description("foo".into())
                        .build()
                        .unwrap();
-               assert_eq!(offer.description(), PrintableString("foo"));
+               assert_eq!(offer.description(), Some(PrintableString("foo")));
                assert_eq!(offer.as_tlv_stream().description, Some(&String::from("foo")));
 
                let offer = OfferBuilder::new(pubkey(42))
@@ -1429,8 +1430,15 @@ mod tests {
                        .description("bar".into())
                        .build()
                        .unwrap();
-               assert_eq!(offer.description(), PrintableString("bar"));
+               assert_eq!(offer.description(), Some(PrintableString("bar")));
                assert_eq!(offer.as_tlv_stream().description, Some(&String::from("bar")));
+
+               let offer = OfferBuilder::new(pubkey(42))
+                       .amount_msats(1000)
+                       .build()
+                       .unwrap();
+               assert_eq!(offer.description(), Some(PrintableString("")));
+               assert_eq!(offer.as_tlv_stream().description, Some(&String::from("")));
        }
 
        #[test]
@@ -1541,6 +1549,7 @@ mod tests {
                        .build()
                        .unwrap();
                let tlv_stream = offer.as_tlv_stream();
+               assert!(!offer.expects_quantity());
                assert_eq!(offer.supported_quantity(), Quantity::One);
                assert_eq!(tlv_stream.quantity_max, None);
 
@@ -1549,6 +1558,7 @@ mod tests {
                        .build()
                        .unwrap();
                let tlv_stream = offer.as_tlv_stream();
+               assert!(offer.expects_quantity());
                assert_eq!(offer.supported_quantity(), Quantity::Unbounded);
                assert_eq!(tlv_stream.quantity_max, Some(0));
 
@@ -1557,6 +1567,7 @@ mod tests {
                        .build()
                        .unwrap();
                let tlv_stream = offer.as_tlv_stream();
+               assert!(offer.expects_quantity());
                assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
                assert_eq!(tlv_stream.quantity_max, Some(10));
 
@@ -1565,6 +1576,7 @@ mod tests {
                        .build()
                        .unwrap();
                let tlv_stream = offer.as_tlv_stream();
+               assert!(offer.expects_quantity());
                assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
                assert_eq!(tlv_stream.quantity_max, Some(1));
 
@@ -1574,6 +1586,7 @@ mod tests {
                        .build()
                        .unwrap();
                let tlv_stream = offer.as_tlv_stream();
+               assert!(!offer.expects_quantity());
                assert_eq!(offer.supported_quantity(), Quantity::One);
                assert_eq!(tlv_stream.quantity_max, None);
        }
@@ -1655,6 +1668,14 @@ mod tests {
                        panic!("error parsing offer: {:?}", e);
                }
 
+               let offer = OfferBuilder::new(pubkey(42))
+                       .description("foo".to_string())
+                       .amount_msats(1000)
+                       .build().unwrap();
+               if let Err(e) = offer.to_string().parse::<Offer>() {
+                       panic!("error parsing offer: {:?}", e);
+               }
+
                let mut tlv_stream = offer.as_tlv_stream();
                tlv_stream.description = None;
 
@@ -1845,6 +1866,9 @@ mod bolt12_tests {
                        // with blinded path via Bob (0x424242...), blinding 020202...
                        "lno1pgx9getnwss8vetrw3hhyucs5ypjgef743p5fzqq9nqxh0ah7y87rzv3ud0eleps9kl2d5348hq2k8qzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgqpqqqqqqqqqqqqqqqqqqqqqqqqqqqzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqqzq3zyg3zyg3zyg3vggzamrjghtt05kvkvpcp0a79gmy3nt6jsn98ad2xs8de6sl9qmgvcvs",
 
+                       // ... and with sciddir introduction node
+                       "lno1pgx9getnwss8vetrw3hhyucs3yqqqqqqqqqqqqp2qgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqqyqqqqqqqqqqqqqqqqqqqqqqqqqqqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqqgzyg3zyg3zyg3z93pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpj",
+
                        // ... and with second blinded path via Carol (0x434343...), blinding 020202...
                        "lno1pgx9getnwss8vetrw3hhyucsl5q5yqeyv5l2cs6y3qqzesrth7mlzrlp3xg7xhulusczm04x6g6nms9trspqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqqsqqqqqqqqqqqqqqqqqqqqqqqqqqpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsqpqg3zyg3zyg3zygz0uc7h32x9s0aecdhxlk075kn046aafpuuyw8f5j652t3vha2yqrsyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsqzqqqqqqqqqqqqqqqqqqqqqqqqqqqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqqyzyg3zyg3zyg3zzcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pksese",
 
@@ -1875,7 +1899,7 @@ mod bolt12_tests {
                // Malformed: empty
                assert_eq!(
                        "lno1".parse::<Offer>(),
-                       Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingDescription)),
+                       Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSigningPubkey)),
                );
 
                // Malformed: truncated at type
@@ -2000,7 +2024,8 @@ mod bolt12_tests {
 
                // Missing offer_description
                assert_eq!(
-                       "lno1zcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pksese".parse::<Offer>(),
+                       // TODO: Match the spec once it is updated.
+                       "lno1pqpq86qkyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg".parse::<Offer>(),
                        Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingDescription)),
                );