Skip to content

Commit 39bfe5b

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 b7ac70a commit 39bfe5b

File tree

7 files changed

+206
-54
lines changed

7 files changed

+206
-54
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 6 additions & 1 deletion
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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ 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], iv_bytes: &[u8; IV_LEN]) -> [u8; 32] {
93+
let chacha_block = ChaCha20::get_single_block(&self.offers_base_key, iv_bytes);
94+
for i in 0..bytes.len() {
95+
bytes[i] = chacha_block[i] ^ bytes[i];
96+
}
97+
98+
bytes
99+
}
89100
}
90101

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

lightning/src/offers/invoice.rs

Lines changed: 6 additions & 7 deletions
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

Lines changed: 28 additions & 14 deletions
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

@@ -682,7 +687,7 @@ impl InvoiceRequestContents {
682687
}
683688

684689
pub(super) fn derives_keys(&self) -> bool {
685-
self.inner.payer.0.derives_keys()
690+
self.inner.payer.0.derives_payer_keys()
686691
}
687692

688693
pub(super) fn chain(&self) -> ChainHash {
@@ -915,6 +920,7 @@ mod tests {
915920
#[cfg(feature = "std")]
916921
use core::time::Duration;
917922
use crate::sign::KeyMaterial;
923+
use crate::ln::channelmanager::PaymentId;
918924
use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures};
919925
use crate::ln::inbound_payment::ExpandedKey;
920926
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
@@ -1060,12 +1066,13 @@ mod tests {
10601066
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
10611067
let entropy = FixedEntropy {};
10621068
let secp_ctx = Secp256k1::new();
1069+
let payment_id = PaymentId([1; 32]);
10631070

10641071
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
10651072
.amount_msats(1000)
10661073
.build().unwrap();
10671074
let invoice_request = offer
1068-
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy)
1075+
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy, payment_id)
10691076
.unwrap()
10701077
.build().unwrap()
10711078
.sign(payer_sign).unwrap();
@@ -1075,7 +1082,10 @@ mod tests {
10751082
.unwrap()
10761083
.build().unwrap()
10771084
.sign(recipient_sign).unwrap();
1078-
assert!(invoice.verify(&expanded_key, &secp_ctx));
1085+
match invoice.verify(&expanded_key, &secp_ctx) {
1086+
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
1087+
Err(()) => panic!("verification failed"),
1088+
}
10791089

10801090
// Fails verification with altered fields
10811091
let (
@@ -1098,7 +1108,7 @@ mod tests {
10981108
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
10991109

11001110
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1101-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1111+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
11021112

11031113
// Fails verification with altered metadata
11041114
let (
@@ -1121,20 +1131,21 @@ mod tests {
11211131
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
11221132

11231133
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1124-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1134+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
11251135
}
11261136

11271137
#[test]
11281138
fn builds_invoice_request_with_derived_payer_id() {
11291139
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
11301140
let entropy = FixedEntropy {};
11311141
let secp_ctx = Secp256k1::new();
1142+
let payment_id = PaymentId([1; 32]);
11321143

11331144
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
11341145
.amount_msats(1000)
11351146
.build().unwrap();
11361147
let invoice_request = offer
1137-
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx)
1148+
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx, payment_id)
11381149
.unwrap()
11391150
.build_and_sign()
11401151
.unwrap();
@@ -1143,7 +1154,10 @@ mod tests {
11431154
.unwrap()
11441155
.build().unwrap()
11451156
.sign(recipient_sign).unwrap();
1146-
assert!(invoice.verify(&expanded_key, &secp_ctx));
1157+
match invoice.verify(&expanded_key, &secp_ctx) {
1158+
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
1159+
Err(()) => panic!("verification failed"),
1160+
}
11471161

11481162
// Fails verification with altered fields
11491163
let (
@@ -1166,7 +1180,7 @@ mod tests {
11661180
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
11671181

11681182
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1169-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1183+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
11701184

11711185
// Fails verification with altered payer id
11721186
let (
@@ -1189,7 +1203,7 @@ mod tests {
11891203
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
11901204

11911205
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1192-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1206+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
11931207
}
11941208

11951209
#[test]

lightning/src/offers/offer.rs

Lines changed: 23 additions & 12 deletions
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)