Skip to content

Commit 6612b18

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 8dbb2d9 commit 6612b18

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
@@ -111,6 +111,7 @@ use core::time::Duration;
111111
use crate::io;
112112
use crate::blinded_path::BlindedPath;
113113
use crate::ln::PaymentHash;
114+
use crate::ln::channelmanager::PaymentId;
114115
use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
115116
use crate::ln::inbound_payment::ExpandedKey;
116117
use crate::ln::msgs::DecodeError;
@@ -598,10 +599,11 @@ impl Bolt12Invoice {
598599
merkle::message_digest(SIGNATURE_TAG, &self.bytes).as_ref().clone()
599600
}
600601

601-
/// Verifies that the invoice was for a request or refund created using the given key.
602+
/// Verifies that the invoice was for a request or refund created using the given key. Returns
603+
/// the associated [`PaymentId`] to use when sending the payment.
602604
pub fn verify<T: secp256k1::Signing>(
603605
&self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
604-
) -> bool {
606+
) -> Result<PaymentId, ()> {
605607
self.contents.verify(TlvStream::new(&self.bytes), key, secp_ctx)
606608
}
607609

@@ -667,7 +669,7 @@ impl InvoiceContents {
667669

668670
fn verify<T: secp256k1::Signing>(
669671
&self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
670-
) -> bool {
672+
) -> Result<PaymentId, ()> {
671673
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
672674
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
673675
match record.r#type {
@@ -687,10 +689,7 @@ impl InvoiceContents {
687689
},
688690
};
689691

690-
match signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) {
691-
Ok(_) => true,
692-
Err(()) => false,
693-
}
692+
signer::verify_payer_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx)
694693
}
695694

696695
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
@@ -65,6 +65,7 @@ use crate::sign::EntropySource;
6565
use crate::io;
6666
use crate::blinded_path::BlindedPath;
6767
use crate::ln::PaymentHash;
68+
use crate::ln::channelmanager::PaymentId;
6869
use crate::ln::features::InvoiceRequestFeatures;
6970
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
7071
use crate::ln::msgs::DecodeError;
@@ -129,10 +130,12 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerI
129130
}
130131

131132
pub(super) fn deriving_metadata<ES: Deref>(
132-
offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES
133+
offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
134+
payment_id: PaymentId,
133135
) -> Self where ES::Target: EntropySource {
134136
let nonce = Nonce::from_entropy_source(entropy_source);
135-
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
137+
let payment_id = Some(payment_id);
138+
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id);
136139
let metadata = Metadata::Derived(derivation_material);
137140
Self {
138141
offer,
@@ -146,10 +149,12 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerI
146149

147150
impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T> {
148151
pub(super) fn deriving_payer_id<ES: Deref>(
149-
offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>
152+
offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES,
153+
secp_ctx: &'b Secp256k1<T>, payment_id: PaymentId
150154
) -> Self where ES::Target: EntropySource {
151155
let nonce = Nonce::from_entropy_source(entropy_source);
152-
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
156+
let payment_id = Some(payment_id);
157+
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, payment_id);
153158
let metadata = Metadata::DerivedSigningPubkey(derivation_material);
154159
Self {
155160
offer,
@@ -260,7 +265,7 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a
260265
let mut tlv_stream = self.invoice_request.as_tlv_stream();
261266
debug_assert!(tlv_stream.2.payer_id.is_none());
262267
tlv_stream.0.metadata = None;
263-
if !metadata.derives_keys() {
268+
if !metadata.derives_payer_keys() {
264269
tlv_stream.2.payer_id = self.payer_id.as_ref();
265270
}
266271

@@ -648,7 +653,7 @@ impl InvoiceRequestContents {
648653
}
649654

650655
pub(super) fn derives_keys(&self) -> bool {
651-
self.inner.payer.0.derives_keys()
656+
self.inner.payer.0.derives_payer_keys()
652657
}
653658

654659
pub(super) fn chain(&self) -> ChainHash {
@@ -839,6 +844,7 @@ mod tests {
839844
#[cfg(feature = "std")]
840845
use core::time::Duration;
841846
use crate::sign::KeyMaterial;
847+
use crate::ln::channelmanager::PaymentId;
842848
use crate::ln::features::InvoiceRequestFeatures;
843849
use crate::ln::inbound_payment::ExpandedKey;
844850
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
@@ -945,12 +951,13 @@ mod tests {
945951
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
946952
let entropy = FixedEntropy {};
947953
let secp_ctx = Secp256k1::new();
954+
let payment_id = PaymentId([1; 32]);
948955

949956
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
950957
.amount_msats(1000)
951958
.build().unwrap();
952959
let invoice_request = offer
953-
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy)
960+
.request_invoice_deriving_metadata(payer_id, &expanded_key, &entropy, payment_id)
954961
.unwrap()
955962
.build().unwrap()
956963
.sign(payer_sign).unwrap();
@@ -960,7 +967,10 @@ mod tests {
960967
.unwrap()
961968
.build().unwrap()
962969
.sign(recipient_sign).unwrap();
963-
assert!(invoice.verify(&expanded_key, &secp_ctx));
970+
match invoice.verify(&expanded_key, &secp_ctx) {
971+
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
972+
Err(()) => panic!("verification failed"),
973+
}
964974

965975
// Fails verification with altered fields
966976
let (
@@ -985,7 +995,7 @@ mod tests {
985995
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
986996

987997
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
988-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
998+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
989999

9901000
// Fails verification with altered metadata
9911001
let (
@@ -1010,20 +1020,21 @@ mod tests {
10101020
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
10111021

10121022
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1013-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1023+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
10141024
}
10151025

10161026
#[test]
10171027
fn builds_invoice_request_with_derived_payer_id() {
10181028
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
10191029
let entropy = FixedEntropy {};
10201030
let secp_ctx = Secp256k1::new();
1031+
let payment_id = PaymentId([1; 32]);
10211032

10221033
let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
10231034
.amount_msats(1000)
10241035
.build().unwrap();
10251036
let invoice_request = offer
1026-
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx)
1037+
.request_invoice_deriving_payer_id(&expanded_key, &entropy, &secp_ctx, payment_id)
10271038
.unwrap()
10281039
.build_and_sign()
10291040
.unwrap();
@@ -1032,7 +1043,10 @@ mod tests {
10321043
.unwrap()
10331044
.build().unwrap()
10341045
.sign(recipient_sign).unwrap();
1035-
assert!(invoice.verify(&expanded_key, &secp_ctx));
1046+
match invoice.verify(&expanded_key, &secp_ctx) {
1047+
Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])),
1048+
Err(()) => panic!("verification failed"),
1049+
}
10361050

10371051
// Fails verification with altered fields
10381052
let (
@@ -1057,7 +1071,7 @@ mod tests {
10571071
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
10581072

10591073
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1060-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1074+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
10611075

10621076
// Fails verification with altered payer id
10631077
let (
@@ -1082,7 +1096,7 @@ mod tests {
10821096
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
10831097

10841098
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
1085-
assert!(!invoice.verify(&expanded_key, &secp_ctx));
1099+
assert!(invoice.verify(&expanded_key, &secp_ctx).is_err());
10861100
}
10871101

10881102
#[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)