Skip to content

Commit e9c6bbc

Browse files
committed
Centralize custom TLV validation behind CustomTlvs
Introduce a `CustomTlvs` wrapper to move sorting and validation of custom TLVs out of `RecipientOnionFields` and into a dedicated type. This makes TLV validity an explicit construction-time concern, allowing `RecipientOnionFields` to assume correctness and remain a simple data carrier. In turn, custom TLV usage becomes easier to extend without duplicating protocol checks.
1 parent 7fe3268 commit e9c6bbc

5 files changed

Lines changed: 79 additions & 48 deletions

File tree

lightning/src/ln/blinded_payment_tests.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use crate::ln::msgs::{
2222
};
2323
use crate::ln::onion_payment;
2424
use crate::ln::onion_utils::{self, LocalHTLCFailureReason};
25-
use crate::ln::outbound_payment::{Retry, IDEMPOTENCY_TIMEOUT_TICKS};
25+
use crate::ln::outbound_payment::{RecipientCustomTlvs, Retry, IDEMPOTENCY_TIMEOUT_TICKS};
2626
use crate::ln::types::ChannelId;
2727
use crate::offers::invoice::UnsignedBolt12Invoice;
2828
use crate::prelude::*;
@@ -1431,8 +1431,7 @@ fn custom_tlvs_to_blinded_path() {
14311431
);
14321432

14331433
let recipient_onion_fields = RecipientOnionFields::spontaneous_empty()
1434-
.with_custom_tlvs(vec![((1 << 16) + 1, vec![42, 42])])
1435-
.unwrap();
1434+
.with_custom_tlvs(RecipientCustomTlvs::new(vec![((1 << 16) + 1, vec![42, 42])]).unwrap());
14361435
nodes[0].node.send_payment(payment_hash, recipient_onion_fields.clone(),
14371436
PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap();
14381437
check_added_monitors(&nodes[0], 1);

lightning/src/ln/max_payment_path_len_tests.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ use crate::ln::msgs;
2323
use crate::ln::msgs::{BaseMessageHandler, OnionMessageHandler};
2424
use crate::ln::onion_utils;
2525
use crate::ln::onion_utils::MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY;
26-
use crate::ln::outbound_payment::{RecipientOnionFields, Retry, RetryableSendFailure};
26+
use crate::ln::outbound_payment::{
27+
RecipientCustomTlvs, RecipientOnionFields, Retry, RetryableSendFailure,
28+
};
2729
use crate::prelude::*;
2830
use crate::routing::router::{
2931
PaymentParameters, RouteParameters, DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
@@ -259,9 +261,9 @@ fn one_hop_blinded_path_with_custom_tlv() {
259261
- final_payload_len_without_custom_tlv;
260262

261263
// Check that we can send the maximum custom TLV with 1 blinded hop.
262-
let max_sized_onion = RecipientOnionFields::spontaneous_empty()
263-
.with_custom_tlvs(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])])
264-
.unwrap();
264+
let max_sized_onion = RecipientOnionFields::spontaneous_empty().with_custom_tlvs(
265+
RecipientCustomTlvs::new(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]).unwrap(),
266+
);
265267
let id = PaymentId(payment_hash.0);
266268
let no_retry = Retry::Attempts(0);
267269
nodes[1]
@@ -385,9 +387,9 @@ fn blinded_path_with_custom_tlv() {
385387
- reserved_packet_bytes_without_custom_tlv;
386388

387389
// Check that we can send the maximum custom TLV size with 0 intermediate unblinded hops.
388-
let max_sized_onion = RecipientOnionFields::spontaneous_empty()
389-
.with_custom_tlvs(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])])
390-
.unwrap();
390+
let max_sized_onion = RecipientOnionFields::spontaneous_empty().with_custom_tlvs(
391+
RecipientCustomTlvs::new(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]).unwrap(),
392+
);
391393
let no_retry = Retry::Attempts(0);
392394
let id = PaymentId(payment_hash.0);
393395
nodes[1]

lightning/src/ln/msgs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3537,7 +3537,7 @@ impl<'a> Writeable for OutboundOnionPayload<'a> {
35373537
ref invoice_request,
35383538
ref custom_tlvs,
35393539
} => {
3540-
// We need to update [`ln::outbound_payment::RecipientOnionFields::with_custom_tlvs`]
3540+
// We need to update [`ln::outbound_payments::RecipientCustomTlvs::new`]
35413541
// to reject any reserved types in the experimental range if new ones are ever
35423542
// standardized.
35433543
let invoice_request_tlv = invoice_request.map(|invreq| (77_777, invreq.encode())); // TODO: update TLV type once the async payments spec is merged

lightning/src/ln/outbound_payment.rs

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,54 @@ pub enum ProbeSendFailure {
677677
DuplicateProbe,
678678
}
679679

680+
/// A validated, sorted set of custom TLVs for payment recipient onion fields.
681+
#[derive(Clone)]
682+
pub struct RecipientCustomTlvs(Vec<(u64, Vec<u8>)>);
683+
684+
impl RecipientCustomTlvs {
685+
/// Each TLV is provided as a `(u64, Vec<u8>)` for the type number and
686+
/// serialized value respectively. TLV type numbers must be unique and
687+
/// within the range reserved for custom types, i.e. >= 2^16, otherwise
688+
/// this method will return `Err(())`.
689+
///
690+
/// This method will also error for TLV types in the experimental range
691+
/// which have since been standardized within the protocol. This currently
692+
/// includes 5482373484 (keysend) and 77_777 (invoice requests for async
693+
/// payments).
694+
pub fn new(mut tlvs: Vec<(u64, Vec<u8>)>) -> Result<Self, ()> {
695+
tlvs.sort_unstable_by_key(|(typ, _)| *typ);
696+
let mut prev_type = None;
697+
for (typ, _) in tlvs.iter() {
698+
if *typ < 1 << 16 {
699+
return Err(());
700+
}
701+
if *typ == 5482373484 {
702+
return Err(());
703+
} // keysend
704+
if *typ == 77_777 {
705+
return Err(());
706+
} // invoice requests for async payments
707+
match prev_type {
708+
Some(prev) if prev >= *typ => return Err(()),
709+
_ => {},
710+
}
711+
prev_type = Some(*typ);
712+
}
713+
714+
Ok(Self(tlvs))
715+
}
716+
717+
/// Returns the inner TLV list.
718+
pub(super) fn into_inner(self) -> Vec<(u64, Vec<u8>)> {
719+
self.0
720+
}
721+
722+
/// Borrow the inner TLV list.
723+
pub fn as_slice(&self) -> &[(u64, Vec<u8>)] {
724+
&self.0
725+
}
726+
}
727+
680728
/// Information which is provided, encrypted, to the payment recipient when sending HTLCs.
681729
///
682730
/// This should generally be constructed with data communicated to us from the recipient (via a
@@ -739,31 +787,13 @@ impl RecipientOnionFields {
739787
Self { payment_secret: None, payment_metadata: None, custom_tlvs: Vec::new() }
740788
}
741789

742-
/// Creates a new [`RecipientOnionFields`] from an existing one, adding custom TLVs. Each
743-
/// TLV is provided as a `(u64, Vec<u8>)` for the type number and serialized value
744-
/// respectively. TLV type numbers must be unique and within the range
745-
/// reserved for custom types, i.e. >= 2^16, otherwise this method will return `Err(())`.
746-
///
747-
/// This method will also error for types in the experimental range which have been
748-
/// standardized within the protocol, which only includes 5482373484 (keysend) for now.
790+
/// Creates a new [`RecipientOnionFields`] from an existing one, adding validated custom TLVs.
749791
///
750792
/// See [`Self::custom_tlvs`] for more info.
751793
#[rustfmt::skip]
752-
pub fn with_custom_tlvs(mut self, mut custom_tlvs: Vec<(u64, Vec<u8>)>) -> Result<Self, ()> {
753-
custom_tlvs.sort_unstable_by_key(|(typ, _)| *typ);
754-
let mut prev_type = None;
755-
for (typ, _) in custom_tlvs.iter() {
756-
if *typ < 1 << 16 { return Err(()); }
757-
if *typ == 5482373484 { return Err(()); } // keysend
758-
if *typ == 77_777 { return Err(()); } // invoice requests for async payments
759-
match prev_type {
760-
Some(prev) if prev >= *typ => return Err(()),
761-
_ => {},
762-
}
763-
prev_type = Some(*typ);
764-
}
765-
self.custom_tlvs = custom_tlvs;
766-
Ok(self)
794+
pub fn with_custom_tlvs(mut self, custom_tlvs: RecipientCustomTlvs) -> Self {
795+
self.custom_tlvs = custom_tlvs.into_inner();
796+
self
767797
}
768798

769799
/// Gets the custom TLVs that will be sent or have been received.
@@ -2815,8 +2845,8 @@ mod tests {
28152845
use crate::ln::channelmanager::{PaymentId, RecipientOnionFields};
28162846
use crate::ln::inbound_payment::ExpandedKey;
28172847
use crate::ln::outbound_payment::{
2818-
Bolt12PaymentError, OutboundPayments, PendingOutboundPayment, Retry, RetryableSendFailure,
2819-
StaleExpiration,
2848+
Bolt12PaymentError, OutboundPayments, PendingOutboundPayment, RecipientCustomTlvs, Retry,
2849+
RetryableSendFailure, StaleExpiration,
28202850
};
28212851
#[cfg(feature = "std")]
28222852
use crate::offers::invoice::DEFAULT_RELATIVE_EXPIRY;
@@ -2843,22 +2873,23 @@ mod tests {
28432873
fn test_recipient_onion_fields_with_custom_tlvs() {
28442874
let onion_fields = RecipientOnionFields::spontaneous_empty();
28452875

2846-
let bad_type_range_tlvs = vec![
2876+
let bad_type_range_tlvs = RecipientCustomTlvs::new(vec![
28472877
(0, vec![42]),
28482878
(1, vec![42; 32]),
2849-
];
2850-
assert!(onion_fields.clone().with_custom_tlvs(bad_type_range_tlvs).is_err());
2879+
]);
2880+
assert!(bad_type_range_tlvs.is_err());
28512881

2852-
let keysend_tlv = vec![
2882+
let keysend_tlv = RecipientCustomTlvs::new(vec![
28532883
(5482373484, vec![42; 32]),
2854-
];
2855-
assert!(onion_fields.clone().with_custom_tlvs(keysend_tlv).is_err());
2884+
]);
2885+
assert!(keysend_tlv.is_err());
28562886

2857-
let good_tlvs = vec![
2887+
let good_tlvs = RecipientCustomTlvs::new(vec![
28582888
((1 << 16) + 1, vec![42]),
28592889
((1 << 16) + 3, vec![42; 32]),
2860-
];
2861-
assert!(onion_fields.with_custom_tlvs(good_tlvs).is_ok());
2890+
]);
2891+
assert!(good_tlvs.is_ok());
2892+
onion_fields.with_custom_tlvs(good_tlvs.unwrap());
28622893
}
28632894

28642895
#[test]

lightning/src/ln/payment_tests.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use crate::ln::msgs;
3232
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent};
3333
use crate::ln::onion_utils::{self, LocalHTLCFailureReason};
3434
use crate::ln::outbound_payment::{
35-
ProbeSendFailure, Retry, RetryableSendFailure, IDEMPOTENCY_TIMEOUT_TICKS,
35+
ProbeSendFailure, RecipientCustomTlvs, Retry, RetryableSendFailure, IDEMPOTENCY_TIMEOUT_TICKS,
3636
};
3737
use crate::ln::types::ChannelId;
3838
use crate::routing::gossip::{EffectiveCapacity, RoutingFees};
@@ -4539,7 +4539,7 @@ fn test_retry_custom_tlvs() {
45394539

45404540
let custom_tlvs = vec![((1 << 16) + 1, vec![0x42u8; 16])];
45414541
let onion = RecipientOnionFields::secret_only(payment_secret);
4542-
let onion = onion.with_custom_tlvs(custom_tlvs.clone()).unwrap();
4542+
let onion = onion.with_custom_tlvs(RecipientCustomTlvs::new(custom_tlvs.clone()).unwrap());
45434543

45444544
nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone()));
45454545
nodes[0].node.send_payment(hash, onion, id, route_params.clone(), Retry::Attempts(1)).unwrap();
@@ -5079,8 +5079,7 @@ fn peel_payment_onion_custom_tlvs() {
50795079
let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat);
50805080
let route = functional_test_utils::get_route(&nodes[0], &route_params).unwrap();
50815081
let mut recipient_onion = RecipientOnionFields::spontaneous_empty()
5082-
.with_custom_tlvs(vec![(414141, vec![42; 1200])])
5083-
.unwrap();
5082+
.with_custom_tlvs(RecipientCustomTlvs::new(vec![(414141, vec![42; 1200])]).unwrap());
50845083
let prng_seed = chanmon_cfgs[0].keys_manager.get_secure_random_bytes();
50855084
let session_priv = SecretKey::from_slice(&prng_seed[..]).expect("RNG is busted");
50865085
let keysend_preimage = PaymentPreimage([42; 32]);

0 commit comments

Comments
 (0)