Skip to content

Commit 1b6aafb

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 3af2ff2 commit 1b6aafb

File tree

7 files changed

+190
-52
lines changed

7 files changed

+190
-52
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,12 @@ struct ClaimableHTLC {
221221
///
222222
/// This is not exported to bindings users as we just use [u8; 32] directly
223223
#[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)]
224-
pub struct PaymentId(pub [u8; 32]);
224+
pub struct PaymentId(pub [u8; Self::LENGTH]);
225+
226+
impl PaymentId {
227+
/// Number of bytes in the id.
228+
pub const LENGTH: usize = 32;
229+
}
225230

226231
impl Writeable for PaymentId {
227232
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
@@ -78,6 +78,17 @@ impl ExpandedKey {
7878
hmac.input(&nonce.0);
7979
hmac
8080
}
81+
82+
/// Encrypts or decrypts the given `bytes`. Used for data included in an offer message's
83+
/// metadata (e.g., payment id).
84+
pub(crate) fn crypt_for_offer(&self, mut bytes: [u8; 32], iv_bytes: &[u8; IV_LEN]) -> [u8; 32] {
85+
let chacha_block = ChaCha20::get_single_block(&self.offers_base_key, iv_bytes);
86+
for i in 0..bytes.len() {
87+
bytes[i] = chacha_block[i] ^ bytes[i];
88+
}
89+
90+
bytes
91+
}
8192
}
8293

8394
/// 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
@@ -107,6 +107,7 @@ use core::time::Duration;
107107
use crate::io;
108108
use crate::blinded_path::BlindedPath;
109109
use crate::ln::PaymentHash;
110+
use crate::ln::channelmanager::PaymentId;
110111
use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
111112
use crate::ln::inbound_payment::ExpandedKey;
112113
use crate::ln::msgs::DecodeError;
@@ -593,10 +594,11 @@ impl Bolt12Invoice {
593594
merkle::message_digest(SIGNATURE_TAG, &self.bytes).as_ref().clone()
594595
}
595596

596-
/// Verifies that the invoice was for a request or refund created using the given key.
597+
/// Verifies that the invoice was for a request or refund created using the given key. Returns
598+
/// the associated [`PaymentId`] to use when sending the payment.
597599
pub fn verify<T: secp256k1::Signing>(
598600
&self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
599-
) -> bool {
601+
) -> Result<PaymentId, ()> {
600602
self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx)
601603
}
602604

@@ -655,7 +657,7 @@ impl InvoiceContents {
655657

656658
fn verify<T: secp256k1::Signing>(
657659
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
658-
) -> bool {
660+
) -> Result<PaymentId, ()> {
659661
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
660662
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
661663
match record.r#type {
@@ -675,10 +677,7 @@ impl InvoiceContents {
675677
},
676678
};
677679

678-
match signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) {
679-
Ok(_) => true,
680-
Err(()) => false,
681-
}
680+
signer::verify_payer_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx)
682681
}
683682

684683
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
@@ -63,6 +63,7 @@ use crate::sign::EntropySource;
6363
use crate::io;
6464
use crate::blinded_path::BlindedPath;
6565
use crate::ln::PaymentHash;
66+
use crate::ln::channelmanager::PaymentId;
6667
use crate::ln::features::InvoiceRequestFeatures;
6768
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
6869
use crate::ln::msgs::DecodeError;
@@ -127,10 +128,12 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerI
127128
}
128129

129130
pub(super) fn deriving_metadata<ES: Deref>(
130-
offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES
131+
offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
132+
payment_id: PaymentId,
131133
) -> Self where ES::Target: EntropySource {
132134
let nonce = Nonce::from_entropy_source(entropy_source);
133-
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
135+
let payment_id = Some(payment_id);
136+
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id);
134137
let metadata = Metadata::Derived(derivation_material);
135138
Self {
136139
offer,
@@ -144,10 +147,12 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerI
144147

145148
impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T> {
146149
pub(super) fn deriving_payer_id<ES: Deref>(
147-
offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>
150+
offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES,
151+
secp_ctx: &'b Secp256k1<T>, payment_id: PaymentId
148152
) -> Self where ES::Target: EntropySource {
149153
let nonce = Nonce::from_entropy_source(entropy_source);
150-
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
154+
let payment_id = Some(payment_id);
155+
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id);
151156
let metadata = Metadata::DerivedSigningPubkey(derivation_material);
152157
Self {
153158
offer,
@@ -258,7 +263,7 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a
258263
let mut tlv_stream = self.invoice_request.as_tlv_stream();
259264
debug_assert!(tlv_stream.2.payer_id.is_none());
260265
tlv_stream.0.metadata = None;
261-
if !metadata.derives_keys() {
266+
if !metadata.derives_payer_keys() {
262267
tlv_stream.2.payer_id = self.payer_id.as_ref();
263268
}
264269

@@ -645,7 +650,7 @@ impl InvoiceRequestContents {
645650
}
646651

647652
pub(super) fn derives_keys(&self) -> bool {
648-
self.inner.payer.0.derives_keys()
653+
self.inner.payer.0.derives_payer_keys()
649654
}
650655

651656
pub(super) fn chain(&self) -> ChainHash {
@@ -836,6 +841,7 @@ mod tests {
836841
#[cfg(feature = "std")]
837842
use core::time::Duration;
838843
use crate::sign::KeyMaterial;
844+
use crate::ln::channelmanager::PaymentId;
839845
use crate::ln::features::InvoiceRequestFeatures;
840846
use crate::ln::inbound_payment::ExpandedKey;
841847
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
@@ -940,12 +946,13 @@ mod tests {
940946
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
941947
let entropy = FixedEntropy {};
942948
let secp_ctx = Secp256k1::new();
949+
let payment_id = PaymentId([1; 32]);
943950

944951
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
945952
.amount_msats(1000)
946953
.build().unwrap();
947954
let invoice_request = offer
948-
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy)
955+
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy, payment_id)
949956
.unwrap()
950957
.build().unwrap()
951958
.sign(payer_sign).unwrap();
@@ -955,7 +962,10 @@ mod tests {
955962
.unwrap()
956963
.build().unwrap()
957964
.sign(recipient_sign).unwrap();
958-
assert!(invoice.verify(&expanded_key, &secp_ctx));
965+
match invoice.verify(&expanded_key, &secp_ctx) {
966+
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
967+
Err(()) => panic!("verification failed"),
968+
}
959969

960970
// Fails verification with altered fields
961971
let (
@@ -978,7 +988,7 @@ mod tests {
978988
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
979989

980990
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
981-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
991+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
982992

983993
// Fails verification with altered metadata
984994
let (
@@ -1001,20 +1011,21 @@ mod tests {
10011011
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
10021012

10031013
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1004-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1014+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
10051015
}
10061016

10071017
#[test]
10081018
fn builds_invoice_request_with_derived_payer_id() {
10091019
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
10101020
let entropy = FixedEntropy {};
10111021
let secp_ctx = Secp256k1::new();
1022+
let payment_id = PaymentId([1; 32]);
10121023

10131024
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
10141025
.amount_msats(1000)
10151026
.build().unwrap();
10161027
let invoice_request = offer
1017-
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx)
1028+
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx, payment_id)
10181029
.unwrap()
10191030
.build_and_sign()
10201031
.unwrap();
@@ -1023,7 +1034,10 @@ mod tests {
10231034
.unwrap()
10241035
.build().unwrap()
10251036
.sign(recipient_sign).unwrap();
1026-
assert!(invoice.verify(&expanded_key, &secp_ctx));
1037+
match invoice.verify(&expanded_key, &secp_ctx) {
1038+
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
1039+
Err(()) => panic!("verification failed"),
1040+
}
10271041

10281042
// Fails verification with altered fields
10291043
let (
@@ -1046,7 +1060,7 @@ mod tests {
10461060
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
10471061

10481062
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1049-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1063+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
10501064

10511065
// Fails verification with altered payer id
10521066
let (
@@ -1069,7 +1083,7 @@ mod tests {
10691083
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
10701084

10711085
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1072-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1086+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
10731087
}
10741088

10751089
#[test]

lightning/src/offers/offer.rs

Lines changed: 21 additions & 10 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

@@ -450,10 +451,12 @@ impl Offer {
450451

451452
/// Similar to [`Offer::request_invoice`] except it:
452453
/// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each
453-
/// request, and
454+
/// request,
454455
/// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such
455456
/// that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice was requested
456-
/// using a base [`ExpandedKey`] from which the payer id was derived.
457+
/// using a base [`ExpandedKey`] from which the payer id was derived, and
458+
/// - includes the [`PaymentId`] encrypted in [`InvoiceRequest::metadata`] so that it can be
459+
/// used when sending the payment for the requested invoice.
457460
///
458461
/// Useful to protect the sender's privacy.
459462
///
@@ -464,7 +467,8 @@ impl Offer {
464467
/// [`Bolt12Invoice::verify`]: crate::offers::invoice::Bolt12Invoice::verify
465468
/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
466469
pub fn request_invoice_deriving_payer_id<'a, 'b, ES: Deref, T: secp256k1::Signing>(
467-
&'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>
470+
&'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>,
471+
payment_id: PaymentId
468472
) -> Result<InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T>, Bolt12SemanticError>
469473
where
470474
ES::Target: EntropySource,
@@ -473,7 +477,9 @@ impl Offer {
473477
return Err(Bolt12SemanticError::UnknownRequiredFeatures);
474478
}
475479

476-
Ok(InvoiceRequestBuilder::deriving_payer_id(self, expanded_key, entropy_source, secp_ctx))
480+
Ok(InvoiceRequestBuilder::deriving_payer_id(
481+
self, expanded_key, entropy_source, secp_ctx, payment_id
482+
))
477483
}
478484

479485
/// Similar to [`Offer::request_invoice_deriving_payer_id`] except uses `payer_id` for the
@@ -485,7 +491,8 @@ impl Offer {
485491
///
486492
/// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
487493
pub fn request_invoice_deriving_metadata<ES: Deref>(
488-
&self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES
494+
&self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
495+
payment_id: PaymentId
489496
) -> Result<InvoiceRequestBuilder<ExplicitPayerId, secp256k1::SignOnly>, Bolt12SemanticError>
490497
where
491498
ES::Target: EntropySource,
@@ -494,7 +501,9 @@ impl Offer {
494501
return Err(Bolt12SemanticError::UnknownRequiredFeatures);
495502
}
496503

497-
Ok(InvoiceRequestBuilder::deriving_metadata(self, payer_id, expanded_key, entropy_source))
504+
Ok(InvoiceRequestBuilder::deriving_metadata(
505+
self, payer_id, expanded_key, entropy_source, payment_id
506+
))
498507
}
499508

500509
/// Creates an [`InvoiceRequestBuilder`] for the offer with the given `metadata` and `payer_id`,
@@ -641,11 +650,13 @@ impl OfferContents {
641650
let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| {
642651
match record.r#type {
643652
OFFER_METADATA_TYPE => false,
644-
OFFER_NODE_ID_TYPE => !self.metadata.as_ref().unwrap().derives_keys(),
653+
OFFER_NODE_ID_TYPE => {
654+
!self.metadata.as_ref().unwrap().derives_recipient_keys()
655+
},
645656
_ => true,
646657
}
647658
});
648-
signer::verify_metadata(
659+
signer::verify_recipient_metadata(
649660
metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx
650661
)
651662
},

0 commit comments

Comments
 (0)