Skip to content

Commit bbdc53a

Browse files
committed
Refund metadata and payer id derivation
Add support for deriving a transient payer id for each Refund from an ExpandedKey and a nonce. This facilitates payer privacy by not tying any Refund 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 94c7679 commit bbdc53a

File tree

1 file changed

+74
-16
lines changed

1 file changed

+74
-16
lines changed

lightning/src/offers/refund.rs

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,23 @@
7373
7474
use bitcoin::blockdata::constants::ChainHash;
7575
use bitcoin::network::constants::Network;
76-
use bitcoin::secp256k1::PublicKey;
76+
use bitcoin::secp256k1::{PublicKey, Secp256k1, self};
7777
use core::convert::TryFrom;
78+
use core::ops::Deref;
7879
use core::str::FromStr;
7980
use core::time::Duration;
81+
use crate::chain::keysinterface::EntropySource;
8082
use crate::io;
8183
use crate::ln::PaymentHash;
8284
use crate::ln::features::InvoiceRequestFeatures;
85+
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
8386
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
8487
use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
8588
use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
8689
use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
8790
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
8891
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
89-
use crate::offers::signer::Metadata;
92+
use crate::offers::signer::{Metadata, MetadataMaterial};
9093
use crate::onion_message::BlindedPath;
9194
use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer};
9295
use crate::util::string::PrintableString;
@@ -96,16 +99,19 @@ use crate::prelude::*;
9699
#[cfg(feature = "std")]
97100
use std::time::SystemTime;
98101

102+
const IV_BYTES: &[u8; IV_LEN] = b"LDK Refund ~~~~~";
103+
99104
/// Builds a [`Refund`] for the "offer for money" flow.
100105
///
101106
/// See [module-level documentation] for usage.
102107
///
103108
/// [module-level documentation]: self
104-
pub struct RefundBuilder {
109+
pub struct RefundBuilder<'a, T: secp256k1::Signing> {
105110
refund: RefundContents,
111+
secp_ctx: Option<&'a Secp256k1<T>>,
106112
}
107113

108-
impl RefundBuilder {
114+
impl<'a> RefundBuilder<'a, secp256k1::SignOnly> {
109115
/// Creates a new builder for a refund using the [`Refund::payer_id`] for the public node id to
110116
/// send to if no [`Refund::paths`] are set. Otherwise, it may be a transient pubkey.
111117
///
@@ -119,13 +125,47 @@ impl RefundBuilder {
119125
}
120126

121127
let metadata = Metadata::Bytes(metadata);
122-
let refund = RefundContents {
123-
payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
124-
paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
125-
quantity: None, payer_id, payer_note: None,
126-
};
128+
Ok(Self {
129+
refund: RefundContents {
130+
payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
131+
paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
132+
quantity: None, payer_id, payer_note: None,
133+
},
134+
secp_ctx: None,
135+
})
136+
}
137+
}
127138

128-
Ok(RefundBuilder { refund })
139+
impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> {
140+
/// Similar to [`RefundBuilder::new`] except, if [`RefundBuilder::path`] is called, the payer id
141+
/// is derived from the given [`ExpandedKey`] and nonce. This provides sender privacy by using a
142+
/// different payer id for each refund, assuming a different nonce is used. Otherwise, the
143+
/// provided `node_id` is used for the payer id.
144+
///
145+
/// Also, sets the metadata when [`RefundBuilder::build`] is called such that it can be used to
146+
/// verify that an [`InvoiceRequest`] was produced for the refund given an [`ExpandedKey`].
147+
///
148+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
149+
/// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
150+
pub fn deriving_payer_id<ES: Deref>(
151+
description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
152+
secp_ctx: &'a Secp256k1<T>, amount_msats: u64
153+
) -> Result<Self, SemanticError> where ES::Target: EntropySource {
154+
if amount_msats > MAX_VALUE_MSAT {
155+
return Err(SemanticError::InvalidAmount);
156+
}
157+
158+
let nonce = Nonce::from_entropy_source(entropy_source);
159+
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
160+
let metadata = Metadata::DerivedSigningPubkey(derivation_material);
161+
Ok(Self {
162+
refund: RefundContents {
163+
payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
164+
paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
165+
quantity: None, payer_id: node_id, payer_note: None,
166+
},
167+
secp_ctx: Some(secp_ctx),
168+
})
129169
}
130170

131171
/// Sets the [`Refund::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has
@@ -192,18 +232,36 @@ impl RefundBuilder {
192232
self.refund.chain = None;
193233
}
194234

235+
// Create the metadata for stateless verification of an Invoice.
236+
if self.refund.payer.0.has_derivation_material() {
237+
let mut metadata = core::mem::take(&mut self.refund.payer.0);
238+
239+
if self.refund.paths.is_none() {
240+
metadata = metadata.without_keys();
241+
}
242+
243+
let mut tlv_stream = self.refund.as_tlv_stream();
244+
tlv_stream.0.metadata = None;
245+
tlv_stream.2.payer_id = None;
246+
247+
let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx);
248+
metadata = derived_metadata;
249+
if let Some(keys) = keys {
250+
self.refund.payer_id = keys.public_key();
251+
}
252+
253+
self.refund.payer.0 = metadata;
254+
}
255+
195256
let mut bytes = Vec::new();
196257
self.refund.write(&mut bytes).unwrap();
197258

198-
Ok(Refund {
199-
bytes,
200-
contents: self.refund,
201-
})
259+
Ok(Refund { bytes, contents: self.refund })
202260
}
203261
}
204262

205263
#[cfg(test)]
206-
impl RefundBuilder {
264+
impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> {
207265
fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self {
208266
self.refund.features = features;
209267
self
@@ -283,7 +341,7 @@ impl Refund {
283341
///
284342
/// [`payer_id`]: Self::payer_id
285343
pub fn metadata(&self) -> &[u8] {
286-
&self.contents.payer.0.as_bytes().unwrap()[..]
344+
self.contents.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[])
287345
}
288346

289347
/// A chain that the refund is valid for.

0 commit comments

Comments
 (0)