Skip to content

BOLT 12 spec updates #1972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lightning/src/offers/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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[..]
}
Expand Down
41 changes: 36 additions & 5 deletions lightning/src/offers/invoice_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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]
Expand Down
76 changes: 38 additions & 38 deletions lightning/src/offers/offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added this comment on the wrong variant. More generally, I'd expect this to tell me why I should use one or the other, not just that I should.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed and further documented each variant. PTAL

/// 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,
}
}
}
Expand Down Expand Up @@ -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()),
};

Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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))
Expand All @@ -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);
}

Expand Down Expand Up @@ -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>() {
Expand All @@ -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);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lightning/src/offers/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading