//! The payment recipient must include a [`PaymentHash`], so as to reveal the preimage upon payment
//! receipt, and one or more [`BlindedPath`]s for the payer to use when sending the payment.
//!
-//! ```ignore
+//! ```
//! extern crate bitcoin;
//! extern crate lightning;
//!
//!
//! // Invoice for the "offer to be paid" flow.
//! InvoiceRequest::try_from(bytes)?
-//! .respond_with(payment_paths, payment_hash)?
+#![cfg_attr(feature = "std", doc = "
+ .respond_with(payment_paths, payment_hash)?
+")]
+#![cfg_attr(not(feature = "std"), doc = "
+ .respond_with_no_std(payment_paths, payment_hash, core::time::Duration::from_secs(0))?
+")]
//! .relative_expiry(3600)
//! .allow_mpp()
//! .fallback_v0_p2wpkh(&wpubkey_hash)
//! // Invoice for the "offer for money" flow.
//! "lnr1qcp4256ypq"
//! .parse::<Refund>()?
-//! .respond_with(payment_paths, payment_hash, pubkey)?
+#![cfg_attr(feature = "std", doc = "
+ .respond_with(payment_paths, payment_hash, pubkey)?
+")]
+#![cfg_attr(not(feature = "std"), doc = "
+ .respond_with_no_std(payment_paths, payment_hash, pubkey, core::time::Duration::from_secs(0))?
+")]
//! .relative_expiry(3600)
//! .allow_mpp()
//! .fallback_v0_p2wpkh(&wpubkey_hash)
) -> Result<Self, SemanticError> {
let amount_msats = match invoice_request.amount_msats() {
Some(amount_msats) => amount_msats,
- None => match invoice_request.contents.offer.amount() {
+ None => match invoice_request.contents.inner.offer.amount() {
Some(Amount::Bitcoin { amount_msats }) => {
- amount_msats * invoice_request.quantity().unwrap_or(1)
+ amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1))
+ .ok_or(SemanticError::InvalidAmount)?
},
Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency),
None => return Err(SemanticError::MissingAmount),
fields: InvoiceFields {
payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats,
fallbacks: None, features: Bolt12InvoiceFeatures::empty(),
- signing_pubkey: invoice_request.contents.offer.signing_pubkey(),
+ signing_pubkey: invoice_request.contents.inner.offer.signing_pubkey(),
},
};
}
impl<'a> UnsignedInvoice<'a> {
+ /// The public key corresponding to the key needed to sign the invoice.
+ pub fn signing_pubkey(&self) -> PublicKey {
+ self.invoice.fields().signing_pubkey
+ }
+
/// Signs the invoice using the given function.
pub fn sign<F, E>(self, sign: F) -> Result<Invoice, SignError<E>>
where
/// [`Offer`]: crate::offers::offer::Offer
/// [`Refund`]: crate::offers::refund::Refund
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
pub struct Invoice {
bytes: Vec<u8>,
contents: InvoiceContents,
///
/// [`Offer`]: crate::offers::offer::Offer
/// [`Refund`]: crate::offers::refund::Refund
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
enum InvoiceContents {
/// Contents for an [`Invoice`] corresponding to an [`Offer`].
///
}
/// Invoice-specific fields for an `invoice` message.
+#[derive(Clone, Debug, PartialEq)]
struct InvoiceFields {
payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
created_at: Duration,
&self.contents.fields().features
}
- /// The public key used to sign invoices.
+ /// The public key corresponding to the key used to sign the invoice.
pub fn signing_pubkey(&self) -> PublicKey {
self.contents.fields().signing_pubkey
}
- /// Signature of the invoice using [`Invoice::signing_pubkey`].
+ /// Signature of the invoice verified using [`Invoice::signing_pubkey`].
pub fn signature(&self) -> Signature {
self.signature
}
+ /// Hash that was used for signing the invoice.
+ pub fn signable_hash(&self) -> [u8; 32] {
+ merkle::message_digest(SIGNATURE_TAG, &self.bytes).as_ref().clone()
+ }
+
#[cfg(test)]
fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) =
#[cfg(feature = "std")]
fn is_offer_or_refund_expired(&self) -> bool {
match self {
- InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.offer.is_expired(),
+ InvoiceContents::ForOffer { invoice_request, .. } =>
+ invoice_request.inner.offer.is_expired(),
InvoiceContents::ForRefund { refund, .. } => refund.is_expired(),
}
}
/// Information needed to route a payment across a [`BlindedPath`].
#[derive(Clone, Debug, PartialEq)]
pub struct BlindedPayInfo {
- fee_base_msat: u32,
- fee_proportional_millionths: u32,
- cltv_expiry_delta: u16,
- htlc_minimum_msat: u64,
- htlc_maximum_msat: u64,
- features: BlindedHopFeatures,
+ /// Base fee charged (in millisatoshi) for the entire blinded path.
+ pub fee_base_msat: u32,
+
+ /// Liquidity fee charged (in millionths of the amount transferred) for the entire blinded path
+ /// (i.e., 10,000 is 1%).
+ pub fee_proportional_millionths: u32,
+
+ /// Number of blocks subtracted from an incoming HTLC's `cltv_expiry` for the entire blinded
+ /// path.
+ pub cltv_expiry_delta: u16,
+
+ /// The minimum HTLC value (in millisatoshi) that is acceptable to all channel peers on the
+ /// blinded path from the introduction node to the recipient, accounting for any fees, i.e., as
+ /// seen by the recipient.
+ pub htlc_minimum_msat: u64,
+
+ /// The maximum HTLC value (in millisatoshi) that is acceptable to all channel peers on the
+ /// blinded path from the introduction node to the recipient, accounting for any fees, i.e., as
+ /// seen by the recipient.
+ pub htlc_maximum_msat: u64,
+
+ /// Features set in `encrypted_data_tlv` for the `encrypted_recipient_data` TLV record in an
+ /// onion payload.
+ pub features: BlindedHopFeatures,
}
impl_writeable!(BlindedPayInfo, {
#[cfg(test)]
mod tests {
- use super::{DEFAULT_RELATIVE_EXPIRY, BlindedPayInfo, FallbackAddress, FullInvoiceTlvStreamRef, Invoice, InvoiceTlvStreamRef, SIGNATURE_TAG};
+ use super::{DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, Invoice, InvoiceTlvStreamRef, SIGNATURE_TAG};
use bitcoin::blockdata::script::Script;
use bitcoin::hashes::Hash;
use bitcoin::network::constants::Network;
- use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey, XOnlyPublicKey, self};
- use bitcoin::secp256k1::schnorr::Signature;
+ use bitcoin::secp256k1::{Message, Secp256k1, XOnlyPublicKey, self};
use bitcoin::util::address::{Address, Payload, WitnessVersion};
use bitcoin::util::schnorr::TweakedPublicKey;
- use core::convert::{Infallible, TryFrom};
+ use core::convert::TryFrom;
use core::time::Duration;
- use crate::ln::PaymentHash;
use crate::ln::msgs::DecodeError;
- use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
+ use crate::ln::features::Bolt12InvoiceFeatures;
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self};
- use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef};
+ use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity};
use crate::offers::parse::{ParseError, SemanticError};
use crate::offers::payer::PayerTlvStreamRef;
use crate::offers::refund::RefundBuilder;
- use crate::onion_message::{BlindedHop, BlindedPath};
+ use crate::offers::test_utils::*;
use crate::util::ser::{BigSize, Iterable, Writeable};
- fn payer_keys() -> KeyPair {
- let secp_ctx = Secp256k1::new();
- KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap())
- }
-
- fn payer_sign(digest: &Message) -> Result<Signature, Infallible> {
- let secp_ctx = Secp256k1::new();
- let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
- Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))
- }
-
- fn payer_pubkey() -> PublicKey {
- payer_keys().public_key()
- }
-
- fn recipient_keys() -> KeyPair {
- let secp_ctx = Secp256k1::new();
- KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap())
- }
-
- fn recipient_sign(digest: &Message) -> Result<Signature, Infallible> {
- let secp_ctx = Secp256k1::new();
- let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap());
- Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))
- }
-
- fn recipient_pubkey() -> PublicKey {
- recipient_keys().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>;
}
}
}
- fn payment_paths() -> Vec<(BlindedPath, BlindedPayInfo)> {
- let paths = vec![
- 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: pubkey(44), encrypted_payload: vec![0; 44] },
- ],
- },
- BlindedPath {
- introduction_node_id: pubkey(40),
- blinding_point: pubkey(41),
- blinded_hops: vec![
- BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] },
- BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] },
- ],
- },
- ];
-
- let payinfo = vec![
- BlindedPayInfo {
- fee_base_msat: 1,
- fee_proportional_millionths: 1_000,
- cltv_expiry_delta: 42,
- htlc_minimum_msat: 100,
- htlc_maximum_msat: 1_000_000_000_000,
- features: BlindedHopFeatures::empty(),
- },
- BlindedPayInfo {
- fee_base_msat: 1,
- fee_proportional_millionths: 1_000,
- cltv_expiry_delta: 42,
- htlc_minimum_msat: 100,
- htlc_maximum_msat: 1_000_000_000_000,
- features: BlindedHopFeatures::empty(),
- },
- ];
-
- paths.into_iter().zip(payinfo.into_iter()).collect()
- }
-
- fn payment_hash() -> PaymentHash {
- PaymentHash([42; 32])
- }
-
- fn now() -> Duration {
- std::time::SystemTime::now()
- .duration_since(std::time::SystemTime::UNIX_EPOCH)
- .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH")
- }
-
#[test]
fn builds_invoice_for_offer_with_defaults() {
let payment_paths = payment_paths();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths.clone(), payment_hash, now).unwrap()
+ .respond_with_no_std(payment_paths.clone(), payment_hash, now).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
).is_ok()
);
+ let digest = Message::from_slice(&invoice.signable_hash()).unwrap();
+ let pubkey = recipient_pubkey().into();
+ let secp_ctx = Secp256k1::verification_only();
+ assert!(secp_ctx.verify_schnorr(&invoice.signature, &digest, &pubkey).is_ok());
+
assert_eq!(
invoice.as_tlv_stream(),
(
let now = now();
let invoice = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
.build().unwrap()
- .respond_with(payment_paths.clone(), payment_hash, recipient_pubkey(), now).unwrap()
+ .respond_with_no_std(payment_paths.clone(), payment_hash, recipient_pubkey(), now)
+ .unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
}
}
+ #[cfg(feature = "std")]
+ #[test]
+ fn builds_invoice_from_offer_with_expiration() {
+ let future_expiry = Duration::from_secs(u64::max_value());
+ let past_expiry = Duration::from_secs(0);
+
+ if let Err(e) = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .absolute_expiry(future_expiry)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash())
+ .unwrap()
+ .build()
+ {
+ panic!("error building invoice: {:?}", e);
+ }
+
+ match OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .absolute_expiry(past_expiry)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build_unchecked()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash())
+ .unwrap()
+ .build()
+ {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, SemanticError::AlreadyExpired),
+ }
+ }
+
#[cfg(feature = "std")]
#[test]
fn builds_invoice_from_refund_with_expiration() {
if let Err(e) = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
.absolute_expiry(future_expiry)
.build().unwrap()
- .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap()
+ .respond_with(payment_paths(), payment_hash(), recipient_pubkey())
+ .unwrap()
.build()
{
panic!("error building invoice: {:?}", e);
match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
.absolute_expiry(past_expiry)
.build().unwrap()
- .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap()
+ .respond_with(payment_paths(), payment_hash(), recipient_pubkey())
+ .unwrap()
.build()
{
Ok(_) => panic!("expected error"),
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now).unwrap()
.relative_expiry(one_hour.as_secs() as u32)
.build().unwrap()
.sign(recipient_sign).unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now - one_hour).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now - one_hour).unwrap()
.relative_expiry(one_hour.as_secs() as u32 - 1)
.build().unwrap()
.sign(recipient_sign).unwrap();
.amount_msats(1001).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
assert_eq!(tlv_stream.amount, Some(1001));
}
+ #[test]
+ fn builds_invoice_with_quantity_from_request() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .supported_quantity(Quantity::Unbounded)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .quantity(2).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
+ assert_eq!(invoice.amount_msats(), 2000);
+ assert_eq!(tlv_stream.amount, Some(2000));
+
+ match OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .supported_quantity(Quantity::Unbounded)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .quantity(u64::max_value()).unwrap()
+ .build_unchecked()
+ .sign(payer_sign).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now())
+ {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, SemanticError::InvalidAmount),
+ }
+ }
+
#[test]
fn builds_invoice_with_fallback_address() {
let script = Script::new();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.fallback_v0_p2wsh(&script.wscript_hash())
.fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap())
.fallback_v1_p2tr_tweaked(&tweaked_pubkey)
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.allow_mpp()
.build().unwrap()
.sign(recipient_sign).unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(|_| Err(()))
{
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(payer_sign)
{
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.relative_expiry(3600)
.build().unwrap()
.sign(recipient_sign).unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.allow_mpp()
.build().unwrap()
.sign(recipient_sign).unwrap();
.build().unwrap()
.sign(payer_sign).unwrap();
let mut unsigned_invoice = invoice_request
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.fallback_v0_p2wsh(&script.wscript_hash())
.fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap())
.fallback_v1_p2tr_tweaked(&tweaked_pubkey)
.build().unwrap();
// Only standard addresses will be included.
- let mut fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap();
+ let fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap();
// Non-standard addresses
fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 41] });
fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 1] });
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.invoice
.write(&mut buffer).unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();
let last_signature_byte = invoice.bytes.last_mut().unwrap();
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
.build().unwrap()
.sign(payer_sign).unwrap()
- .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
.build().unwrap()
.sign(recipient_sign).unwrap();