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.
This commit is contained in:
Matt Corallo 2021-12-21 06:03:07 +00:00
parent 928c9b806e
commit 8ed6e64913
2 changed files with 130 additions and 26 deletions

View file

@ -218,10 +218,13 @@ pub const DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA: u64 = 18;
/// * `D`: exactly one [`TaggedField::Description`] or [`TaggedField::DescriptionHash`]
/// * `H`: exactly one [`TaggedField::PaymentHash`]
/// * `T`: the timestamp is set
/// * `C`: the CLTV expiry is set
/// * `S`: the payment secret is set
/// * `M`: payment metadata is set
///
/// This is not exported to bindings users as we likely need to manually select one set of boolean type parameters.
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> {
pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> {
currency: Currency,
amount: Option<u64>,
si_prefix: Option<SiPrefix>,
@ -234,6 +237,7 @@ pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S:
phantom_t: core::marker::PhantomData<T>,
phantom_c: core::marker::PhantomData<C>,
phantom_s: core::marker::PhantomData<S>,
phantom_m: core::marker::PhantomData<M>,
}
/// Represents a syntactically and semantically correct lightning BOLT11 invoice.
@ -488,7 +492,7 @@ pub mod constants {
pub const TAG_FEATURES: u8 = 5;
}
impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False, tb::False> {
impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False, tb::False, tb::False> {
/// Construct new, empty `InvoiceBuilder`. All necessary fields have to be filled first before
/// `InvoiceBuilder::build(self)` becomes available.
pub fn new(currrency: Currency) -> Self {
@ -505,14 +509,15 @@ impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False, tb::False> {
phantom_t: core::marker::PhantomData,
phantom_c: core::marker::PhantomData,
phantom_s: core::marker::PhantomData,
phantom_m: core::marker::PhantomData,
}
}
}
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, C, S> {
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> {
/// Helper function to set the completeness flags.
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> {
InvoiceBuilder::<DN, HN, TN, CN, SN> {
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> {
InvoiceBuilder::<DN, HN, TN, CN, SN, MN> {
currency: self.currency,
amount: self.amount,
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
phantom_t: core::marker::PhantomData,
phantom_c: core::marker::PhantomData,
phantom_s: core::marker::PhantomData,
phantom_m: core::marker::PhantomData,
}
}
@ -569,7 +575,7 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBui
}
}
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, tb::True, C, S> {
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, tb::True, C, S, M> {
/// Builds a [`RawInvoice`] if no [`CreationError`] occurred while construction any of the
/// fields.
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
}
}
impl<H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<tb::False, H, T, C, S> {
impl<H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<tb::False, H, T, C, S, M> {
/// Set the description. This function is only available if no description (hash) was set.
pub fn description(mut self, description: String) -> InvoiceBuilder<tb::True, H, T, C, S> {
pub fn description(mut self, description: String) -> InvoiceBuilder<tb::True, H, T, C, S, M> {
match Description::new(description) {
Ok(d) => self.tagged_fields.push(TaggedField::Description(d)),
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
}
/// Set the description hash. This function is only available if no description (hash) was set.
pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder<tb::True, H, T, C, S> {
pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder<tb::True, H, T, C, S, M> {
self.tagged_fields.push(TaggedField::DescriptionHash(Sha256(description_hash)));
self.set_flags()
}
/// Set the description or description hash. This function is only available if no description (hash) was set.
pub fn invoice_description(self, description: InvoiceDescription) -> InvoiceBuilder<tb::True, H, T, C, S> {
pub fn invoice_description(self, description: InvoiceDescription) -> InvoiceBuilder<tb::True, H, T, C, S, M> {
match description {
InvoiceDescription::Direct(desc) => {
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
}
}
impl<D: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, tb::False, T, C, S> {
impl<D: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<D, tb::False, T, C, S, M> {
/// Set the payment hash. This function is only available if no payment hash was set.
pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder<D, tb::True, T, C, S> {
pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder<D, tb::True, T, C, S, M> {
self.tagged_fields.push(TaggedField::PaymentHash(Sha256(hash)));
self.set_flags()
}
}
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, tb::False, C, S> {
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, tb::False, C, S, M> {
/// Sets the timestamp to a specific [`SystemTime`].
#[cfg(feature = "std")]
pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder<D, H, tb::True, C, S> {
pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder<D, H, tb::True, C, S, M> {
match PositiveTimestamp::from_system_time(time) {
Ok(t) => self.timestamp = Some(t),
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
/// Sets the timestamp to a duration since the Unix epoch, dropping the subsecond part (which
/// is not representable in BOLT 11 invoices).
pub fn duration_since_epoch(mut self, time: Duration) -> InvoiceBuilder<D, H, tb::True, C, S> {
pub fn duration_since_epoch(mut self, time: Duration) -> InvoiceBuilder<D, H, tb::True, C, S, M> {
match PositiveTimestamp::from_duration_since_epoch(time) {
Ok(t) => self.timestamp = Some(t),
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
/// Sets the timestamp to the current system time.
#[cfg(feature = "std")]
pub fn current_timestamp(mut self) -> InvoiceBuilder<D, H, tb::True, C, S> {
pub fn current_timestamp(mut self) -> InvoiceBuilder<D, H, tb::True, C, S, M> {
let now = PositiveTimestamp::from_system_time(SystemTime::now());
self.timestamp = Some(now.expect("for the foreseeable future this shouldn't happen"));
self.set_flags()
}
}
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, tb::False, S> {
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, S: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, T, tb::False, S, M> {
/// Sets `min_final_cltv_expiry_delta`.
pub fn min_final_cltv_expiry_delta(mut self, min_final_cltv_expiry_delta: u64) -> InvoiceBuilder<D, H, T, tb::True, S> {
pub fn min_final_cltv_expiry_delta(mut self, min_final_cltv_expiry_delta: u64) -> InvoiceBuilder<D, H, T, tb::True, S, M> {
self.tagged_fields.push(TaggedField::MinFinalCltvExpiryDelta(MinFinalCltvExpiryDelta(min_final_cltv_expiry_delta)));
self.set_flags()
}
}
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T, C, tb::False> {
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, T, C, tb::False, M> {
/// Sets the payment secret and relevant features.
pub fn payment_secret(mut self, payment_secret: PaymentSecret) -> InvoiceBuilder<D, H, T, C, tb::True> {
let mut features = InvoiceFeatures::empty();
features.set_variable_length_onion_required();
features.set_payment_secret_required();
pub fn payment_secret(mut self, payment_secret: PaymentSecret) -> InvoiceBuilder<D, H, T, C, tb::True, M> {
let mut found_features = false;
for field in self.tagged_fields.iter_mut() {
if let TaggedField::Features(f) = field {
found_features = true;
f.set_variable_length_onion_required();
f.set_payment_secret_required();
}
}
self.tagged_fields.push(TaggedField::PaymentSecret(payment_secret));
self.tagged_fields.push(TaggedField::Features(features));
if !found_features {
let mut features = InvoiceFeatures::empty();
features.set_variable_length_onion_required();
features.set_payment_secret_required();
self.tagged_fields.push(TaggedField::Features(features));
}
self.set_flags()
}
}
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T, C, tb::True> {
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, C, S, tb::False> {
/// Sets the payment metadata.
///
/// By default features are set to *optionally* allow the sender to include the payment metadata.
/// If you wish to require that the sender include the metadata (and fail to parse the invoice if
/// they don't support payment metadata fields), you need to call
/// [`InvoiceBuilder::require_payment_metadata`] after this.
pub fn payment_metadata(mut self, payment_metadata: Vec<u8>) -> InvoiceBuilder<D, H, T, C, S, tb::True> {
self.tagged_fields.push(TaggedField::PaymentMetadata(payment_metadata));
let mut found_features = false;
for field in self.tagged_fields.iter_mut() {
if let TaggedField::Features(f) = field {
found_features = true;
f.set_payment_metadata_optional();
}
}
if !found_features {
let mut features = InvoiceFeatures::empty();
features.set_payment_metadata_optional();
self.tagged_fields.push(TaggedField::Features(features));
}
self.set_flags()
}
}
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, C, S, tb::True> {
/// Sets forwarding of payment metadata as required. A reader of the invoice which does not
/// support sending payment metadata will fail to read the invoice.
pub fn require_payment_metadata(mut self) -> InvoiceBuilder<D, H, T, C, S, tb::True> {
for field in self.tagged_fields.iter_mut() {
if let TaggedField::Features(f) = field {
f.set_payment_metadata_required();
}
}
self
}
}
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, M: tb::Bool> InvoiceBuilder<D, H, T, C, tb::True, M> {
/// Sets the `basic_mpp` feature as optional.
pub fn basic_mpp(mut self) -> Self {
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,
}
}
impl InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, tb::True> {
impl<M: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, tb::True, M> {
/// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail
/// and MUST produce a recoverable signature valid for the given hash and if applicable also for
/// the included payee public key.

View file

@ -332,6 +332,56 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, bool, bool)> {
true, // Different features than set in InvoiceBuilder
true, // Some unknown fields
),
( // Older version of the payment metadata test with a payment_pubkey set
"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0".to_owned(),
InvoiceBuilder::new(Currency::Bitcoin)
.amount_milli_satoshis(1_000_000_000)
.duration_since_epoch(Duration::from_secs(1496314658))
.payment_hash(sha256::Hash::from_hex(
"0001020304050607080900010203040506070809000102030405060708090102"
).unwrap())
.description("payment metadata inside".to_owned())
.payment_metadata(hex::decode("01fafaf0").unwrap())
.require_payment_metadata()
.payee_pub_key(PublicKey::from_slice(&hex::decode(
"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"
).unwrap()).unwrap())
.payment_secret(PaymentSecret([0x11; 32]))
.build_raw()
.unwrap()
.sign(|_| {
RecoverableSignature::from_compact(
&hex::decode("2150ed137ddb54f9736c6a0290ded709d22bddb7261d1d6518dffb467c6b1eef02afc182491bdacd00b65c83554c914a1c53c61b0a4ef04eccccdfb4365ed259").unwrap(),
RecoveryId::from_i32(1).unwrap()
)
}).unwrap(),
false, // Different features than set in InvoiceBuilder
true, // Some unknown fields
),
(
"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc".to_owned(),
InvoiceBuilder::new(Currency::Bitcoin)
.amount_milli_satoshis(1_000_000_000)
.duration_since_epoch(Duration::from_secs(1496314658))
.payment_hash(sha256::Hash::from_hex(
"0001020304050607080900010203040506070809000102030405060708090102"
).unwrap())
.description("payment metadata inside".to_owned())
.payment_metadata(hex::decode("01fafaf0").unwrap())
.require_payment_metadata()
.payment_secret(PaymentSecret([0x11; 32]))
.build_raw()
.unwrap()
.sign(|_| {
RecoverableSignature::from_compact(
&hex::decode("f5d27be7d9c27d3aa521bc35d77cabd6bda18f1f61716445b19e27e4e17a887508ea8de5a8e1d94f561248f65434e61a221160dac1f1991b9c0f1057b269d898").unwrap(),
RecoveryId::from_i32(1).unwrap()
)
}).unwrap(),
false, // Different features than set in InvoiceBuilder
true, // Some unknown fields
),
]
}