Skip to content

Support sending PaymentMetadata in HTLCs #2127

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 15 commits into from
Apr 19, 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
2 changes: 2 additions & 0 deletions lightning-invoice/src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,8 @@ impl FromBase32 for TaggedField {
Ok(TaggedField::PrivateRoute(PrivateRoute::from_base32(field_data)?)),
constants::TAG_PAYMENT_SECRET =>
Ok(TaggedField::PaymentSecret(PaymentSecret::from_base32(field_data)?)),
constants::TAG_PAYMENT_METADATA =>
Ok(TaggedField::PaymentMetadata(Vec::<u8>::from_base32(field_data)?)),
constants::TAG_FEATURES =>
Ok(TaggedField::Features(InvoiceFeatures::from_base32(field_data)?)),
_ => {
Expand Down
122 changes: 94 additions & 28 deletions lightning-invoice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Woah, this is the first time I'm really looking at this part of the repo, this is so cool. Just to make sure I'm getting things right, these type parameters are here to basically conditionally implement/expose functions to the user based on the state of the invoice (what fields have been added) (such that a user's error will be caught at compile time), and then the PhantomData is there to just use the types because rust doesn't allow unused type params?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep!

currency: Currency,
amount: Option<u64>,
si_prefix: Option<SiPrefix>,
Expand All @@ -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.
Expand Down Expand Up @@ -419,6 +423,7 @@ pub enum TaggedField {
Fallback(Fallback),
PrivateRoute(PrivateRoute),
PaymentSecret(PaymentSecret),
PaymentMetadata(Vec<u8>),
Features(InvoiceFeatures),
}

Expand Down Expand Up @@ -483,15 +488,16 @@ pub mod constants {
pub const TAG_FALLBACK: u8 = 9;
pub const TAG_PRIVATE_ROUTE: u8 = 3;
pub const TAG_PAYMENT_SECRET: u8 = 16;
pub const TAG_PAYMENT_METADATA: u8 = 27;
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 {
pub fn new(currency: Currency) -> Self {
InvoiceBuilder {
currency: currrency,
currency,
amount: None,
si_prefix: None,
timestamp: None,
Expand All @@ -503,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,
Expand All @@ -523,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,
}
}

Expand Down Expand Up @@ -567,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> {
Expand Down Expand Up @@ -601,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),
Expand All @@ -612,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())
Expand All @@ -630,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),
Expand All @@ -652,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),
Expand All @@ -663,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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// If you wish to require that the sender include the metadata (and fail to parse the invoice if
/// If you wish to require that senders include the metadata (and fail to parse the invoice if

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But there's only one sender?

Copy link
Contributor

Choose a reason for hiding this comment

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

Alright, then it should probably be "...that the sender includes the metadata (and fails 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));
Copy link
Contributor

@tnull tnull Apr 19, 2023

Choose a reason for hiding this comment

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

In a follow-up we might want to look into: Do we need/want to limit the size of the payment metadata field? How can we communicate the influence it might have on the maximum path length?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We'll need to rework all that code for blinded paths too, indeed let's do it in a followup - #2201

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() {
Expand All @@ -702,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.
Expand Down Expand Up @@ -954,6 +1010,10 @@ impl RawInvoice {
find_extract!(self.known_tagged_fields(), TaggedField::PaymentSecret(ref x), x)
}

pub fn payment_metadata(&self) -> Option<&Vec<u8>> {
find_extract!(self.known_tagged_fields(), TaggedField::PaymentMetadata(ref x), x)
}

pub fn features(&self) -> Option<&InvoiceFeatures> {
find_extract!(self.known_tagged_fields(), TaggedField::Features(ref x), x)
}
Expand Down Expand Up @@ -1225,6 +1285,11 @@ impl Invoice {
self.signed_invoice.payment_secret().expect("was checked by constructor")
}

/// Get the payment metadata blob if one was included in the invoice
pub fn payment_metadata(&self) -> Option<&Vec<u8>> {
self.signed_invoice.payment_metadata()
}

/// Get the invoice features if they were included in the invoice
pub fn features(&self) -> Option<&InvoiceFeatures> {
self.signed_invoice.features()
Expand Down Expand Up @@ -1374,6 +1439,7 @@ impl TaggedField {
TaggedField::Fallback(_) => constants::TAG_FALLBACK,
TaggedField::PrivateRoute(_) => constants::TAG_PRIVATE_ROUTE,
TaggedField::PaymentSecret(_) => constants::TAG_PAYMENT_SECRET,
TaggedField::PaymentMetadata(_) => constants::TAG_PAYMENT_METADATA,
TaggedField::Features(_) => constants::TAG_FEATURES,
};

Expand Down
56 changes: 54 additions & 2 deletions lightning-invoice/src/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,10 @@ fn pay_invoice_using_amount<P: Deref>(
payer: P
) -> Result<(), PaymentError> where P::Target: Payer {
let payment_hash = PaymentHash((*invoice.payment_hash()).into_inner());
let payment_secret = Some(*invoice.payment_secret());
let recipient_onion = RecipientOnionFields { payment_secret };
let recipient_onion = RecipientOnionFields {
payment_secret: Some(*invoice.payment_secret()),
payment_metadata: invoice.payment_metadata().map(|v| v.clone()),
};
let mut payment_params = PaymentParameters::from_node_id(invoice.recover_payee_pub_key(),
invoice.min_final_cltv_expiry_delta() as u32)
.with_expiry_time(expiry_time_from_unix_epoch(invoice).as_secs())
Expand Down Expand Up @@ -213,6 +215,8 @@ mod tests {
use super::*;
use crate::{InvoiceBuilder, Currency};
use bitcoin_hashes::sha256::Hash as Sha256;
use lightning::events::Event;
use lightning::ln::msgs::ChannelMessageHandler;
use lightning::ln::{PaymentPreimage, PaymentSecret};
use lightning::ln::functional_test_utils::*;
use secp256k1::{SecretKey, Secp256k1};
Expand Down Expand Up @@ -350,4 +354,52 @@ mod tests {
_ => panic!()
}
}

#[test]
#[cfg(feature = "std")]
fn payment_metadata_end_to_end() {
// Test that a payment metadata read from an invoice passed to `pay_invoice` makes it all
// the way out through the `PaymentClaimable` event.
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
create_announced_chan_between_nodes(&nodes, 0, 1);

let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42];

let (payment_hash, payment_secret) =
nodes[1].node.create_inbound_payment(None, 7200, None).unwrap();

let invoice = InvoiceBuilder::new(Currency::Bitcoin)
.description("test".into())
.payment_hash(Sha256::from_slice(&payment_hash.0).unwrap())
.payment_secret(payment_secret)
.current_timestamp()
.min_final_cltv_expiry_delta(144)
.amount_milli_satoshis(50_000)
.payment_metadata(payment_metadata.clone())
.build_signed(|hash| {
Secp256k1::new().sign_ecdsa_recoverable(hash,
&nodes[1].keys_manager.backing.get_node_secret_key())
})
.unwrap();

pay_invoice(&invoice, Retry::Attempts(0), nodes[0].node).unwrap();
check_added_monitors(&nodes[0], 1);
let send_event = SendEvent::from_node(&nodes[0]);
nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &send_event.msgs[0]);
commitment_signed_dance!(nodes[1], nodes[0], &send_event.commitment_msg, false);

expect_pending_htlcs_forwardable!(nodes[1]);

let mut events = nodes[1].node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
match events.pop().unwrap() {
Event::PaymentClaimable { onion_fields, .. } => {
assert_eq!(Some(payment_metadata), onion_fields.unwrap().payment_metadata);
},
_ => panic!("Unexpected event")
}
}
}
3 changes: 3 additions & 0 deletions lightning-invoice/src/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ impl ToBase32 for TaggedField {
TaggedField::PaymentSecret(ref payment_secret) => {
write_tagged_field(writer, constants::TAG_PAYMENT_SECRET, payment_secret)
},
TaggedField::PaymentMetadata(ref payment_metadata) => {
write_tagged_field(writer, constants::TAG_PAYMENT_METADATA, payment_metadata)
},
TaggedField::Features(ref features) => {
write_tagged_field(writer, constants::TAG_FEATURES, features)
},
Expand Down
Loading