Skip to content

Commit 8ed6e64

Browse files
committed
Support setting the new payment metadata field in invoices
This adds support for setting the new payment metadata field in BOLT11 invoices, using a new type flag on the builder to enforce transition correctness. We allow users to set the payment metadata as either optional or required, defaulting to optional so that invoice parsing does not fail if the sender does not support payment metadata fields.
1 parent 928c9b8 commit 8ed6e64

File tree

2 files changed

+130
-26
lines changed

2 files changed

+130
-26
lines changed

lightning-invoice/src/lib.rs

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,13 @@ pub const DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA: u64 = 18;
218218
/// * `D`: exactly one [`TaggedField::Description`] or [`TaggedField::DescriptionHash`]
219219
/// * `H`: exactly one [`TaggedField::PaymentHash`]
220220
/// * `T`: the timestamp is set
221+
/// * `C`: the CLTV expiry is set
222+
/// * `S`: the payment secret is set
223+
/// * `M`: payment metadata is set
221224
///
222225
/// This is not exported to bindings users as we likely need to manually select one set of boolean type parameters.
223226
#[derive(Eq, PartialEq, Debug, Clone)]
224-
pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> {
227+
pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> {
225228
currency: Currency,
226229
amount: Option<u64>,
227230
si_prefix: Option<SiPrefix>,
@@ -234,6 +237,7 @@ pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S:
234237
phantom_t: core::marker::PhantomData<T>,
235238
phantom_c: core::marker::PhantomData<C>,
236239
phantom_s: core::marker::PhantomData<S>,
240+
phantom_m: core::marker::PhantomData<M>,
237241
}
238242

239243
/// Represents a syntactically and semantically correct lightning BOLT11 invoice.
@@ -488,7 +492,7 @@ pub mod constants {
488492
pub const TAG_FEATURES: u8 = 5;
489493
}
490494

491-
impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False, tb::False> {
495+
impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False, tb::False, tb::False> {
492496
/// Construct new, empty `InvoiceBuilder`. All necessary fields have to be filled first before
493497
/// `InvoiceBuilder::build(self)` becomes available.
494498
pub fn new(currrency: Currency) -> Self {
@@ -505,14 +509,15 @@ impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False, tb::False> {
505509
phantom_t: core::marker::PhantomData,
506510
phantom_c: core::marker::PhantomData,
507511
phantom_s: core::marker::PhantomData,
512+
phantom_m: core::marker::PhantomData,
508513
}
509514
}
510515
}
511516

512-
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, C, S> {
517+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, T, C, S, M> {
513518
/// Helper function to set the completeness flags.
514-
fn set_flags<DN: tb::Bool, HN: tb::Bool, TN: tb::Bool, CN: tb::Bool, SN: tb::Bool>(self) -> InvoiceBuilder<DN, HN, TN, CN, SN> {
515-
InvoiceBuilder::<DN, HN, TN, CN, SN> {
519+
fn set_flags<DN: tb::Bool, HN: tb::Bool, TN: tb::Bool, CN: tb::Bool, SN: tb::Bool, MN: tb::Bool>(self) -> InvoiceBuilder<DN, HN, TN, CN, SN, MN> {
520+
InvoiceBuilder::<DN, HN, TN, CN, SN, MN> {
516521
currency: self.currency,
517522
amount: self.amount,
518523
si_prefix: self.si_prefix,
@@ -525,6 +530,7 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBui
525530
phantom_t: core::marker::PhantomData,
526531
phantom_c: core::marker::PhantomData,
527532
phantom_s: core::marker::PhantomData,
533+
phantom_m: core::marker::PhantomData,
528534
}
529535
}
530536

@@ -569,7 +575,7 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBui
569575
}
570576
}
571577

572-
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, tb::True, C, S> {
578+
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, tb::True, C, S, M> {
573579
/// Builds a [`RawInvoice`] if no [`CreationError`] occurred while construction any of the
574580
/// fields.
575581
pub fn build_raw(self) -> Result<RawInvoice, CreationError> {
@@ -603,9 +609,9 @@ impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, tb
603609
}
604610
}
605611

606-
impl<H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<tb::False, H, T, C, S> {
612+
impl<H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<tb::False, H, T, C, S, M> {
607613
/// Set the description. This function is only available if no description (hash) was set.
608-
pub fn description(mut self, description: String) -> InvoiceBuilder<tb::True, H, T, C, S> {
614+
pub fn description(mut self, description: String) -> InvoiceBuilder<tb::True, H, T, C, S, M> {
609615
match Description::new(description) {
610616
Ok(d) => self.tagged_fields.push(TaggedField::Description(d)),
611617
Err(e) => self.error = Some(e),
@@ -614,13 +620,13 @@ impl<H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<tb::Fals
614620
}
615621

616622
/// Set the description hash. This function is only available if no description (hash) was set.
617-
pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder<tb::True, H, T, C, S> {
623+
pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder<tb::True, H, T, C, S, M> {
618624
self.tagged_fields.push(TaggedField::DescriptionHash(Sha256(description_hash)));
619625
self.set_flags()
620626
}
621627

622628
/// Set the description or description hash. This function is only available if no description (hash) was set.
623-
pub fn invoice_description(self, description: InvoiceDescription) -> InvoiceBuilder<tb::True, H, T, C, S> {
629+
pub fn invoice_description(self, description: InvoiceDescription) -> InvoiceBuilder<tb::True, H, T, C, S, M> {
624630
match description {
625631
InvoiceDescription::Direct(desc) => {
626632
self.description(desc.clone().into_inner())
@@ -632,18 +638,18 @@ impl<H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<tb::Fals
632638
}
633639
}
634640

635-
impl<D: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, tb::False, T, C, S> {
641+
impl<D: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<D, tb::False, T, C, S, M> {
636642
/// Set the payment hash. This function is only available if no payment hash was set.
637-
pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder<D, tb::True, T, C, S> {
643+
pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder<D, tb::True, T, C, S, M> {
638644
self.tagged_fields.push(TaggedField::PaymentHash(Sha256(hash)));
639645
self.set_flags()
640646
}
641647
}
642648

643-
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, tb::False, C, S> {
649+
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, tb::False, C, S, M> {
644650
/// Sets the timestamp to a specific [`SystemTime`].
645651
#[cfg(feature = "std")]
646-
pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder<D, H, tb::True, C, S> {
652+
pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder<D, H, tb::True, C, S, M> {
647653
match PositiveTimestamp::from_system_time(time) {
648654
Ok(t) => self.timestamp = Some(t),
649655
Err(e) => self.error = Some(e),
@@ -654,7 +660,7 @@ impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, tb
654660

655661
/// Sets the timestamp to a duration since the Unix epoch, dropping the subsecond part (which
656662
/// is not representable in BOLT 11 invoices).
657-
pub fn duration_since_epoch(mut self, time: Duration) -> InvoiceBuilder<D, H, tb::True, C, S> {
663+
pub fn duration_since_epoch(mut self, time: Duration) -> InvoiceBuilder<D, H, tb::True, C, S, M> {
658664
match PositiveTimestamp::from_duration_since_epoch(time) {
659665
Ok(t) => self.timestamp = Some(t),
660666
Err(e) => self.error = Some(e),
@@ -665,34 +671,82 @@ impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, tb
665671

666672
/// Sets the timestamp to the current system time.
667673
#[cfg(feature = "std")]
668-
pub fn current_timestamp(mut self) -> InvoiceBuilder<D, H, tb::True, C, S> {
674+
pub fn current_timestamp(mut self) -> InvoiceBuilder<D, H, tb::True, C, S, M> {
669675
let now = PositiveTimestamp::from_system_time(SystemTime::now());
670676
self.timestamp = Some(now.expect("for the foreseeable future this shouldn't happen"));
671677
self.set_flags()
672678
}
673679
}
674680

675-
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, tb::False, S> {
681+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, T, tb::False, S, M> {
676682
/// Sets `min_final_cltv_expiry_delta`.
677-
pub fn min_final_cltv_expiry_delta(mut self, min_final_cltv_expiry_delta: u64) -> InvoiceBuilder<D, H, T, tb::True, S> {
683+
pub fn min_final_cltv_expiry_delta(mut self, min_final_cltv_expiry_delta: u64) -> InvoiceBuilder<D, H, T, tb::True, S, M> {
678684
self.tagged_fields.push(TaggedField::MinFinalCltvExpiryDelta(MinFinalCltvExpiryDelta(min_final_cltv_expiry_delta)));
679685
self.set_flags()
680686
}
681687
}
682688

683-
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T, C, tb::False> {
689+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, T, C, tb::False, M> {
684690
/// Sets the payment secret and relevant features.
685-
pub fn payment_secret(mut self, payment_secret: PaymentSecret) -> InvoiceBuilder<D, H, T, C, tb::True> {
686-
let mut features = InvoiceFeatures::empty();
687-
features.set_variable_length_onion_required();
688-
features.set_payment_secret_required();
691+
pub fn payment_secret(mut self, payment_secret: PaymentSecret) -> InvoiceBuilder<D, H, T, C, tb::True, M> {
692+
let mut found_features = false;
693+
for field in self.tagged_fields.iter_mut() {
694+
if let TaggedField::Features(f) = field {
695+
found_features = true;
696+
f.set_variable_length_onion_required();
697+
f.set_payment_secret_required();
698+
}
699+
}
689700
self.tagged_fields.push(TaggedField::PaymentSecret(payment_secret));
690-
self.tagged_fields.push(TaggedField::Features(features));
701+
if !found_features {
702+
let mut features = InvoiceFeatures::empty();
703+
features.set_variable_length_onion_required();
704+
features.set_payment_secret_required();
705+
self.tagged_fields.push(TaggedField::Features(features));
706+
}
691707
self.set_flags()
692708
}
693709
}
694710

695-
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T, C, tb::True> {
711+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, C, S, tb::False> {
712+
/// Sets the payment metadata.
713+
///
714+
/// By default features are set to *optionally* allow the sender to include the payment metadata.
715+
/// If you wish to require that the sender include the metadata (and fail to parse the invoice if
716+
/// they don't support payment metadata fields), you need to call
717+
/// [`InvoiceBuilder::require_payment_metadata`] after this.
718+
pub fn payment_metadata(mut self, payment_metadata: Vec<u8>) -> InvoiceBuilder<D, H, T, C, S, tb::True> {
719+
self.tagged_fields.push(TaggedField::PaymentMetadata(payment_metadata));
720+
let mut found_features = false;
721+
for field in self.tagged_fields.iter_mut() {
722+
if let TaggedField::Features(f) = field {
723+
found_features = true;
724+
f.set_payment_metadata_optional();
725+
}
726+
}
727+
if !found_features {
728+
let mut features = InvoiceFeatures::empty();
729+
features.set_payment_metadata_optional();
730+
self.tagged_fields.push(TaggedField::Features(features));
731+
}
732+
self.set_flags()
733+
}
734+
}
735+
736+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, C, S, tb::True> {
737+
/// Sets forwarding of payment metadata as required. A reader of the invoice which does not
738+
/// support sending payment metadata will fail to read the invoice.
739+
pub fn require_payment_metadata(mut self) -> InvoiceBuilder<D, H, T, C, S, tb::True> {
740+
for field in self.tagged_fields.iter_mut() {
741+
if let TaggedField::Features(f) = field {
742+
f.set_payment_metadata_required();
743+
}
744+
}
745+
self
746+
}
747+
}
748+
749+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, T, C, tb::True, M> {
696750
/// Sets the `basic_mpp` feature as optional.
697751
pub fn basic_mpp(mut self) -> Self {
698752
for field in self.tagged_fields.iter_mut() {
@@ -704,7 +758,7 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T,
704758
}
705759
}
706760

707-
impl InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, tb::True> {
761+
impl<M: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, tb::True, M> {
708762
/// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail
709763
/// and MUST produce a recoverable signature valid for the given hash and if applicable also for
710764
/// the included payee public key.

lightning-invoice/tests/ser_de.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,56 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, bool, bool)> {
332332
true, // Different features than set in InvoiceBuilder
333333
true, // Some unknown fields
334334
),
335+
( // Older version of the payment metadata test with a payment_pubkey set
336+
"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0".to_owned(),
337+
InvoiceBuilder::new(Currency::Bitcoin)
338+
.amount_milli_satoshis(1_000_000_000)
339+
.duration_since_epoch(Duration::from_secs(1496314658))
340+
.payment_hash(sha256::Hash::from_hex(
341+
"0001020304050607080900010203040506070809000102030405060708090102"
342+
).unwrap())
343+
.description("payment metadata inside".to_owned())
344+
.payment_metadata(hex::decode("01fafaf0").unwrap())
345+
.require_payment_metadata()
346+
.payee_pub_key(PublicKey::from_slice(&hex::decode(
347+
"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"
348+
).unwrap()).unwrap())
349+
.payment_secret(PaymentSecret([0x11; 32]))
350+
.build_raw()
351+
.unwrap()
352+
.sign(|_| {
353+
RecoverableSignature::from_compact(
354+
&hex::decode("2150ed137ddb54f9736c6a0290ded709d22bddb7261d1d6518dffb467c6b1eef02afc182491bdacd00b65c83554c914a1c53c61b0a4ef04eccccdfb4365ed259").unwrap(),
355+
RecoveryId::from_i32(1).unwrap()
356+
)
357+
}).unwrap(),
358+
false, // Different features than set in InvoiceBuilder
359+
true, // Some unknown fields
360+
),
361+
(
362+
"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc".to_owned(),
363+
InvoiceBuilder::new(Currency::Bitcoin)
364+
.amount_milli_satoshis(1_000_000_000)
365+
.duration_since_epoch(Duration::from_secs(1496314658))
366+
.payment_hash(sha256::Hash::from_hex(
367+
"0001020304050607080900010203040506070809000102030405060708090102"
368+
).unwrap())
369+
.description("payment metadata inside".to_owned())
370+
.payment_metadata(hex::decode("01fafaf0").unwrap())
371+
.require_payment_metadata()
372+
.payment_secret(PaymentSecret([0x11; 32]))
373+
.build_raw()
374+
.unwrap()
375+
.sign(|_| {
376+
RecoverableSignature::from_compact(
377+
&hex::decode("f5d27be7d9c27d3aa521bc35d77cabd6bda18f1f61716445b19e27e4e17a887508ea8de5a8e1d94f561248f65434e61a221160dac1f1991b9c0f1057b269d898").unwrap(),
378+
RecoveryId::from_i32(1).unwrap()
379+
)
380+
}).unwrap(),
381+
false, // Different features than set in InvoiceBuilder
382+
true, // Some unknown fields
383+
),
384+
335385
]
336386
}
337387

0 commit comments

Comments
 (0)