]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Merge pull request #1972 from jkczyz/2023-01-bolt12-spec-updates
authorMatt Corallo <649246+TheBlueMatt@users.noreply.github.com>
Mon, 30 Jan 2023 23:36:20 +0000 (23:36 +0000)
committerGitHub <noreply@github.com>
Mon, 30 Jan 2023 23:36:20 +0000 (23:36 +0000)
BOLT 12 spec updates

lightning/src/offers/invoice.rs
lightning/src/offers/invoice_request.rs
lightning/src/offers/offer.rs
lightning/src/offers/parse.rs
lightning/src/offers/refund.rs

index 99a97368d92408f735db7509012bb44adc01a680..f423677fbc9dae50568a0d7b3569d0e261db16ab 100644 (file)
@@ -338,8 +338,10 @@ struct InvoiceFields {
 
 impl Invoice {
        /// Paths to the recipient originating from publicly reachable nodes, including information
-       /// needed for routing payments across them. Blinded paths provide recipient privacy by
-       /// obfuscating its node id.
+       /// needed for routing payments across them.
+       ///
+       /// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
+       /// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
        pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
                &self.contents.fields().payment_paths[..]
        }
index e863ef6cd613f2c79759e34b5b2df6987e60e448..5b15704ca5127f413a0a26f3958ada9899f6c3fc 100644 (file)
@@ -333,7 +333,9 @@ impl InvoiceRequest {
        /// for the invoice.
        ///
        /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
-       /// must contain one or more elements.
+       /// must contain one or more elements ordered from most-preferred to least-preferred, if there's
+       /// a preference. Note, however, that any privacy is lost if a public node id was used for
+       /// [`Offer::signing_pubkey`].
        ///
        /// Errors if the request contains unknown required features.
        ///
@@ -845,11 +847,12 @@ mod tests {
 
        #[test]
        fn builds_invoice_request_with_quantity() {
+               let one = NonZeroU64::new(1).unwrap();
                let ten = NonZeroU64::new(10).unwrap();
 
                let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
                        .amount_msats(1000)
-                       .supported_quantity(Quantity::one())
+                       .supported_quantity(Quantity::One)
                        .build().unwrap()
                        .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
                        .build().unwrap()
@@ -860,7 +863,7 @@ mod tests {
 
                match OfferBuilder::new("foo".into(), recipient_pubkey())
                        .amount_msats(1000)
-                       .supported_quantity(Quantity::one())
+                       .supported_quantity(Quantity::One)
                        .build().unwrap()
                        .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
                        .amount_msats(2_000).unwrap()
@@ -918,6 +921,17 @@ mod tests {
                        Ok(_) => panic!("expected error"),
                        Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
                }
+
+               match OfferBuilder::new("foo".into(), recipient_pubkey())
+                       .amount_msats(1000)
+                       .supported_quantity(Quantity::Bounded(one))
+                       .build().unwrap()
+                       .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build()
+               {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
+               }
        }
 
        #[test]
@@ -1102,11 +1116,12 @@ mod tests {
 
        #[test]
        fn parses_invoice_request_with_quantity() {
+               let one = NonZeroU64::new(1).unwrap();
                let ten = NonZeroU64::new(10).unwrap();
 
                let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
                        .amount_msats(1000)
-                       .supported_quantity(Quantity::one())
+                       .supported_quantity(Quantity::One)
                        .build().unwrap()
                        .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
                        .build().unwrap()
@@ -1121,7 +1136,7 @@ mod tests {
 
                let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
                        .amount_msats(1000)
-                       .supported_quantity(Quantity::one())
+                       .supported_quantity(Quantity::One)
                        .build().unwrap()
                        .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
                        .amount_msats(2_000).unwrap()
@@ -1206,6 +1221,22 @@ mod tests {
                        Ok(_) => panic!("expected error"),
                        Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
                }
+
+               let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
+                       .amount_msats(1000)
+                       .supported_quantity(Quantity::Bounded(one))
+                       .build().unwrap()
+                       .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build_unchecked()
+                       .sign(payer_sign).unwrap();
+
+               let mut buffer = Vec::new();
+               invoice_request.write(&mut buffer).unwrap();
+
+               match InvoiceRequest::try_from(buffer) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
+               }
        }
 
        #[test]
index d92d0d8bb4ca9ec3812fc6ad3dd37b5b7b948f9e..a2008b6a0b5a0899e47c09c495829fd6a567bfd4 100644 (file)
@@ -106,7 +106,7 @@ impl OfferBuilder {
                let offer = OfferContents {
                        chains: None, metadata: None, amount: None, description,
                        features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
-                       supported_quantity: Quantity::one(), signing_pubkey,
+                       supported_quantity: Quantity::One, signing_pubkey,
                };
                OfferBuilder { offer }
        }
@@ -178,7 +178,7 @@ impl OfferBuilder {
        }
 
        /// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
-       /// [`Quantity::one`].
+       /// [`Quantity::One`].
        ///
        /// Successive calls to this method will override the previous setting.
        pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
@@ -464,19 +464,17 @@ impl OfferContents {
 
        fn is_valid_quantity(&self, quantity: u64) -> bool {
                match self.supported_quantity {
-                       Quantity::Bounded(n) => {
-                               let n = n.get();
-                               if n == 1 { false }
-                               else { quantity > 0 && quantity <= n }
-                       },
+                       Quantity::Bounded(n) => quantity <= n.get(),
                        Quantity::Unbounded => quantity > 0,
+                       Quantity::One => quantity == 1,
                }
        }
 
        fn expects_quantity(&self) -> bool {
                match self.supported_quantity {
-                       Quantity::Bounded(n) => n.get() != 1,
+                       Quantity::Bounded(_) => true,
                        Quantity::Unbounded => true,
+                       Quantity::One => false,
                }
        }
 
@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
 /// Quantity of items supported by an [`Offer`].
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub enum Quantity {
-       /// Up to a specific number of items (inclusive).
+       /// Up to a specific number of items (inclusive). Use when more than one item can be requested
+       /// but is limited (e.g., because of per customer or inventory limits).
+       ///
+       /// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item
+       /// is supported.
        Bounded(NonZeroU64),
-       /// One or more items.
+       /// One or more items. Use when more than one item can be requested without any limit.
        Unbounded,
+       /// Only one item. Use when only a single item can be requested.
+       One,
 }
 
 impl Quantity {
-       /// The default quantity of one.
-       pub fn one() -> Self {
-               Quantity::Bounded(NonZeroU64::new(1).unwrap())
-       }
-
        fn to_tlv_record(&self) -> Option<u64> {
                match self {
-                       Quantity::Bounded(n) => {
-                               let n = n.get();
-                               if n == 1 { None } else { Some(n) }
-                       },
+                       Quantity::Bounded(n) => Some(n.get()),
                        Quantity::Unbounded => Some(0),
+                       Quantity::One => None,
                }
        }
 }
@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
                        .map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));
 
                let supported_quantity = match quantity_max {
-                       None => Quantity::one(),
+                       None => Quantity::One,
                        Some(0) => Quantity::Unbounded,
-                       Some(1) => return Err(SemanticError::InvalidQuantity),
                        Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
                };
 
@@ -708,7 +704,7 @@ mod tests {
                assert!(!offer.is_expired());
                assert_eq!(offer.paths(), &[]);
                assert_eq!(offer.issuer(), None);
-               assert_eq!(offer.supported_quantity(), Quantity::one());
+               assert_eq!(offer.supported_quantity(), Quantity::One);
                assert_eq!(offer.signing_pubkey(), pubkey(42));
 
                assert_eq!(
@@ -930,14 +926,15 @@ mod tests {
 
        #[test]
        fn builds_offer_with_supported_quantity() {
+               let one = NonZeroU64::new(1).unwrap();
                let ten = NonZeroU64::new(10).unwrap();
 
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
-                       .supported_quantity(Quantity::one())
+                       .supported_quantity(Quantity::One)
                        .build()
                        .unwrap();
                let tlv_stream = offer.as_tlv_stream();
-               assert_eq!(offer.supported_quantity(), Quantity::one());
+               assert_eq!(offer.supported_quantity(), Quantity::One);
                assert_eq!(tlv_stream.quantity_max, None);
 
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
@@ -956,13 +953,21 @@ mod tests {
                assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
                assert_eq!(tlv_stream.quantity_max, Some(10));
 
+               let offer = OfferBuilder::new("foo".into(), pubkey(42))
+                       .supported_quantity(Quantity::Bounded(one))
+                       .build()
+                       .unwrap();
+               let tlv_stream = offer.as_tlv_stream();
+               assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
+               assert_eq!(tlv_stream.quantity_max, Some(1));
+
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
                        .supported_quantity(Quantity::Bounded(ten))
-                       .supported_quantity(Quantity::one())
+                       .supported_quantity(Quantity::One)
                        .build()
                        .unwrap();
                let tlv_stream = offer.as_tlv_stream();
-               assert_eq!(offer.supported_quantity(), Quantity::one());
+               assert_eq!(offer.supported_quantity(), Quantity::One);
                assert_eq!(tlv_stream.quantity_max, None);
        }
 
@@ -1094,7 +1099,7 @@ mod tests {
        #[test]
        fn parses_offer_with_quantity() {
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
-                       .supported_quantity(Quantity::one())
+                       .supported_quantity(Quantity::One)
                        .build()
                        .unwrap();
                if let Err(e) = offer.to_string().parse::<Offer>() {
@@ -1117,17 +1122,12 @@ mod tests {
                        panic!("error parsing offer: {:?}", e);
                }
 
-               let mut tlv_stream = offer.as_tlv_stream();
-               tlv_stream.quantity_max = Some(1);
-
-               let mut encoded_offer = Vec::new();
-               tlv_stream.write(&mut encoded_offer).unwrap();
-
-               match Offer::try_from(encoded_offer) {
-                       Ok(_) => panic!("expected error"),
-                       Err(e) => {
-                               assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
-                       },
+               let offer = OfferBuilder::new("foo".into(), pubkey(42))
+                       .supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
+                       .build()
+                       .unwrap();
+               if let Err(e) = offer.to_string().parse::<Offer>() {
+                       panic!("error parsing offer: {:?}", e);
                }
        }
 
index a7d13e57050379d7982c819501186708d79cf34c..35c1425acc13b86316858065afb1ed15a7a6402e 100644 (file)
@@ -157,6 +157,8 @@ pub enum SemanticError {
        InvalidQuantity,
        /// A quantity or quantity bounds was provided but was not expected.
        UnexpectedQuantity,
+       /// Metadata was provided but was not expected.
+       UnexpectedMetadata,
        /// Payer metadata was expected but was missing.
        MissingPayerMetadata,
        /// A payer id was expected but was missing.
index bee6cc7f5f61ddd39a89744bf33f9d6bc8ea66f5..fff33873954a6f92ab55ad2de8d326447dafd0ac 100644 (file)
@@ -118,9 +118,9 @@ impl RefundBuilder {
                }
 
                let refund = RefundContents {
-                       payer: PayerContents(metadata), metadata: None, description, absolute_expiry: None,
-                       issuer: None, paths: None, chain: None, amount_msats,
-                       features: InvoiceRequestFeatures::empty(), payer_id, payer_note: None,
+                       payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
+                       paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
+                       quantity: None, payer_id, payer_note: None,
                };
 
                Ok(RefundBuilder { refund })
@@ -162,6 +162,20 @@ impl RefundBuilder {
                self
        }
 
+       /// Sets [`Refund::quantity`] of items. This is purely for informational purposes. It is useful
+       /// when the refund pertains to an [`Invoice`] that paid for more than one item from an
+       /// [`Offer`] as specified by [`InvoiceRequest::quantity`].
+       ///
+       /// Successive calls to this method will override the previous setting.
+       ///
+       /// [`Invoice`]: crate::offers::invoice::Invoice
+       /// [`InvoiceRequest::quantity`]: crate::offers::invoice_request::InvoiceRequest::quantity
+       /// [`Offer`]: crate::offers::offer::Offer
+       pub fn quantity(mut self, quantity: u64) -> Self {
+               self.refund.quantity = Some(quantity);
+               self
+       }
+
        /// Sets the [`Refund::payer_note`].
        ///
        /// Successive calls to this method will override the previous setting.
@@ -215,7 +229,6 @@ pub struct Refund {
 pub(super) struct RefundContents {
        payer: PayerContents,
        // offer fields
-       metadata: Option<Vec<u8>>,
        description: String,
        absolute_expiry: Option<Duration>,
        issuer: Option<String>,
@@ -224,6 +237,7 @@ pub(super) struct RefundContents {
        chain: Option<ChainHash>,
        amount_msats: u64,
        features: InvoiceRequestFeatures,
+       quantity: Option<u64>,
        payer_id: PublicKey,
        payer_note: Option<String>,
 }
@@ -285,6 +299,11 @@ impl Refund {
                &self.contents.features
        }
 
+       /// The quantity of an item that refund is for.
+       pub fn quantity(&self) -> Option<u64> {
+               self.contents.quantity
+       }
+
        /// A public node id to send to in the case where there are no [`paths`]. Otherwise, a possibly
        /// transient pubkey.
        ///
@@ -312,7 +331,9 @@ impl Refund {
        /// offer, which does have a `signing_pubkey`.
        ///
        /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
-       /// must contain one or more elements.
+       /// must contain one or more elements ordered from most-preferred to least-preferred, if there's
+       /// a preference. Note, however, that any privacy is lost if a public node id is used for
+       /// `signing_pubkey`.
        ///
        /// Errors if the request contains unknown required features.
        ///
@@ -375,7 +396,7 @@ impl RefundContents {
 
                let offer = OfferTlvStreamRef {
                        chains: None,
-                       metadata: self.metadata.as_ref(),
+                       metadata: None,
                        currency: None,
                        amount: None,
                        description: Some(&self.description),
@@ -396,7 +417,7 @@ impl RefundContents {
                        chain: self.chain.as_ref(),
                        amount: Some(self.amount_msats),
                        features,
-                       quantity: None,
+                       quantity: self.quantity,
                        payer_id: Some(&self.payer_id),
                        payer_note: self.payer_note.as_ref(),
                };
@@ -477,6 +498,10 @@ impl TryFrom<RefundTlvStream> for RefundContents {
                        Some(metadata) => PayerContents(metadata),
                };
 
+               if metadata.is_some() {
+                       return Err(SemanticError::UnexpectedMetadata);
+               }
+
                if chains.is_some() {
                        return Err(SemanticError::UnexpectedChain);
                }
@@ -514,20 +539,14 @@ impl TryFrom<RefundTlvStream> for RefundContents {
 
                let features = features.unwrap_or_else(InvoiceRequestFeatures::empty);
 
-               // TODO: Check why this isn't in the spec.
-               if quantity.is_some() {
-                       return Err(SemanticError::UnexpectedQuantity);
-               }
-
                let payer_id = match payer_id {
                        None => return Err(SemanticError::MissingPayerId),
                        Some(payer_id) => payer_id,
                };
 
-               // TODO: Should metadata be included?
                Ok(RefundContents {
-                       payer, metadata, description, absolute_expiry, issuer, paths, chain, amount_msats,
-                       features, payer_id, payer_note,
+                       payer, description, absolute_expiry, issuer, paths, chain, amount_msats, features,
+                       quantity, payer_id, payer_note,
                })
        }
 }
@@ -755,6 +774,24 @@ mod tests {
                assert_eq!(tlv_stream.chain, Some(&testnet));
        }
 
+       #[test]
+       fn builds_refund_with_quantity() {
+               let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+                       .quantity(10)
+                       .build().unwrap();
+               let (_, _, tlv_stream) = refund.as_tlv_stream();
+               assert_eq!(refund.quantity(), Some(10));
+               assert_eq!(tlv_stream.quantity, Some(10));
+
+               let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+                       .quantity(10)
+                       .quantity(1)
+                       .build().unwrap();
+               let (_, _, tlv_stream) = refund.as_tlv_stream();
+               assert_eq!(refund.quantity(), Some(1));
+               assert_eq!(tlv_stream.quantity, Some(1));
+       }
+
        #[test]
        fn builds_refund_with_payer_note() {
                let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
@@ -888,6 +925,7 @@ mod tests {
                        .path(paths[1].clone())
                        .chain(Network::Testnet)
                        .features_unchecked(InvoiceRequestFeatures::unknown())
+                       .quantity(10)
                        .payer_note("baz".into())
                        .build()
                        .unwrap();
@@ -900,6 +938,7 @@ mod tests {
                                assert_eq!(refund.issuer(), Some(PrintableString("bar")));
                                assert_eq!(refund.chain(), ChainHash::using_genesis_block(Network::Testnet));
                                assert_eq!(refund.features(), &InvoiceRequestFeatures::unknown());
+                               assert_eq!(refund.quantity(), Some(10));
                                assert_eq!(refund.payer_note(), Some(PrintableString("baz")));
                        },
                        Err(e) => panic!("error parsing refund: {:?}", e),
@@ -914,6 +953,17 @@ mod tests {
                        panic!("error parsing refund: {:?}", e);
                }
 
+               let metadata = vec![42; 32];
+               let mut tlv_stream = refund.as_tlv_stream();
+               tlv_stream.1.metadata = Some(&metadata);
+
+               match Refund::try_from(tlv_stream.to_bytes()) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedMetadata));
+                       },
+               }
+
                let chains = vec![ChainHash::using_genesis_block(Network::Testnet)];
                let mut tlv_stream = refund.as_tlv_stream();
                tlv_stream.1.chains = Some(&chains);
@@ -967,16 +1017,6 @@ mod tests {
                                assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedSigningPubkey));
                        },
                }
-
-               let mut tlv_stream = refund.as_tlv_stream();
-               tlv_stream.2.quantity = Some(10);
-
-               match Refund::try_from(tlv_stream.to_bytes()) {
-                       Ok(_) => panic!("expected error"),
-                       Err(e) => {
-                               assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedQuantity));
-                       },
-               }
        }
 
        #[test]