//! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
//! [`Offer`]: crate::offers::offer::Offer
//!
-//! ```ignore
+//! ```
//! extern crate bitcoin;
//! extern crate core;
//! extern crate lightning;
//! use lightning::offers::refund::{Refund, RefundBuilder};
//! use lightning::util::ser::{Readable, Writeable};
//!
-//! # use lightning::onion_message::BlindedPath;
+//! # use lightning::blinded_path::BlindedPath;
//! # #[cfg(feature = "std")]
//! # use std::time::SystemTime;
//! #
use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
-use bitcoin::secp256k1::PublicKey;
+use bitcoin::secp256k1::{PublicKey, Secp256k1, self};
use core::convert::TryFrom;
+use core::ops::Deref;
use core::str::FromStr;
use core::time::Duration;
+use crate::sign::EntropySource;
use crate::io;
+use crate::blinded_path::BlindedPath;
use crate::ln::PaymentHash;
use crate::ln::features::InvoiceRequestFeatures;
+use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
-use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
+use crate::offers::invoice::{BlindedPayInfo, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder};
use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
-use crate::onion_message::BlindedPath;
+use crate::offers::signer::{Metadata, MetadataMaterial, self};
use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer};
use crate::util::string::PrintableString;
#[cfg(feature = "std")]
use std::time::SystemTime;
+pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Refund ~~~~~";
+
/// Builds a [`Refund`] for the "offer for money" flow.
///
/// See [module-level documentation] for usage.
///
+/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+///
/// [module-level documentation]: self
-pub struct RefundBuilder {
+pub struct RefundBuilder<'a, T: secp256k1::Signing> {
refund: RefundContents,
+ secp_ctx: Option<&'a Secp256k1<T>>,
}
-impl RefundBuilder {
+impl<'a> RefundBuilder<'a, secp256k1::SignOnly> {
/// Creates a new builder for a refund using the [`Refund::payer_id`] for the public node id to
/// send to if no [`Refund::paths`] are set. Otherwise, it may be a transient pubkey.
///
return Err(SemanticError::InvalidAmount);
}
- let refund = RefundContents {
- payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
- paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
- quantity: None, payer_id, payer_note: None,
- };
+ let metadata = Metadata::Bytes(metadata);
+ Ok(Self {
+ refund: RefundContents {
+ payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
+ paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
+ quantity: None, payer_id, payer_note: None,
+ },
+ secp_ctx: None,
+ })
+ }
+}
- Ok(RefundBuilder { refund })
+impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> {
+ /// Similar to [`RefundBuilder::new`] except, if [`RefundBuilder::path`] is called, the payer id
+ /// is derived from the given [`ExpandedKey`] and nonce. This provides sender privacy by using a
+ /// different payer id for each refund, assuming a different nonce is used. Otherwise, the
+ /// provided `node_id` is used for the payer id.
+ ///
+ /// Also, sets the metadata when [`RefundBuilder::build`] is called such that it can be used to
+ /// verify that an [`InvoiceRequest`] was produced for the refund given an [`ExpandedKey`].
+ ///
+ /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+ /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
+ pub fn deriving_payer_id<ES: Deref>(
+ description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
+ secp_ctx: &'a Secp256k1<T>, amount_msats: u64
+ ) -> Result<Self, SemanticError> where ES::Target: EntropySource {
+ if amount_msats > MAX_VALUE_MSAT {
+ return Err(SemanticError::InvalidAmount);
+ }
+
+ let nonce = Nonce::from_entropy_source(entropy_source);
+ let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
+ let metadata = Metadata::DerivedSigningPubkey(derivation_material);
+ Ok(Self {
+ refund: RefundContents {
+ payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
+ paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
+ quantity: None, payer_id: node_id, payer_note: None,
+ },
+ secp_ctx: Some(secp_ctx),
+ })
}
/// Sets the [`Refund::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has
self.refund.chain = None;
}
+ // Create the metadata for stateless verification of an Invoice.
+ if self.refund.payer.0.has_derivation_material() {
+ let mut metadata = core::mem::take(&mut self.refund.payer.0);
+
+ if self.refund.paths.is_none() {
+ metadata = metadata.without_keys();
+ }
+
+ let mut tlv_stream = self.refund.as_tlv_stream();
+ tlv_stream.0.metadata = None;
+ if metadata.derives_keys() {
+ tlv_stream.2.payer_id = None;
+ }
+
+ let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx);
+ metadata = derived_metadata;
+ if let Some(keys) = keys {
+ self.refund.payer_id = keys.public_key();
+ }
+
+ self.refund.payer.0 = metadata;
+ }
+
let mut bytes = Vec::new();
self.refund.write(&mut bytes).unwrap();
- Ok(Refund {
- bytes,
- contents: self.refund,
- })
+ Ok(Refund { bytes, contents: self.refund })
}
}
#[cfg(test)]
-impl RefundBuilder {
+impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> {
fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self {
self.refund.features = features;
self
/// [`Invoice`]: crate::offers::invoice::Invoice
/// [`Offer`]: crate::offers::offer::Offer
#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
pub struct Refund {
pub(super) bytes: Vec<u8>,
pub(super) contents: RefundContents,
///
/// [`Invoice`]: crate::offers::invoice::Invoice
#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
pub(super) struct RefundContents {
payer: PayerContents,
// offer fields
/// A complete description of the purpose of the refund. Intended to be displayed to the user
/// but with the caveat that it has not been verified in any way.
pub fn description(&self) -> PrintableString {
- PrintableString(&self.contents.description)
+ self.contents.description()
}
/// Duration since the Unix epoch when an invoice should no longer be sent.
///
/// [`payer_id`]: Self::payer_id
pub fn metadata(&self) -> &[u8] {
- &self.contents.payer.0
+ self.contents.metadata()
}
/// A chain that the refund is valid for.
self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str()))
}
- /// Creates an [`Invoice`] for the refund with the given required fields.
+ /// Creates an [`InvoiceBuilder`] for the refund with the given required fields and using the
+ /// [`Duration`] since [`std::time::SystemTime::UNIX_EPOCH`] as the creation time.
+ ///
+ /// See [`Refund::respond_with_no_std`] for further details where the aforementioned creation
+ /// time is used for the `created_at` parameter.
+ ///
+ /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+ ///
+ /// [`Duration`]: core::time::Duration
+ #[cfg(feature = "std")]
+ pub fn respond_with(
+ &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
+ signing_pubkey: PublicKey,
+ ) -> Result<InvoiceBuilder<ExplicitSigningPubkey>, SemanticError> {
+ let created_at = std::time::SystemTime::now()
+ .duration_since(std::time::SystemTime::UNIX_EPOCH)
+ .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
+
+ self.respond_with_no_std(payment_paths, payment_hash, signing_pubkey, created_at)
+ }
+
+ /// Creates an [`InvoiceBuilder`] for the refund with the given required fields.
///
/// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after
- /// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter
- /// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`]
- /// is not available.
+ /// `created_at`, which is used to set [`Invoice::created_at`]. Useful for `no-std` builds where
+ /// [`std::time::SystemTime`] is not available.
///
/// The caller is expected to remember the preimage of `payment_hash` in order to
/// claim a payment for the invoice.
///
/// Errors if the request contains unknown required features.
///
- /// [`Invoice`]: crate::offers::invoice::Invoice
+ /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+ ///
/// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at
- pub fn respond_with(
+ pub fn respond_with_no_std(
&self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
- signing_pubkey: PublicKey,
- #[cfg(any(test, not(feature = "std")))]
- created_at: Duration
- ) -> Result<InvoiceBuilder, SemanticError> {
+ signing_pubkey: PublicKey, created_at: Duration
+ ) -> Result<InvoiceBuilder<ExplicitSigningPubkey>, SemanticError> {
if self.features().requires_unknown_bits() {
return Err(SemanticError::UnknownRequiredFeatures);
}
- #[cfg(all(not(test), feature = "std"))]
+ InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey)
+ }
+
+ /// Creates an [`InvoiceBuilder`] for the refund using the given required fields and that uses
+ /// derived signing keys to sign the [`Invoice`].
+ ///
+ /// See [`Refund::respond_with`] for further details.
+ ///
+ /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+ ///
+ /// [`Invoice`]: crate::offers::invoice::Invoice
+ #[cfg(feature = "std")]
+ pub fn respond_using_derived_keys<ES: Deref>(
+ &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
+ expanded_key: &ExpandedKey, entropy_source: ES
+ ) -> Result<InvoiceBuilder<DerivedSigningPubkey>, SemanticError>
+ where
+ ES::Target: EntropySource,
+ {
let created_at = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
- InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey)
+ self.respond_using_derived_keys_no_std(
+ payment_paths, payment_hash, created_at, expanded_key, entropy_source
+ )
+ }
+
+ /// Creates an [`InvoiceBuilder`] for the refund using the given required fields and that uses
+ /// derived signing keys to sign the [`Invoice`].
+ ///
+ /// See [`Refund::respond_with_no_std`] for further details.
+ ///
+ /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+ ///
+ /// [`Invoice`]: crate::offers::invoice::Invoice
+ pub fn respond_using_derived_keys_no_std<ES: Deref>(
+ &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
+ created_at: core::time::Duration, expanded_key: &ExpandedKey, entropy_source: ES
+ ) -> Result<InvoiceBuilder<DerivedSigningPubkey>, SemanticError>
+ where
+ ES::Target: EntropySource,
+ {
+ if self.features().requires_unknown_bits() {
+ return Err(SemanticError::UnknownRequiredFeatures);
+ }
+
+ let nonce = Nonce::from_entropy_source(entropy_source);
+ let keys = signer::derive_keys(nonce, expanded_key);
+ InvoiceBuilder::for_refund_using_keys(self, payment_paths, created_at, payment_hash, keys)
}
#[cfg(test)]
}
impl RefundContents {
+ pub fn description(&self) -> PrintableString {
+ PrintableString(&self.description)
+ }
+
#[cfg(feature = "std")]
pub(super) fn is_expired(&self) -> bool {
match self.absolute_expiry {
}
}
+ pub(super) fn metadata(&self) -> &[u8] {
+ self.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[])
+ }
+
pub(super) fn chain(&self) -> ChainHash {
self.chain.unwrap_or_else(|| self.implied_chain())
}
ChainHash::using_genesis_block(Network::Bitcoin)
}
+ pub(super) fn derives_keys(&self) -> bool {
+ self.payer.0.derives_keys()
+ }
+
+ pub(super) fn payer_id(&self) -> PublicKey {
+ self.payer_id
+ }
+
pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
let payer = PayerTlvStreamRef {
- metadata: Some(&self.payer.0),
+ metadata: self.payer.0.as_bytes(),
};
let offer = OfferTlvStreamRef {
let payer = match payer_metadata {
None => return Err(SemanticError::MissingPayerMetadata),
- Some(metadata) => PayerContents(metadata),
+ Some(metadata) => PayerContents(Metadata::Bytes(metadata)),
};
if metadata.is_some() {
use bitcoin::blockdata::constants::ChainHash;
use bitcoin::network::constants::Network;
- use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
+ use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
use core::convert::TryFrom;
use core::time::Duration;
+ use crate::blinded_path::{BlindedHop, BlindedPath};
+ use crate::sign::KeyMaterial;
use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures};
+ use crate::ln::inbound_payment::ExpandedKey;
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
use crate::offers::offer::OfferTlvStreamRef;
use crate::offers::parse::{ParseError, SemanticError};
use crate::offers::payer::PayerTlvStreamRef;
- use crate::onion_message::{BlindedHop, BlindedPath};
+ use crate::offers::test_utils::*;
use crate::util::ser::{BigSize, Writeable};
use crate::util::string::PrintableString;
- fn payer_pubkey() -> PublicKey {
- let secp_ctx = Secp256k1::new();
- KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()).public_key()
- }
-
- fn pubkey(byte: u8) -> PublicKey {
- let secp_ctx = Secp256k1::new();
- PublicKey::from_secret_key(&secp_ctx, &privkey(byte))
- }
-
- fn privkey(byte: u8) -> SecretKey {
- SecretKey::from_slice(&[byte; 32]).unwrap()
- }
-
trait ToBytes {
fn to_bytes(&self) -> Vec<u8>;
}
}
}
+ #[test]
+ fn builds_refund_with_metadata_derived() {
+ let desc = "foo".to_string();
+ let node_id = payer_pubkey();
+ let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+ let entropy = FixedEntropy {};
+ let secp_ctx = Secp256k1::new();
+
+ let refund = RefundBuilder
+ ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000)
+ .unwrap()
+ .build().unwrap();
+ assert_eq!(refund.payer_id(), node_id);
+
+ // Fails verification with altered fields
+ let invoice = refund
+ .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+ .unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ assert!(invoice.verify(&expanded_key, &secp_ctx));
+
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.2.amount = Some(2000);
+
+ let mut encoded_refund = Vec::new();
+ tlv_stream.write(&mut encoded_refund).unwrap();
+
+ let invoice = Refund::try_from(encoded_refund).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+ .unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ assert!(!invoice.verify(&expanded_key, &secp_ctx));
+
+ // Fails verification with altered metadata
+ let mut tlv_stream = refund.as_tlv_stream();
+ let metadata = tlv_stream.0.metadata.unwrap().iter().copied().rev().collect();
+ tlv_stream.0.metadata = Some(&metadata);
+
+ let mut encoded_refund = Vec::new();
+ tlv_stream.write(&mut encoded_refund).unwrap();
+
+ let invoice = Refund::try_from(encoded_refund).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+ .unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ assert!(!invoice.verify(&expanded_key, &secp_ctx));
+ }
+
+ #[test]
+ fn builds_refund_with_derived_payer_id() {
+ let desc = "foo".to_string();
+ let node_id = payer_pubkey();
+ let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+ let entropy = FixedEntropy {};
+ let secp_ctx = Secp256k1::new();
+
+ let blinded_path = BlindedPath {
+ introduction_node_id: pubkey(40),
+ blinding_point: pubkey(41),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] },
+ BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] },
+ ],
+ };
+
+ let refund = RefundBuilder
+ ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000)
+ .unwrap()
+ .path(blinded_path)
+ .build().unwrap();
+ assert_ne!(refund.payer_id(), node_id);
+
+ let invoice = refund
+ .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+ .unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ assert!(invoice.verify(&expanded_key, &secp_ctx));
+
+ // Fails verification with altered fields
+ let mut tlv_stream = refund.as_tlv_stream();
+ tlv_stream.2.amount = Some(2000);
+
+ let mut encoded_refund = Vec::new();
+ tlv_stream.write(&mut encoded_refund).unwrap();
+
+ let invoice = Refund::try_from(encoded_refund).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+ .unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ assert!(!invoice.verify(&expanded_key, &secp_ctx));
+
+ // Fails verification with altered payer_id
+ let mut tlv_stream = refund.as_tlv_stream();
+ let payer_id = pubkey(1);
+ tlv_stream.2.payer_id = Some(&payer_id);
+
+ let mut encoded_refund = Vec::new();
+ tlv_stream.write(&mut encoded_refund).unwrap();
+
+ let invoice = Refund::try_from(encoded_refund).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+ .unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ assert!(!invoice.verify(&expanded_key, &secp_ctx));
+ }
+
#[test]
fn builds_refund_with_absolute_expiry() {
let future_expiry = Duration::from_secs(u64::max_value());
assert_eq!(tlv_stream.payer_note, Some(&String::from("baz")));
}
+ #[test]
+ fn fails_responding_with_unknown_required_features() {
+ match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+ .features_unchecked(InvoiceRequestFeatures::unknown())
+ .build().unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+ {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, SemanticError::UnknownRequiredFeatures),
+ }
+ }
+
#[test]
fn parses_refund_with_metadata() {
let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()