Skip to content

Commit 070a6f3

Browse files
committed
InvoiceRequest metadata and payer id derivation
Add support for deriving a transient payer id for each InvoiceRequest from an ExpandedKey and a nonce. This facilitates payer privacy by not tying any InvoiceRequest to any other nor to the payer's node id. Additionally, support stateless Invoice verification by setting payer metadata using an HMAC over the nonce and the remaining TLV records, which will be later verified when receiving an Invoice response.
1 parent 6ba2ba7 commit 070a6f3

File tree

2 files changed

+77
-4
lines changed

2 files changed

+77
-4
lines changed

lightning/src/offers/invoice_request.rs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,14 @@ use core::convert::TryFrom;
6060
use crate::io;
6161
use crate::ln::PaymentHash;
6262
use crate::ln::features::InvoiceRequestFeatures;
63-
use crate::ln::inbound_payment::ExpandedKey;
63+
use crate::ln::inbound_payment::{ExpandedKey, Nonce};
6464
use crate::ln::msgs::DecodeError;
6565
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
6666
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self};
6767
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
6868
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
6969
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
70+
use crate::offers::signer::{MetadataMaterial, DerivedPubkey};
7071
use crate::onion_message::BlindedPath;
7172
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
7273
use crate::util::string::PrintableString;
@@ -83,6 +84,7 @@ const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "sig
8384
pub struct InvoiceRequestBuilder<'a> {
8485
offer: &'a Offer,
8586
invoice_request: InvoiceRequestContents,
87+
metadata_material: Option<MetadataMaterial>,
8688
}
8789

8890
impl<'a> InvoiceRequestBuilder<'a> {
@@ -94,9 +96,45 @@ impl<'a> InvoiceRequestBuilder<'a> {
9496
amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None,
9597
payer_id, payer_note: None,
9698
},
99+
metadata_material: None,
97100
}
98101
}
99102

103+
#[allow(unused)]
104+
pub(super) fn deriving_payer_id(offer: &'a Offer, payer_id: DerivedPubkey) -> Self {
105+
let (payer_id, metadata_material) = payer_id.into_parts();
106+
Self {
107+
offer,
108+
invoice_request: InvoiceRequestContents {
109+
payer: PayerContents(vec![]), offer: offer.contents.clone(), chain: None,
110+
amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None,
111+
payer_id, payer_note: None,
112+
},
113+
metadata_material: Some(metadata_material),
114+
}
115+
}
116+
117+
/// Sets the [`InvoiceRequest::metadata`] derived from the given `key` and any fields set prior
118+
/// to calling [`InvoiceRequestBuilder::build`]. Allows for stateless verification of an
119+
/// [`Invoice`] when using a public node id as the [`InvoiceRequest::payer_id`] instead of a
120+
/// derived one.
121+
///
122+
/// Errors if already called or if the builder was constructed with [`Self::deriving_payer_id`].
123+
///
124+
/// [`Invoice`]: crate::offers::invoice::Invoice
125+
#[allow(unused)]
126+
pub(crate) fn metadata_derived(
127+
mut self, key: &ExpandedKey, nonce: Nonce
128+
) -> Result<Self, SemanticError> {
129+
if self.metadata_material.is_some() {
130+
return Err(SemanticError::UnexpectedMetadata);
131+
}
132+
133+
self.invoice_request.payer = PayerContents(vec![]);
134+
self.metadata_material = Some(MetadataMaterial::new(nonce, key));
135+
Ok(self)
136+
}
137+
100138
/// Sets the [`InvoiceRequest::chain`] of the given [`Network`] for paying an invoice. If not
101139
/// called, [`Network::Bitcoin`] is assumed. Errors if the chain for `network` is not supported
102140
/// by the offer.
@@ -153,6 +191,21 @@ impl<'a> InvoiceRequestBuilder<'a> {
153191
}
154192
}
155193

194+
// Create the metadata for stateless verification of an Invoice.
195+
if let Some(mut metadata_material) = self.metadata_material {
196+
debug_assert!(self.invoice_request.payer.0.is_empty());
197+
let mut tlv_stream = self.invoice_request.as_tlv_stream();
198+
tlv_stream.0.metadata = None;
199+
tlv_stream.2.payer_id = None;
200+
tlv_stream.write(&mut metadata_material).unwrap();
201+
202+
self.invoice_request.payer.0 = metadata_material.into_metadata();
203+
}
204+
205+
if self.invoice_request.payer.0.is_empty() {
206+
return Err(SemanticError::MissingPayerMetadata);
207+
}
208+
156209
let chain = self.invoice_request.chain();
157210
if !self.offer.supports_chain(chain) {
158211
return Err(SemanticError::UnsupportedChain);
@@ -171,7 +224,7 @@ impl<'a> InvoiceRequestBuilder<'a> {
171224
self.invoice_request.amount_msats, self.invoice_request.quantity
172225
)?;
173226

174-
let InvoiceRequestBuilder { offer, invoice_request } = self;
227+
let InvoiceRequestBuilder { offer, invoice_request, .. } = self;
175228
Ok(UnsignedInvoiceRequest { offer, invoice_request })
176229
}
177230
}
@@ -200,7 +253,7 @@ impl<'a> InvoiceRequestBuilder<'a> {
200253
}
201254

202255
pub(super) fn build_unchecked(self) -> UnsignedInvoiceRequest<'a> {
203-
let InvoiceRequestBuilder { offer, invoice_request } = self;
256+
let InvoiceRequestBuilder { offer, invoice_request, .. } = self;
204257
UnsignedInvoiceRequest { offer, invoice_request }
205258
}
206259
}
@@ -998,7 +1051,7 @@ mod tests {
9981051
let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
9991052
.amount_msats(1000)
10001053
.build().unwrap()
1001-
.request_invoice(vec![42; 32], payer_pubkey()).unwrap()
1054+
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
10021055
.build().unwrap()
10031056
.sign(payer_sign).unwrap();
10041057

lightning/src/offers/offer.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,26 @@ impl Offer {
442442
Ok(InvoiceRequestBuilder::new(self, metadata, payer_id))
443443
}
444444

445+
/// Similar to [`Offer::request_invoice`] except it:
446+
/// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each
447+
/// request, and
448+
/// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such
449+
/// that it can be used by [`Invoice::verify`] to determine if the invoice was requested using
450+
/// a base [`ExpandedKey`] from which the payer id was derived.
451+
///
452+
/// [`Invoice::verify`]: crate::offers::invoice::Invoice::verify
453+
/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
454+
#[allow(unused)]
455+
pub(crate) fn request_invoice_deriving_payer_id(
456+
&self, payer_id: DerivedPubkey,
457+
) -> Result<InvoiceRequestBuilder, SemanticError> {
458+
if self.features().requires_unknown_bits() {
459+
return Err(SemanticError::UnknownRequiredFeatures);
460+
}
461+
462+
Ok(InvoiceRequestBuilder::deriving_payer_id(self, payer_id))
463+
}
464+
445465
#[cfg(test)]
446466
pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
447467
self.contents.as_tlv_stream()

0 commit comments

Comments
 (0)