Skip to content

Commit 7a3e06b

Browse files
committed
Include PaymentId in payer metadata
When receiving a BOLT 12 invoice originating from either an invoice request or a refund, the invoice should only be paid once. To accomplish this, require that the invoice includes an encrypted payment id in the payer metadata. This allows ChannelManager to track a payment when requesting but prior to receiving the invoice. Thus, it can determine if the invoice has already been paid.
1 parent 861e0ee commit 7a3e06b

File tree

7 files changed

+202
-54
lines changed

7 files changed

+202
-54
lines changed

lightning/src/ln/channelmanager.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,12 @@ impl From<&ClaimableHTLC> for events::ClaimedHTLC {
237237
///
238238
/// This is not exported to bindings users as we just use [u8; 32] directly
239239
#[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)]
240-
pub struct PaymentId(pub [u8; 32]);
240+
pub struct PaymentId(pub [u8; Self::LENGTH]);
241+
242+
impl PaymentId {
243+
/// Number of bytes in the id.
244+
pub const LENGTH: usize = 32;
245+
}
241246

242247
impl Writeable for PaymentId {
243248
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {

lightning/src/ln/inbound_payment.rs

+7
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ impl ExpandedKey {
8686
hmac.input(&nonce.0);
8787
hmac
8888
}
89+
90+
/// Encrypts or decrypts the given `bytes`. Used for data included in an offer message's
91+
/// metadata (e.g., payment id).
92+
pub(crate) fn crypt_for_offer(&self, mut bytes: [u8; 32], nonce: Nonce) -> [u8; 32] {
93+
ChaCha20::encrypt_single_block_in_place(&self.offers_encryption_key, &nonce.0, &mut bytes);
94+
bytes
95+
}
8996
}
9097

9198
/// A 128-bit number used only once.

lightning/src/offers/invoice.rs

+6-7
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ use core::time::Duration;
110110
use crate::io;
111111
use crate::blinded_path::BlindedPath;
112112
use crate::ln::PaymentHash;
113+
use crate::ln::channelmanager::PaymentId;
113114
use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures};
114115
use crate::ln::inbound_payment::ExpandedKey;
115116
use crate::ln::msgs::DecodeError;
@@ -695,10 +696,11 @@ impl Bolt12Invoice {
695696
merkle::message_digest(SIGNATURE_TAG, &self.bytes).as_ref().clone()
696697
}
697698

698-
/// Verifies that the invoice was for a request or refund created using the given key.
699+
/// Verifies that the invoice was for a request or refund created using the given key. Returns
700+
/// the associated [`PaymentId`] to use when sending the payment.
699701
pub fn verify<T: secp256k1::Signing>(
700702
&self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
701-
) -> bool {
703+
) -> Result<PaymentId, ()> {
702704
self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx)
703705
}
704706

@@ -947,7 +949,7 @@ impl InvoiceContents {
947949

948950
fn verify<T: secp256k1::Signing>(
949951
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
950-
) -> bool {
952+
) -> Result<PaymentId, ()> {
951953
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
952954
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
953955
match record.r#type {
@@ -967,10 +969,7 @@ impl InvoiceContents {
967969
},
968970
};
969971

970-
match signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) {
971-
Ok(_) => true,
972-
Err(()) => false,
973-
}
972+
signer::verify_payer_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx)
974973
}
975974

976975
fn derives_keys(&self) -> bool {

lightning/src/offers/invoice_request.rs

+28-14
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ use crate::sign::EntropySource;
6464
use crate::io;
6565
use crate::blinded_path::BlindedPath;
6666
use crate::ln::PaymentHash;
67+
use crate::ln::channelmanager::PaymentId;
6768
use crate::ln::features::InvoiceRequestFeatures;
6869
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
6970
use crate::ln::msgs::DecodeError;
@@ -128,10 +129,12 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerI
128129
}
129130

130131
pub(super) fn deriving_metadata<ES: Deref>(
131-
offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES
132+
offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
133+
payment_id: PaymentId,
132134
) -> Self where ES::Target: EntropySource {
133135
let nonce = Nonce::from_entropy_source(entropy_source);
134-
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
136+
let payment_id = Some(payment_id);
137+
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id);
135138
let metadata = Metadata::Derived(derivation_material);
136139
Self {
137140
offer,
@@ -145,10 +148,12 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerI
145148

146149
impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T> {
147150
pub(super) fn deriving_payer_id<ES: Deref>(
148-
offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>
151+
offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES,
152+
secp_ctx: &'b Secp256k1<T>, payment_id: PaymentId
149153
) -> Self where ES::Target: EntropySource {
150154
let nonce = Nonce::from_entropy_source(entropy_source);
151-
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
155+
let payment_id = Some(payment_id);
156+
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id);
152157
let metadata = Metadata::DerivedSigningPubkey(derivation_material);
153158
Self {
154159
offer,
@@ -259,7 +264,7 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a
259264
let mut tlv_stream = self.invoice_request.as_tlv_stream();
260265
debug_assert!(tlv_stream.2.payer_id.is_none());
261266
tlv_stream.0.metadata = None;
262-
if !metadata.derives_keys() {
267+
if !metadata.derives_payer_keys() {
263268
tlv_stream.2.payer_id = self.payer_id.as_ref();
264269
}
265270

@@ -691,7 +696,7 @@ impl InvoiceRequestContents {
691696
}
692697

693698
pub(super) fn derives_keys(&self) -> bool {
694-
self.inner.payer.0.derives_keys()
699+
self.inner.payer.0.derives_payer_keys()
695700
}
696701

697702
pub(super) fn chain(&self) -> ChainHash {
@@ -924,6 +929,7 @@ mod tests {
924929
#[cfg(feature = "std")]
925930
use core::time::Duration;
926931
use crate::sign::KeyMaterial;
932+
use crate::ln::channelmanager::PaymentId;
927933
use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures};
928934
use crate::ln::inbound_payment::ExpandedKey;
929935
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
@@ -1069,12 +1075,13 @@ mod tests {
10691075
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
10701076
let entropy = FixedEntropy {};
10711077
let secp_ctx = Secp256k1::new();
1078+
let payment_id = PaymentId([1; 32]);
10721079

10731080
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
10741081
.amount_msats(1000)
10751082
.build().unwrap();
10761083
let invoice_request = offer
1077-
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy)
1084+
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy, payment_id)
10781085
.unwrap()
10791086
.build().unwrap()
10801087
.sign(payer_sign).unwrap();
@@ -1084,7 +1091,10 @@ mod tests {
10841091
.unwrap()
10851092
.build().unwrap()
10861093
.sign(recipient_sign).unwrap();
1087-
assert!(invoice.verify(&expanded_key, &secp_ctx));
1094+
match invoice.verify(&expanded_key, &secp_ctx) {
1095+
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
1096+
Err(()) => panic!("verification failed"),
1097+
}
10881098

10891099
// Fails verification with altered fields
10901100
let (
@@ -1107,7 +1117,7 @@ mod tests {
11071117
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
11081118

11091119
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1110-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1120+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
11111121

11121122
// Fails verification with altered metadata
11131123
let (
@@ -1130,20 +1140,21 @@ mod tests {
11301140
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
11311141

11321142
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1133-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1143+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
11341144
}
11351145

11361146
#[test]
11371147
fn builds_invoice_request_with_derived_payer_id() {
11381148
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
11391149
let entropy = FixedEntropy {};
11401150
let secp_ctx = Secp256k1::new();
1151+
let payment_id = PaymentId([1; 32]);
11411152

11421153
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
11431154
.amount_msats(1000)
11441155
.build().unwrap();
11451156
let invoice_request = offer
1146-
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx)
1157+
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx, payment_id)
11471158
.unwrap()
11481159
.build_and_sign()
11491160
.unwrap();
@@ -1152,7 +1163,10 @@ mod tests {
11521163
.unwrap()
11531164
.build().unwrap()
11541165
.sign(recipient_sign).unwrap();
1155-
assert!(invoice.verify(&expanded_key, &secp_ctx));
1166+
match invoice.verify(&expanded_key, &secp_ctx) {
1167+
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
1168+
Err(()) => panic!("verification failed"),
1169+
}
11561170

11571171
// Fails verification with altered fields
11581172
let (
@@ -1175,7 +1189,7 @@ mod tests {
11751189
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
11761190

11771191
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1178-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1192+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
11791193

11801194
// Fails verification with altered payer id
11811195
let (
@@ -1198,7 +1212,7 @@ mod tests {
11981212
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
11991213

12001214
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1201-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1215+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
12021216
}
12031217

12041218
#[test]

lightning/src/offers/offer.rs

+23-12
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ use core::time::Duration;
7777
use crate::sign::EntropySource;
7878
use crate::io;
7979
use crate::blinded_path::BlindedPath;
80+
use crate::ln::channelmanager::PaymentId;
8081
use crate::ln::features::OfferFeatures;
8182
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
8283
use crate::ln::msgs::MAX_VALUE_MSAT;
@@ -169,7 +170,7 @@ impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> {
169170
secp_ctx: &'a Secp256k1<T>
170171
) -> Self where ES::Target: EntropySource {
171172
let nonce = Nonce::from_entropy_source(entropy_source);
172-
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
173+
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, None);
173174
let metadata = Metadata::DerivedSigningPubkey(derivation_material);
174175
OfferBuilder {
175176
offer: OfferContents {
@@ -283,7 +284,7 @@ impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
283284
let mut tlv_stream = self.offer.as_tlv_stream();
284285
debug_assert_eq!(tlv_stream.metadata, None);
285286
tlv_stream.metadata = None;
286-
if metadata.derives_keys() {
287+
if metadata.derives_recipient_keys() {
287288
tlv_stream.node_id = None;
288289
}
289290

@@ -454,10 +455,12 @@ impl Offer {
454455

455456
/// Similar to [`Offer::request_invoice`] except it:
456457
/// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each
457-
/// request, and
458-
/// - sets the [`InvoiceRequest::payer_metadata`] when [`InvoiceRequestBuilder::build`] is
459-
/// called such that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice
460-
/// was requested using a base [`ExpandedKey`] from which the payer id was derived.
458+
/// request,
459+
/// - sets [`InvoiceRequest::payer_metadata`] when [`InvoiceRequestBuilder::build`] is called
460+
/// such that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice was
461+
/// requested using a base [`ExpandedKey`] from which the payer id was derived, and
462+
/// - includes the [`PaymentId`] encrypted in [`InvoiceRequest::payer_metadata`] so that it can
463+
/// be used when sending the payment for the requested invoice.
461464
///
462465
/// Useful to protect the sender's privacy.
463466
///
@@ -468,7 +471,8 @@ impl Offer {
468471
/// [`Bolt12Invoice::verify`]: crate::offers::invoice::Bolt12Invoice::verify
469472
/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
470473
pub fn request_invoice_deriving_payer_id<'a, 'b, ES: Deref, T: secp256k1::Signing>(
471-
&'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>
474+
&'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>,
475+
payment_id: PaymentId
472476
) -> Result<InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T>, Bolt12SemanticError>
473477
where
474478
ES::Target: EntropySource,
@@ -477,7 +481,9 @@ impl Offer {
477481
return Err(Bolt12SemanticError::UnknownRequiredFeatures);
478482
}
479483

480-
Ok(InvoiceRequestBuilder::deriving_payer_id(self, expanded_key, entropy_source, secp_ctx))
484+
Ok(InvoiceRequestBuilder::deriving_payer_id(
485+
self, expanded_key, entropy_source, secp_ctx, payment_id
486+
))
481487
}
482488

483489
/// Similar to [`Offer::request_invoice_deriving_payer_id`] except uses `payer_id` for the
@@ -489,7 +495,8 @@ impl Offer {
489495
///
490496
/// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
491497
pub fn request_invoice_deriving_metadata<ES: Deref>(
492-
&self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES
498+
&self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
499+
payment_id: PaymentId
493500
) -> Result<InvoiceRequestBuilder<ExplicitPayerId, secp256k1::SignOnly>, Bolt12SemanticError>
494501
where
495502
ES::Target: EntropySource,
@@ -498,7 +505,9 @@ impl Offer {
498505
return Err(Bolt12SemanticError::UnknownRequiredFeatures);
499506
}
500507

501-
Ok(InvoiceRequestBuilder::deriving_metadata(self, payer_id, expanded_key, entropy_source))
508+
Ok(InvoiceRequestBuilder::deriving_metadata(
509+
self, payer_id, expanded_key, entropy_source, payment_id
510+
))
502511
}
503512

504513
/// Creates an [`InvoiceRequestBuilder`] for the offer with the given `metadata` and `payer_id`,
@@ -661,11 +670,13 @@ impl OfferContents {
661670
let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| {
662671
match record.r#type {
663672
OFFER_METADATA_TYPE => false,
664-
OFFER_NODE_ID_TYPE => !self.metadata.as_ref().unwrap().derives_keys(),
673+
OFFER_NODE_ID_TYPE => {
674+
!self.metadata.as_ref().unwrap().derives_recipient_keys()
675+
},
665676
_ => true,
666677
}
667678
});
668-
signer::verify_metadata(
679+
signer::verify_recipient_metadata(
669680
metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx
670681
)
671682
},

0 commit comments

Comments
 (0)