Allow users to provide custom TLVs through `RecipientOnionFields`
authorAlec Chen <alecchendev@gmail.com>
Tue, 16 May 2023 22:56:28 +0000 (17:56 -0500)
committerAlec Chen <alecchendev@gmail.com>
Tue, 8 Aug 2023 20:55:00 +0000 (15:55 -0500)
Custom TLVs allow users to send extra application-specific data with
a payment. These have the additional flexibility compared to
`payment_metadata` that they don't have to reflect recipient generated
data provided in an invoice, in which `payment_metadata` could be
reused.

We ensure provided type numbers are unique, increasing, and within the
experimental range with the `RecipientOnionFields::with_custom_tlvs`
method.

This begins sender-side support for custom TLVs.

lightning-invoice/src/payment.rs
lightning/src/ln/channelmanager.rs
lightning/src/ln/outbound_payment.rs
lightning/src/ln/payment_tests.rs

index 42408540ee41e7a9e1f9d8ba6c711c6def7b565d..b67bac13f3447f2061782266a52812fee0cfcac5 100644 (file)
@@ -146,10 +146,8 @@ 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 recipient_onion = RecipientOnionFields {
-               payment_secret: Some(*invoice.payment_secret()),
-               payment_metadata: invoice.payment_metadata().map(|v| v.clone()),
-       };
+       let mut recipient_onion = RecipientOnionFields::secret_only(*invoice.payment_secret());
+       recipient_onion.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())
index 3757b06b3c94dc3b5f2bfca20d6096796980f9a1..374368e83e592955a76a68fc9f28d6de81b10001 100644 (file)
@@ -3943,15 +3943,16 @@ where
                                                                let (cltv_expiry, onion_payload, payment_data, phantom_shared_secret, mut onion_fields) = match routing {
                                                                        PendingHTLCRouting::Receive { payment_data, payment_metadata, incoming_cltv_expiry, phantom_shared_secret } => {
                                                                                let _legacy_hop_data = Some(payment_data.clone());
-                                                                               let onion_fields =
-                                                                                       RecipientOnionFields { payment_secret: Some(payment_data.payment_secret), payment_metadata };
+                                                                               let onion_fields = RecipientOnionFields { payment_secret: Some(payment_data.payment_secret),
+                                                                                               payment_metadata, custom_tlvs: vec![] };
                                                                                (incoming_cltv_expiry, OnionPayload::Invoice { _legacy_hop_data },
                                                                                        Some(payment_data), phantom_shared_secret, onion_fields)
                                                                        },
                                                                        PendingHTLCRouting::ReceiveKeysend { payment_data, payment_preimage, payment_metadata, incoming_cltv_expiry } => {
                                                                                let onion_fields = RecipientOnionFields {
                                                                                        payment_secret: payment_data.as_ref().map(|data| data.payment_secret),
-                                                                                       payment_metadata
+                                                                                       payment_metadata,
+                                                                                       custom_tlvs: vec![],
                                                                                };
                                                                                (incoming_cltv_expiry, OnionPayload::Spontaneous(payment_preimage),
                                                                                        payment_data, None, onion_fields)
index 24d683e5fdc687563a7b1a84064777239e3a6fbd..45c723b852544c37af1a7eafb8f404914cf62c77 100644 (file)
@@ -431,10 +431,13 @@ pub struct RecipientOnionFields {
        /// [`Self::payment_secret`] and while nearly all lightning senders support secrets, metadata
        /// may not be supported as universally.
        pub payment_metadata: Option<Vec<u8>>,
+       /// See [`Self::custom_tlvs`] for more info.
+       pub(super) custom_tlvs: Vec<(u64, Vec<u8>)>,
 }
 
 impl_writeable_tlv_based!(RecipientOnionFields, {
        (0, payment_secret, option),
+       (1, custom_tlvs, optional_vec),
        (2, payment_metadata, option),
 });
 
@@ -443,7 +446,7 @@ impl RecipientOnionFields {
        /// set of onion fields for today's BOLT11 invoices - most nodes require a [`PaymentSecret`]
        /// but do not require or provide any further data.
        pub fn secret_only(payment_secret: PaymentSecret) -> Self {
-               Self { payment_secret: Some(payment_secret), payment_metadata: None }
+               Self { payment_secret: Some(payment_secret), payment_metadata: None, custom_tlvs: Vec::new() }
        }
 
        /// Creates a new [`RecipientOnionFields`] with no fields. This generally does not create
@@ -455,7 +458,46 @@ impl RecipientOnionFields {
        /// [`ChannelManager::send_spontaneous_payment`]: super::channelmanager::ChannelManager::send_spontaneous_payment
        /// [`RecipientOnionFields::secret_only`]: RecipientOnionFields::secret_only
        pub fn spontaneous_empty() -> Self {
-               Self { payment_secret: None, payment_metadata: None }
+               Self { payment_secret: None, payment_metadata: None, custom_tlvs: Vec::new() }
+       }
+
+       /// Creates a new [`RecipientOnionFields`] from an existing one, adding custom TLVs. Each
+       /// TLV is provided as a `(u64, Vec<u8>)` for the type number and serialized value
+       /// respectively. TLV type numbers must be unique and within the range
+       /// reserved for custom types, i.e. >= 2^16, otherwise this method will return `Err(())`.
+       ///
+       /// This method will also error for types in the experimental range which have been
+       /// standardized within the protocol, which only includes 5482373484 (keysend) for now.
+       ///
+       /// See [`Self::custom_tlvs`] for more info.
+       pub fn with_custom_tlvs(mut self, mut custom_tlvs: Vec<(u64, Vec<u8>)>) -> Result<Self, ()> {
+               custom_tlvs.sort_unstable_by_key(|(typ, _)| *typ);
+               let mut prev_type = None;
+               for (typ, _) in custom_tlvs.iter() {
+                       if *typ < 1 << 16 { return Err(()); }
+                       if *typ == 5482373484 { return Err(()); } // keysend
+                       match prev_type {
+                               Some(prev) if prev >= *typ => return Err(()),
+                               _ => {},
+                       }
+                       prev_type = Some(*typ);
+               }
+               self.custom_tlvs = custom_tlvs;
+               Ok(self)
+       }
+
+       /// Gets the custom TLVs that will be sent or have been received.
+       ///
+       /// Custom TLVs allow sending extra application-specific data with a payment. They provide
+       /// additional flexibility on top of payment metadata, as while other implementations may
+       /// require `payment_metadata` to reflect metadata provided in an invoice, custom TLVs
+       /// do not have this restriction.
+       ///
+       /// Note that if this field is non-empty, it will contain strictly increasing TLVs, each
+       /// represented by a `(u64, Vec<u8>)` for its type number and serialized value respectively.
+       /// This is validated when setting this field using [`Self::with_custom_tlvs`].
+       pub fn custom_tlvs(&self) -> &Vec<(u64, Vec<u8>)> {
+               &self.custom_tlvs
        }
 
        /// When we have received some HTLC(s) towards an MPP payment, as we receive further HTLC(s) we
@@ -773,6 +815,7 @@ impl OutboundPayments {
                                                        (*total_msat, RecipientOnionFields {
                                                                        payment_secret: *payment_secret,
                                                                        payment_metadata: payment_metadata.clone(),
+                                                                       custom_tlvs: Vec::new(),
                                                                }, *keysend_preimage)
                                                },
                                                PendingOutboundPayment::Legacy { .. } => {
@@ -1450,6 +1493,28 @@ mod tests {
 
        use alloc::collections::VecDeque;
 
+       #[test]
+       fn test_recipient_onion_fields_with_custom_tlvs() {
+               let onion_fields = RecipientOnionFields::spontaneous_empty();
+
+               let bad_type_range_tlvs = vec![
+                       (0, vec![42]),
+                       (1, vec![42; 32]),
+               ];
+               assert!(onion_fields.clone().with_custom_tlvs(bad_type_range_tlvs).is_err());
+
+               let keysend_tlv = vec![
+                       (5482373484, vec![42; 32]),
+               ];
+               assert!(onion_fields.clone().with_custom_tlvs(keysend_tlv).is_err());
+
+               let good_tlvs = vec![
+                       ((1 << 16) + 1, vec![42]),
+                       ((1 << 16) + 3, vec![42; 32]),
+               ];
+               assert!(onion_fields.with_custom_tlvs(good_tlvs).is_ok());
+       }
+
        #[test]
        #[cfg(feature = "std")]
        fn fails_paying_after_expiration() {
index 7d639611df2873950af4f2bd0b5dfa7b7f7123c8..26ece931770f81202284455c4775d2cf9bf52693 100644 (file)
@@ -3424,7 +3424,7 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) {
 
        // Send the MPP payment, delivering the updated commitment state to nodes[1].
        nodes[0].node.send_payment(payment_hash, RecipientOnionFields {
-                       payment_secret: Some(payment_secret), payment_metadata: Some(payment_metadata),
+                       payment_secret: Some(payment_secret), payment_metadata: Some(payment_metadata), custom_tlvs: vec![],
                }, payment_id, route_params.clone(), Retry::Attempts(1)).unwrap();
        check_added_monitors!(nodes[0], 2);