Skip to content

Commit ccf9215

Browse files
authored
Merge pull request #1972 from jkczyz/2023-01-bolt12-spec-updates
BOLT 12 spec updates
2 parents 437cc69 + 22ea505 commit ccf9215

File tree

5 files changed

+145
-70
lines changed

5 files changed

+145
-70
lines changed

lightning/src/offers/invoice.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,10 @@ struct InvoiceFields {
338338

339339
impl Invoice {
340340
/// Paths to the recipient originating from publicly reachable nodes, including information
341-
/// needed for routing payments across them. Blinded paths provide recipient privacy by
342-
/// obfuscating its node id.
341+
/// needed for routing payments across them.
342+
///
343+
/// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
344+
/// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
343345
pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
344346
&self.contents.fields().payment_paths[..]
345347
}

lightning/src/offers/invoice_request.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,9 @@ impl InvoiceRequest {
333333
/// for the invoice.
334334
///
335335
/// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
336-
/// must contain one or more elements.
336+
/// must contain one or more elements ordered from most-preferred to least-preferred, if there's
337+
/// a preference. Note, however, that any privacy is lost if a public node id was used for
338+
/// [`Offer::signing_pubkey`].
337339
///
338340
/// Errors if the request contains unknown required features.
339341
///
@@ -845,11 +847,12 @@ mod tests {
845847

846848
#[test]
847849
fn builds_invoice_request_with_quantity() {
850+
let one = NonZeroU64::new(1).unwrap();
848851
let ten = NonZeroU64::new(10).unwrap();
849852

850853
let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
851854
.amount_msats(1000)
852-
.supported_quantity(Quantity::one())
855+
.supported_quantity(Quantity::One)
853856
.build().unwrap()
854857
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
855858
.build().unwrap()
@@ -860,7 +863,7 @@ mod tests {
860863

861864
match OfferBuilder::new("foo".into(), recipient_pubkey())
862865
.amount_msats(1000)
863-
.supported_quantity(Quantity::one())
866+
.supported_quantity(Quantity::One)
864867
.build().unwrap()
865868
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
866869
.amount_msats(2_000).unwrap()
@@ -918,6 +921,17 @@ mod tests {
918921
Ok(_) => panic!("expected error"),
919922
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
920923
}
924+
925+
match OfferBuilder::new("foo".into(), recipient_pubkey())
926+
.amount_msats(1000)
927+
.supported_quantity(Quantity::Bounded(one))
928+
.build().unwrap()
929+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
930+
.build()
931+
{
932+
Ok(_) => panic!("expected error"),
933+
Err(e) => assert_eq!(e, SemanticError::MissingQuantity),
934+
}
921935
}
922936

923937
#[test]
@@ -1102,11 +1116,12 @@ mod tests {
11021116

11031117
#[test]
11041118
fn parses_invoice_request_with_quantity() {
1119+
let one = NonZeroU64::new(1).unwrap();
11051120
let ten = NonZeroU64::new(10).unwrap();
11061121

11071122
let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
11081123
.amount_msats(1000)
1109-
.supported_quantity(Quantity::one())
1124+
.supported_quantity(Quantity::One)
11101125
.build().unwrap()
11111126
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
11121127
.build().unwrap()
@@ -1121,7 +1136,7 @@ mod tests {
11211136

11221137
let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
11231138
.amount_msats(1000)
1124-
.supported_quantity(Quantity::one())
1139+
.supported_quantity(Quantity::One)
11251140
.build().unwrap()
11261141
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
11271142
.amount_msats(2_000).unwrap()
@@ -1206,6 +1221,22 @@ mod tests {
12061221
Ok(_) => panic!("expected error"),
12071222
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
12081223
}
1224+
1225+
let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
1226+
.amount_msats(1000)
1227+
.supported_quantity(Quantity::Bounded(one))
1228+
.build().unwrap()
1229+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
1230+
.build_unchecked()
1231+
.sign(payer_sign).unwrap();
1232+
1233+
let mut buffer = Vec::new();
1234+
invoice_request.write(&mut buffer).unwrap();
1235+
1236+
match InvoiceRequest::try_from(buffer) {
1237+
Ok(_) => panic!("expected error"),
1238+
Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)),
1239+
}
12091240
}
12101241

12111242
#[test]

lightning/src/offers/offer.rs

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ impl OfferBuilder {
106106
let offer = OfferContents {
107107
chains: None, metadata: None, amount: None, description,
108108
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
109-
supported_quantity: Quantity::one(), signing_pubkey,
109+
supported_quantity: Quantity::One, signing_pubkey,
110110
};
111111
OfferBuilder { offer }
112112
}
@@ -178,7 +178,7 @@ impl OfferBuilder {
178178
}
179179

180180
/// Sets the quantity of items for [`Offer::supported_quantity`]. If not called, defaults to
181-
/// [`Quantity::one`].
181+
/// [`Quantity::One`].
182182
///
183183
/// Successive calls to this method will override the previous setting.
184184
pub fn supported_quantity(mut self, quantity: Quantity) -> Self {
@@ -464,19 +464,17 @@ impl OfferContents {
464464

465465
fn is_valid_quantity(&self, quantity: u64) -> bool {
466466
match self.supported_quantity {
467-
Quantity::Bounded(n) => {
468-
let n = n.get();
469-
if n == 1 { false }
470-
else { quantity > 0 && quantity <= n }
471-
},
467+
Quantity::Bounded(n) => quantity <= n.get(),
472468
Quantity::Unbounded => quantity > 0,
469+
Quantity::One => quantity == 1,
473470
}
474471
}
475472

476473
fn expects_quantity(&self) -> bool {
477474
match self.supported_quantity {
478-
Quantity::Bounded(n) => n.get() != 1,
475+
Quantity::Bounded(_) => true,
479476
Quantity::Unbounded => true,
477+
Quantity::One => false,
480478
}
481479
}
482480

@@ -549,25 +547,24 @@ pub type CurrencyCode = [u8; 3];
549547
/// Quantity of items supported by an [`Offer`].
550548
#[derive(Clone, Copy, Debug, PartialEq)]
551549
pub enum Quantity {
552-
/// Up to a specific number of items (inclusive).
550+
/// Up to a specific number of items (inclusive). Use when more than one item can be requested
551+
/// but is limited (e.g., because of per customer or inventory limits).
552+
///
553+
/// May be used with `NonZeroU64::new(1)` but prefer to use [`Quantity::One`] if only one item
554+
/// is supported.
553555
Bounded(NonZeroU64),
554-
/// One or more items.
556+
/// One or more items. Use when more than one item can be requested without any limit.
555557
Unbounded,
558+
/// Only one item. Use when only a single item can be requested.
559+
One,
556560
}
557561

558562
impl Quantity {
559-
/// The default quantity of one.
560-
pub fn one() -> Self {
561-
Quantity::Bounded(NonZeroU64::new(1).unwrap())
562-
}
563-
564563
fn to_tlv_record(&self) -> Option<u64> {
565564
match self {
566-
Quantity::Bounded(n) => {
567-
let n = n.get();
568-
if n == 1 { None } else { Some(n) }
569-
},
565+
Quantity::Bounded(n) => Some(n.get()),
570566
Quantity::Unbounded => Some(0),
567+
Quantity::One => None,
571568
}
572569
}
573570
}
@@ -639,9 +636,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
639636
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));
640637

641638
let supported_quantity = match quantity_max {
642-
None => Quantity::one(),
639+
None => Quantity::One,
643640
Some(0) => Quantity::Unbounded,
644-
Some(1) => return Err(SemanticError::InvalidQuantity),
645641
Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()),
646642
};
647643

@@ -708,7 +704,7 @@ mod tests {
708704
assert!(!offer.is_expired());
709705
assert_eq!(offer.paths(), &[]);
710706
assert_eq!(offer.issuer(), None);
711-
assert_eq!(offer.supported_quantity(), Quantity::one());
707+
assert_eq!(offer.supported_quantity(), Quantity::One);
712708
assert_eq!(offer.signing_pubkey(), pubkey(42));
713709

714710
assert_eq!(
@@ -930,14 +926,15 @@ mod tests {
930926

931927
#[test]
932928
fn builds_offer_with_supported_quantity() {
929+
let one = NonZeroU64::new(1).unwrap();
933930
let ten = NonZeroU64::new(10).unwrap();
934931

935932
let offer = OfferBuilder::new("foo".into(), pubkey(42))
936-
.supported_quantity(Quantity::one())
933+
.supported_quantity(Quantity::One)
937934
.build()
938935
.unwrap();
939936
let tlv_stream = offer.as_tlv_stream();
940-
assert_eq!(offer.supported_quantity(), Quantity::one());
937+
assert_eq!(offer.supported_quantity(), Quantity::One);
941938
assert_eq!(tlv_stream.quantity_max, None);
942939

943940
let offer = OfferBuilder::new("foo".into(), pubkey(42))
@@ -956,13 +953,21 @@ mod tests {
956953
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
957954
assert_eq!(tlv_stream.quantity_max, Some(10));
958955

956+
let offer = OfferBuilder::new("foo".into(), pubkey(42))
957+
.supported_quantity(Quantity::Bounded(one))
958+
.build()
959+
.unwrap();
960+
let tlv_stream = offer.as_tlv_stream();
961+
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
962+
assert_eq!(tlv_stream.quantity_max, Some(1));
963+
959964
let offer = OfferBuilder::new("foo".into(), pubkey(42))
960965
.supported_quantity(Quantity::Bounded(ten))
961-
.supported_quantity(Quantity::one())
966+
.supported_quantity(Quantity::One)
962967
.build()
963968
.unwrap();
964969
let tlv_stream = offer.as_tlv_stream();
965-
assert_eq!(offer.supported_quantity(), Quantity::one());
970+
assert_eq!(offer.supported_quantity(), Quantity::One);
966971
assert_eq!(tlv_stream.quantity_max, None);
967972
}
968973

@@ -1094,7 +1099,7 @@ mod tests {
10941099
#[test]
10951100
fn parses_offer_with_quantity() {
10961101
let offer = OfferBuilder::new("foo".into(), pubkey(42))
1097-
.supported_quantity(Quantity::one())
1102+
.supported_quantity(Quantity::One)
10981103
.build()
10991104
.unwrap();
11001105
if let Err(e) = offer.to_string().parse::<Offer>() {
@@ -1117,17 +1122,12 @@ mod tests {
11171122
panic!("error parsing offer: {:?}", e);
11181123
}
11191124

1120-
let mut tlv_stream = offer.as_tlv_stream();
1121-
tlv_stream.quantity_max = Some(1);
1122-
1123-
let mut encoded_offer = Vec::new();
1124-
tlv_stream.write(&mut encoded_offer).unwrap();
1125-
1126-
match Offer::try_from(encoded_offer) {
1127-
Ok(_) => panic!("expected error"),
1128-
Err(e) => {
1129-
assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity));
1130-
},
1125+
let offer = OfferBuilder::new("foo".into(), pubkey(42))
1126+
.supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap()))
1127+
.build()
1128+
.unwrap();
1129+
if let Err(e) = offer.to_string().parse::<Offer>() {
1130+
panic!("error parsing offer: {:?}", e);
11311131
}
11321132
}
11331133

lightning/src/offers/parse.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ pub enum SemanticError {
157157
InvalidQuantity,
158158
/// A quantity or quantity bounds was provided but was not expected.
159159
UnexpectedQuantity,
160+
/// Metadata was provided but was not expected.
161+
UnexpectedMetadata,
160162
/// Payer metadata was expected but was missing.
161163
MissingPayerMetadata,
162164
/// A payer id was expected but was missing.

0 commit comments

Comments
 (0)