said, we deeply welcome people contributing for the first time to an open source
project or pick up Rust while contributing. Don't be shy, you'll learn.
+For the project Code of Conduct, see our [website](https://lightningdevkit.org/code_of_conduct).
+
Communication Channels
-----------------------
contributions but rather understand the project culture before investing an
asymmetric number of hours on development compared to your merged work.
-Browsing through the [meeting minutes](https://github.com/lightningdevkit/rust-lightning/wiki/Meetings)
+Browsing through the [meeting minutes](https://github.com/lightningdevkit/rust-lightning/wiki/Meeting-Notes)
is a good first step. You will learn who is working on what, how releases are
drafted, what are the pending tasks to deliver, where you can contribute review
bandwidth, etc.
let mut payment_hash;
for _ in 0..256 {
payment_hash = PaymentHash(Sha256::hash(&[*payment_id; 1]).into_inner());
- if let Ok(payment_secret) = dest.create_inbound_payment_for_hash(payment_hash, None, 3600) {
+ if let Ok(payment_secret) = dest.create_inbound_payment_for_hash(payment_hash, None, 3600, None) {
return Some((payment_secret, payment_hash));
}
*payment_id = payment_id.wrapping_add(1);
let payment_hash = PaymentHash(Sha256::from_engine(sha).into_inner());
// Note that this may fail - our hashes may collide and we'll end up trying to
// double-register the same payment_hash.
- let _ = channelmanager.create_inbound_payment_for_hash(payment_hash, None, 1);
+ let _ = channelmanager.create_inbound_payment_for_hash(payment_hash, None, 1, None);
},
9 => {
for payment in payments_received.drain(..) {
use secp256k1::ecdsa::{RecoveryId, RecoverableSignature};
use secp256k1::PublicKey;
-use super::{Invoice, Sha256, TaggedField, ExpiryTime, MinFinalCltvExpiry, Fallback, PayeePubKey, InvoiceSignature, PositiveTimestamp,
+use super::{Invoice, Sha256, TaggedField, ExpiryTime, MinFinalCltvExpiryDelta, Fallback, PayeePubKey, InvoiceSignature, PositiveTimestamp,
SemanticError, PrivateRoute, ParseError, ParseOrSemanticError, Description, RawTaggedField, Currency, RawHrp, SiPrefix, RawInvoice,
constants, SignedRawInvoice, RawDataPart, InvoiceFeatures};
Ok(TaggedField::DescriptionHash(Sha256::from_base32(field_data)?)),
constants::TAG_EXPIRY_TIME =>
Ok(TaggedField::ExpiryTime(ExpiryTime::from_base32(field_data)?)),
- constants::TAG_MIN_FINAL_CLTV_EXPIRY =>
- Ok(TaggedField::MinFinalCltvExpiry(MinFinalCltvExpiry::from_base32(field_data)?)),
+ constants::TAG_MIN_FINAL_CLTV_EXPIRY_DELTA =>
+ Ok(TaggedField::MinFinalCltvExpiryDelta(MinFinalCltvExpiryDelta::from_base32(field_data)?)),
constants::TAG_FALLBACK =>
Ok(TaggedField::Fallback(Fallback::from_base32(field_data)?)),
constants::TAG_PRIVATE_ROUTE =>
}
}
-impl FromBase32 for MinFinalCltvExpiry {
+impl FromBase32 for MinFinalCltvExpiryDelta {
type Err = ParseError;
- fn from_base32(field_data: &[u5]) -> Result<MinFinalCltvExpiry, ParseError> {
+ fn from_base32(field_data: &[u5]) -> Result<MinFinalCltvExpiryDelta, ParseError> {
let expiry = parse_int_be::<u64, u5>(field_data, 32);
if let Some(expiry) = expiry {
- Ok(MinFinalCltvExpiry(expiry))
+ Ok(MinFinalCltvExpiryDelta(expiry))
} else {
Err(ParseError::IntegerOverflowError)
}
}
#[test]
- fn test_parse_min_final_cltv_expiry() {
- use crate::MinFinalCltvExpiry;
+ fn test_parse_min_final_cltv_expiry_delta() {
+ use crate::MinFinalCltvExpiryDelta;
use bech32::FromBase32;
let input = from_bech32("pr".as_bytes());
- let expected = Ok(MinFinalCltvExpiry(35));
+ let expected = Ok(MinFinalCltvExpiryDelta(35));
- assert_eq!(MinFinalCltvExpiry::from_base32(&input), expected);
+ assert_eq!(MinFinalCltvExpiryDelta::from_base32(&input), expected);
}
#[test]
/// Default minimum final CLTV expiry as defined by [BOLT 11].
///
/// Note that this is *not* the same value as rust-lightning's minimum CLTV expiry, which is
-/// provided in [`MIN_FINAL_CLTV_EXPIRY`].
+/// provided in [`MIN_FINAL_CLTV_EXPIRY_DELTA`].
///
/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
-/// [`MIN_FINAL_CLTV_EXPIRY`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY
-pub const DEFAULT_MIN_FINAL_CLTV_EXPIRY: u64 = 18;
+/// [`MIN_FINAL_CLTV_EXPIRY_DELTA`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA
+pub const DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA: u64 = 18;
/// Builder for `Invoice`s. It's the most convenient and advised way to use this library. It ensures
/// that only a semantically and syntactically correct Invoice can be built using it.
/// .payment_hash(payment_hash)
/// .payment_secret(payment_secret)
/// .current_timestamp()
-/// .min_final_cltv_expiry(144)
+/// .min_final_cltv_expiry_delta(144)
/// .build_signed(|hash| {
/// Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)
/// })
PayeePubKey(PayeePubKey),
DescriptionHash(Sha256),
ExpiryTime(ExpiryTime),
- MinFinalCltvExpiry(MinFinalCltvExpiry),
+ MinFinalCltvExpiryDelta(MinFinalCltvExpiryDelta),
Fallback(Fallback),
PrivateRoute(PrivateRoute),
PaymentSecret(PaymentSecret),
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct ExpiryTime(Duration);
-/// `min_final_cltv_expiry` to use for the last HTLC in the route
+/// `min_final_cltv_expiry_delta` to use for the last HTLC in the route
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
-pub struct MinFinalCltvExpiry(pub u64);
+pub struct MinFinalCltvExpiryDelta(pub u64);
// TODO: better types instead onf byte arrays
/// Fallback address in case no LN payment is possible
pub const TAG_PAYEE_PUB_KEY: u8 = 19;
pub const TAG_DESCRIPTION_HASH: u8 = 23;
pub const TAG_EXPIRY_TIME: u8 = 6;
- pub const TAG_MIN_FINAL_CLTV_EXPIRY: u8 = 24;
+ pub const TAG_MIN_FINAL_CLTV_EXPIRY_DELTA: u8 = 24;
pub const TAG_FALLBACK: u8 = 9;
pub const TAG_PRIVATE_ROUTE: u8 = 3;
pub const TAG_PAYMENT_SECRET: u8 = 16;
}
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, tb::False, S> {
- /// Sets `min_final_cltv_expiry`.
- pub fn min_final_cltv_expiry(mut self, min_final_cltv_expiry: u64) -> InvoiceBuilder<D, H, T, tb::True, S> {
- self.tagged_fields.push(TaggedField::MinFinalCltvExpiry(MinFinalCltvExpiry(min_final_cltv_expiry)));
+ /// Sets `min_final_cltv_expiry_delta`.
+ pub fn min_final_cltv_expiry_delta(mut self, min_final_cltv_expiry_delta: u64) -> InvoiceBuilder<D, H, T, tb::True, S> {
+ self.tagged_fields.push(TaggedField::MinFinalCltvExpiryDelta(MinFinalCltvExpiryDelta(min_final_cltv_expiry_delta)));
self.set_flags()
}
}
find_extract!(self.known_tagged_fields(), TaggedField::ExpiryTime(ref x), x)
}
- pub fn min_final_cltv_expiry(&self) -> Option<&MinFinalCltvExpiry> {
- find_extract!(self.known_tagged_fields(), TaggedField::MinFinalCltvExpiry(ref x), x)
+ pub fn min_final_cltv_expiry_delta(&self) -> Option<&MinFinalCltvExpiryDelta> {
+ find_extract!(self.known_tagged_fields(), TaggedField::MinFinalCltvExpiryDelta(ref x), x)
}
pub fn payment_secret(&self) -> Option<&PaymentSecret> {
.unwrap_or_else(|| Duration::new(u64::max_value(), 1_000_000_000 - 1)) < at_time
}
- /// Returns the invoice's `min_final_cltv_expiry` time, if present, otherwise
- /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY`].
- pub fn min_final_cltv_expiry(&self) -> u64 {
- self.signed_invoice.min_final_cltv_expiry()
+ /// Returns the invoice's `min_final_cltv_expiry_delta` time, if present, otherwise
+ /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA`].
+ pub fn min_final_cltv_expiry_delta(&self) -> u64 {
+ self.signed_invoice.min_final_cltv_expiry_delta()
.map(|x| x.0)
- .unwrap_or(DEFAULT_MIN_FINAL_CLTV_EXPIRY)
+ .unwrap_or(DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA)
}
/// Returns a list of all fallback addresses
TaggedField::PayeePubKey(_) => constants::TAG_PAYEE_PUB_KEY,
TaggedField::DescriptionHash(_) => constants::TAG_DESCRIPTION_HASH,
TaggedField::ExpiryTime(_) => constants::TAG_EXPIRY_TIME,
- TaggedField::MinFinalCltvExpiry(_) => constants::TAG_MIN_FINAL_CLTV_EXPIRY,
+ TaggedField::MinFinalCltvExpiryDelta(_) => constants::TAG_MIN_FINAL_CLTV_EXPIRY_DELTA,
TaggedField::Fallback(_) => constants::TAG_FALLBACK,
TaggedField::PrivateRoute(_) => constants::TAG_PRIVATE_ROUTE,
TaggedField::PaymentSecret(_) => constants::TAG_PAYMENT_SECRET,
///
/// [phantom invoices]: crate::utils::create_phantom_invoice
MissingRouteHints,
+
+ /// The provided `min_final_cltv_expiry_delta` was less than [`MIN_FINAL_CLTV_EXPIRY_DELTA`].
+ ///
+ /// [`MIN_FINAL_CLTV_EXPIRY_DELTA`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA
+ MinFinalCltvExpiryDeltaTooShort,
}
impl Display for CreationError {
CreationError::TimestampOutOfBounds => f.write_str("The Unix timestamp of the supplied date is less than zero or greater than 35-bits"),
CreationError::InvalidAmount => f.write_str("The supplied millisatoshi amount was greater than the total bitcoin supply"),
CreationError::MissingRouteHints => f.write_str("The invoice required route hints and they weren't provided"),
+ CreationError::MinFinalCltvExpiryDeltaTooShort => f.write_str(
+ "The supplied final CLTV expiry delta was less than LDK's `MIN_FINAL_CLTV_EXPIRY_DELTA`"),
}
}
}
let builder = InvoiceBuilder::new(Currency::Bitcoin)
.payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap())
.duration_since_epoch(Duration::from_secs(1234567))
- .min_final_cltv_expiry(144);
+ .min_final_cltv_expiry_delta(144);
let too_long_string = String::from_iter(
(0..1024).map(|_| '?')
.duration_since_epoch(Duration::from_secs(1234567))
.payee_pub_key(public_key.clone())
.expiry_time(Duration::from_secs(54321))
- .min_final_cltv_expiry(144)
+ .min_final_cltv_expiry_delta(144)
.fallback(Fallback::PubKeyHash([0;20]))
.private_route(route_1.clone())
.private_route(route_2.clone())
);
assert_eq!(invoice.payee_pub_key(), Some(&public_key));
assert_eq!(invoice.expiry_time(), Duration::from_secs(54321));
- assert_eq!(invoice.min_final_cltv_expiry(), 144);
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), 144);
assert_eq!(invoice.fallbacks(), vec![&Fallback::PubKeyHash([0;20])]);
assert_eq!(invoice.private_routes(), vec![&PrivateRoute(route_1), &PrivateRoute(route_2)]);
assert_eq!(
.unwrap();
let invoice = Invoice::from_signed(signed_invoice).unwrap();
- assert_eq!(invoice.min_final_cltv_expiry(), DEFAULT_MIN_FINAL_CLTV_EXPIRY);
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA);
assert_eq!(invoice.expiry_time(), Duration::from_secs(DEFAULT_EXPIRY_TIME));
assert!(!invoice.would_expire(Duration::from_secs(1234568)));
}
let route_params = RouteParameters {
payment_params,
final_value_msat: invoice.amount_milli_satoshis().or(amount_msats).unwrap(),
- final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32,
+ final_cltv_expiry_delta: invoice.min_final_cltv_expiry_delta() as u32,
};
let send_payment = |route: &Route| {
.payment_hash(payment_hash)
.payment_secret(PaymentSecret([0; 32]))
.duration_since_epoch(duration_since_epoch())
- .min_final_cltv_expiry(144)
+ .min_final_cltv_expiry_delta(144)
.amount_milli_satoshis(128)
.build_signed(|hash| {
Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)
.payment_hash(payment_hash)
.payment_secret(PaymentSecret([0; 32]))
.duration_since_epoch(duration_since_epoch())
- .min_final_cltv_expiry(144)
+ .min_final_cltv_expiry_delta(144)
.build_signed(|hash| {
Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)
})
.payment_hash(payment_hash)
.payment_secret(PaymentSecret([0; 32]))
.duration_since_epoch(duration)
- .min_final_cltv_expiry(144)
+ .min_final_cltv_expiry_delta(144)
.amount_milli_satoshis(128)
.build_signed(|hash| {
Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)
RouteParameters {
payment_params,
final_value_msat,
- final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32,
+ final_cltv_expiry_delta: invoice.min_final_cltv_expiry_delta() as u32,
}
}
}
assert!(invoice_payer.pay_invoice(&create_invoice_from_channelmanager_and_duration_since_epoch(
&nodes[1].node, nodes[1].keys_manager, nodes[1].logger, Currency::Bitcoin,
- Some(100_010_000), "Invoice".to_string(), duration_since_epoch(), 3600).unwrap())
+ Some(100_010_000), "Invoice".to_string(), duration_since_epoch(), 3600, None).unwrap())
.is_ok());
let htlc_msgs = nodes[0].node.get_and_clear_pending_msg_events();
assert_eq!(htlc_msgs.len(), 2);
assert!(invoice_payer.pay_invoice(&create_invoice_from_channelmanager_and_duration_since_epoch(
&nodes[1].node, nodes[1].keys_manager, nodes[1].logger, Currency::Bitcoin,
- Some(100_010_000), "Invoice".to_string(), duration_since_epoch(), 3600).unwrap())
+ Some(100_010_000), "Invoice".to_string(), duration_since_epoch(), 3600, None).unwrap())
.is_ok());
let htlc_msgs = nodes[0].node.get_and_clear_pending_msg_events();
assert_eq!(htlc_msgs.len(), 2);
assert!(invoice_payer.pay_invoice(&create_invoice_from_channelmanager_and_duration_since_epoch(
&nodes[1].node, nodes[1].keys_manager, nodes[1].logger, Currency::Bitcoin,
- Some(100_010_000), "Invoice".to_string(), duration_since_epoch(), 3600).unwrap())
+ Some(100_010_000), "Invoice".to_string(), duration_since_epoch(), 3600, None).unwrap())
.is_ok());
let htlc_updates = SendEvent::from_node(&nodes[0]);
check_added_monitors!(nodes[0], 1);
use bech32::{ToBase32, u5, WriteBase32, Base32Len};
use crate::prelude::*;
-use super::{Invoice, Sha256, TaggedField, ExpiryTime, MinFinalCltvExpiry, Fallback, PayeePubKey, InvoiceSignature, PositiveTimestamp,
+use super::{Invoice, Sha256, TaggedField, ExpiryTime, MinFinalCltvExpiryDelta, Fallback, PayeePubKey, InvoiceSignature, PositiveTimestamp,
PrivateRoute, Description, RawTaggedField, Currency, RawHrp, SiPrefix, constants, SignedRawInvoice, RawDataPart};
/// Converts a stream of bytes written to it to base32. On finalization the according padding will
}
}
-impl ToBase32 for MinFinalCltvExpiry {
+impl ToBase32 for MinFinalCltvExpiryDelta {
fn write_base32<W: WriteBase32>(&self, writer: &mut W) -> Result<(), <W as WriteBase32>::Err> {
writer.write(&encode_int_be_base32(self.0))
}
}
-impl Base32Len for MinFinalCltvExpiry {
+impl Base32Len for MinFinalCltvExpiryDelta {
fn base32_len(&self) -> usize {
encoded_int_be_base32_size(self.0)
}
TaggedField::ExpiryTime(ref duration) => {
write_tagged_field(writer, constants::TAG_EXPIRY_TIME, duration)
},
- TaggedField::MinFinalCltvExpiry(ref expiry) => {
- write_tagged_field(writer, constants::TAG_MIN_FINAL_CLTV_EXPIRY, expiry)
+ TaggedField::MinFinalCltvExpiryDelta(ref expiry) => {
+ write_tagged_field(writer, constants::TAG_MIN_FINAL_CLTV_EXPIRY_DELTA, expiry)
},
TaggedField::Fallback(ref fallback_address) => {
write_tagged_field(writer, constants::TAG_FALLBACK, fallback_address)
use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator};
use lightning::chain::keysinterface::{Recipient, NodeSigner, SignerProvider, EntropySource};
use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret};
-use lightning::ln::channelmanager::{ChannelDetails, ChannelManager, PaymentId, PaymentSendFailure, MIN_FINAL_CLTV_EXPIRY};
+use lightning::ln::channelmanager::{ChannelDetails, ChannelManager, PaymentId, PaymentSendFailure, MIN_FINAL_CLTV_EXPIRY_DELTA};
#[cfg(feature = "std")]
use lightning::ln::channelmanager::{PhantomRouteHints, MIN_CLTV_EXPIRY_DELTA};
use lightning::ln::inbound_payment::{create, create_from_hash, ExpandedKey};
/// `invoice_expiry_delta_secs` describes the number of seconds that the invoice is valid for
/// in excess of the current time.
///
+/// You can specify a custom `min_final_cltv_expiry_delta`, or let LDK default it to
+/// [`MIN_FINAL_CLTV_EXPIRY_DELTA`]. The provided expiry must be at least [`MIN_FINAL_CLTV_EXPIRY_DELTA`] - 3.
+/// Note that LDK will add a buffer of 3 blocks to the delta to allow for up to a few new block
+/// confirmations during routing.
+///
/// Note that the provided `keys_manager`'s `NodeSigner` implementation must support phantom
/// invoices in its `sign_invoice` implementation ([`PhantomKeysManager`] satisfies this
/// requirement).
/// [`ChannelManager::create_inbound_payment`]: lightning::ln::channelmanager::ChannelManager::create_inbound_payment
/// [`ChannelManager::create_inbound_payment_for_hash`]: lightning::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash
/// [`PhantomRouteHints::channels`]: lightning::ln::channelmanager::PhantomRouteHints::channels
+/// [`MIN_FINAL_CLTV_EXPIRY_DETLA`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA
pub fn create_phantom_invoice<ES: Deref, NS: Deref, L: Deref>(
amt_msat: Option<u64>, payment_hash: Option<PaymentHash>, description: String,
invoice_expiry_delta_secs: u32, phantom_route_hints: Vec<PhantomRouteHints>, entropy_source: ES,
- node_signer: NS, logger: L, network: Currency,
+ node_signer: NS, logger: L, network: Currency, min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
ES::Target: EntropySource,
let description = InvoiceDescription::Direct(&description,);
_create_phantom_invoice::<ES, NS, L>(
amt_msat, payment_hash, description, invoice_expiry_delta_secs, phantom_route_hints,
- entropy_source, node_signer, logger, network,
+ entropy_source, node_signer, logger, network, min_final_cltv_expiry_delta,
)
}
pub fn create_phantom_invoice_with_description_hash<ES: Deref, NS: Deref, L: Deref>(
amt_msat: Option<u64>, payment_hash: Option<PaymentHash>, invoice_expiry_delta_secs: u32,
description_hash: Sha256, phantom_route_hints: Vec<PhantomRouteHints>, entropy_source: ES,
- node_signer: NS, logger: L, network: Currency
+ node_signer: NS, logger: L, network: Currency, min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
ES::Target: EntropySource,
_create_phantom_invoice::<ES, NS, L>(
amt_msat, payment_hash, InvoiceDescription::Hash(&description_hash),
invoice_expiry_delta_secs, phantom_route_hints, entropy_source, node_signer, logger, network,
+ min_final_cltv_expiry_delta,
)
}
fn _create_phantom_invoice<ES: Deref, NS: Deref, L: Deref>(
amt_msat: Option<u64>, payment_hash: Option<PaymentHash>, description: InvoiceDescription,
invoice_expiry_delta_secs: u32, phantom_route_hints: Vec<PhantomRouteHints>, entropy_source: ES,
- node_signer: NS, logger: L, network: Currency,
+ node_signer: NS, logger: L, network: Currency, min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
ES::Target: EntropySource,
));
}
+ if min_final_cltv_expiry_delta.is_some() && min_final_cltv_expiry_delta.unwrap().saturating_add(3) < MIN_FINAL_CLTV_EXPIRY_DELTA {
+ return Err(SignOrCreationError::CreationError(CreationError::MinFinalCltvExpiryDeltaTooShort));
+ }
+
let invoice = match description {
InvoiceDescription::Direct(description) => {
InvoiceBuilder::new(network).description(description.0.clone())
.duration_since(UNIX_EPOCH)
.expect("Time must be > 1970")
.as_secs(),
+ min_final_cltv_expiry_delta,
)
.map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?;
(payment_hash, payment_secret)
.duration_since(UNIX_EPOCH)
.expect("Time must be > 1970")
.as_secs(),
+ min_final_cltv_expiry_delta,
)
.map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?
};
.current_timestamp()
.payment_hash(Hash::from_slice(&payment_hash.0).unwrap())
.payment_secret(payment_secret)
- .min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into())
+ .min_final_cltv_expiry_delta(
+ // Add a buffer of 3 to the delta if present, otherwise use LDK's minimum.
+ min_final_cltv_expiry_delta.map(|x| x.saturating_add(3)).unwrap_or(MIN_FINAL_CLTV_EXPIRY_DELTA).into())
.expiry_time(Duration::from_secs(invoice_expiry_delta_secs.into()));
if let Some(amt) = amt_msat {
invoice = invoice.amount_milli_satoshis(amt);
///
/// `invoice_expiry_delta_secs` describes the number of seconds that the invoice is valid for
/// in excess of the current time.
+///
+/// You can specify a custom `min_final_cltv_expiry_delta`, or let LDK default it to
+/// [`MIN_FINAL_CLTV_EXPIRY_DELTA`]. The provided expiry must be at least [`MIN_FINAL_CLTV_EXPIRY_DELTA`].
+/// Note that LDK will add a buffer of 3 blocks to the delta to allow for up to a few new block
+/// confirmations during routing.
+///
+/// [`MIN_FINAL_CLTV_EXPIRY_DETLA`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA
pub fn create_invoice_from_channelmanager<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>, node_signer: NS, logger: L,
- network: Currency, amt_msat: Option<u64>, description: String, invoice_expiry_delta_secs: u32
+ network: Currency, amt_msat: Option<u64>, description: String, invoice_expiry_delta_secs: u32,
+ min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
.expect("for the foreseeable future this shouldn't happen");
create_invoice_from_channelmanager_and_duration_since_epoch(
channelmanager, node_signer, logger, network, amt_msat,
- description, duration, invoice_expiry_delta_secs
+ description, duration, invoice_expiry_delta_secs, min_final_cltv_expiry_delta,
)
}
///
/// `invoice_expiry_delta_secs` describes the number of seconds that the invoice is valid for
/// in excess of the current time.
+///
+/// You can specify a custom `min_final_cltv_expiry_delta`, or let LDK default it to
+/// [`MIN_FINAL_CLTV_EXPIRY_DELTA`]. The provided expiry must be at least [`MIN_FINAL_CLTV_EXPIRY_DELTA`].
+/// Note that LDK will add a buffer of 3 blocks to the delta to allow for up to a few new block
+/// confirmations during routing.
+///
+/// [`MIN_FINAL_CLTV_EXPIRY_DETLA`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA
pub fn create_invoice_from_channelmanager_with_description_hash<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>, node_signer: NS, logger: L,
network: Currency, amt_msat: Option<u64>, description_hash: Sha256,
- invoice_expiry_delta_secs: u32
+ invoice_expiry_delta_secs: u32, min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
channelmanager, node_signer, logger, network, amt_msat,
- description_hash, duration, invoice_expiry_delta_secs
+ description_hash, duration, invoice_expiry_delta_secs, min_final_cltv_expiry_delta,
)
}
pub fn create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>, node_signer: NS, logger: L,
network: Currency, amt_msat: Option<u64>, description_hash: Sha256,
- duration_since_epoch: Duration, invoice_expiry_delta_secs: u32
+ duration_since_epoch: Duration, invoice_expiry_delta_secs: u32, min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
_create_invoice_from_channelmanager_and_duration_since_epoch(
channelmanager, node_signer, logger, network, amt_msat,
InvoiceDescription::Hash(&description_hash),
- duration_since_epoch, invoice_expiry_delta_secs
+ duration_since_epoch, invoice_expiry_delta_secs, min_final_cltv_expiry_delta,
)
}
pub fn create_invoice_from_channelmanager_and_duration_since_epoch<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>, node_signer: NS, logger: L,
network: Currency, amt_msat: Option<u64>, description: String, duration_since_epoch: Duration,
- invoice_expiry_delta_secs: u32
+ invoice_expiry_delta_secs: u32, min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
InvoiceDescription::Direct(
&Description::new(description).map_err(SignOrCreationError::CreationError)?,
),
- duration_since_epoch, invoice_expiry_delta_secs
+ duration_since_epoch, invoice_expiry_delta_secs, min_final_cltv_expiry_delta,
)
}
fn _create_invoice_from_channelmanager_and_duration_since_epoch<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>, node_signer: NS, logger: L,
network: Currency, amt_msat: Option<u64>, description: InvoiceDescription,
- duration_since_epoch: Duration, invoice_expiry_delta_secs: u32
+ duration_since_epoch: Duration, invoice_expiry_delta_secs: u32, min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
R::Target: Router,
L::Target: Logger,
{
+ if min_final_cltv_expiry_delta.is_some() && min_final_cltv_expiry_delta.unwrap().saturating_add(3) < MIN_FINAL_CLTV_EXPIRY_DELTA {
+ return Err(SignOrCreationError::CreationError(CreationError::MinFinalCltvExpiryDeltaTooShort));
+ }
+
// `create_inbound_payment` only returns an error if the amount is greater than the total bitcoin
// supply.
let (payment_hash, payment_secret) = channelmanager
- .create_inbound_payment(amt_msat, invoice_expiry_delta_secs)
+ .create_inbound_payment(amt_msat, invoice_expiry_delta_secs, min_final_cltv_expiry_delta)
.map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?;
_create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash(
- channelmanager, node_signer, logger, network, amt_msat, description, duration_since_epoch, invoice_expiry_delta_secs, payment_hash, payment_secret)
+ channelmanager, node_signer, logger, network, amt_msat, description, duration_since_epoch,
+ invoice_expiry_delta_secs, payment_hash, payment_secret, min_final_cltv_expiry_delta)
}
/// See [`create_invoice_from_channelmanager_and_duration_since_epoch`]
pub fn create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>, node_signer: NS, logger: L,
network: Currency, amt_msat: Option<u64>, description: String, duration_since_epoch: Duration,
- invoice_expiry_delta_secs: u32, payment_hash: PaymentHash
+ invoice_expiry_delta_secs: u32, payment_hash: PaymentHash, min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
L::Target: Logger,
{
let payment_secret = channelmanager
- .create_inbound_payment_for_hash(payment_hash,amt_msat, invoice_expiry_delta_secs)
+ .create_inbound_payment_for_hash(payment_hash, amt_msat, invoice_expiry_delta_secs,
+ min_final_cltv_expiry_delta)
.map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?;
_create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash(
channelmanager, node_signer, logger, network, amt_msat,
InvoiceDescription::Direct(
&Description::new(description).map_err(SignOrCreationError::CreationError)?,
),
- duration_since_epoch, invoice_expiry_delta_secs, payment_hash, payment_secret
+ duration_since_epoch, invoice_expiry_delta_secs, payment_hash, payment_secret,
+ min_final_cltv_expiry_delta,
)
}
fn _create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>, node_signer: NS, logger: L,
network: Currency, amt_msat: Option<u64>, description: InvoiceDescription, duration_since_epoch: Duration,
- invoice_expiry_delta_secs: u32, payment_hash: PaymentHash, payment_secret: PaymentSecret
+ invoice_expiry_delta_secs: u32, payment_hash: PaymentHash, payment_secret: PaymentSecret,
+ min_final_cltv_expiry_delta: Option<u16>,
) -> Result<Invoice, SignOrCreationError<()>>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
let our_node_pubkey = channelmanager.get_our_node_id();
let channels = channelmanager.list_channels();
+ if min_final_cltv_expiry_delta.is_some() && min_final_cltv_expiry_delta.unwrap().saturating_add(3) < MIN_FINAL_CLTV_EXPIRY_DELTA {
+ return Err(SignOrCreationError::CreationError(CreationError::MinFinalCltvExpiryDeltaTooShort));
+ }
+
log_trace!(logger, "Creating invoice with payment hash {}", log_bytes!(payment_hash.0));
let invoice = match description {
.payment_hash(Hash::from_slice(&payment_hash.0).unwrap())
.payment_secret(payment_secret)
.basic_mpp()
- .min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into())
+ .min_final_cltv_expiry_delta(
+ // Add a buffer of 3 to the delta if present, otherwise use LDK's minimum.
+ min_final_cltv_expiry_delta.map(|x| x.saturating_add(3)).unwrap_or(MIN_FINAL_CLTV_EXPIRY_DELTA).into())
.expiry_time(Duration::from_secs(invoice_expiry_delta_secs.into()));
if let Some(amt) = amt_msat {
invoice = invoice.amount_milli_satoshis(amt);
#[cfg(test)]
mod test {
use core::time::Duration;
- use crate::{Currency, Description, InvoiceDescription};
+ use crate::{Currency, Description, InvoiceDescription, SignOrCreationError, CreationError};
use bitcoin_hashes::{Hash, sha256};
use bitcoin_hashes::sha256::Hash as Sha256;
use lightning::chain::keysinterface::{EntropySource, PhantomKeysManager};
use lightning::ln::{PaymentPreimage, PaymentHash};
- use lightning::ln::channelmanager::{PhantomRouteHints, MIN_FINAL_CLTV_EXPIRY, PaymentId};
+ use lightning::ln::channelmanager::{PhantomRouteHints, MIN_FINAL_CLTV_EXPIRY_DELTA, PaymentId};
use lightning::ln::functional_test_utils::*;
use lightning::ln::msgs::ChannelMessageHandler;
use lightning::routing::router::{PaymentParameters, RouteParameters, find_route};
let invoice = create_invoice_from_channelmanager_and_duration_since_epoch(
&nodes[1].node, nodes[1].keys_manager, nodes[1].logger, Currency::BitcoinTestnet,
Some(10_000), "test".to_string(), Duration::from_secs(1234567),
- non_default_invoice_expiry_secs).unwrap();
+ non_default_invoice_expiry_secs, None).unwrap();
assert_eq!(invoice.amount_pico_btc(), Some(100_000));
- assert_eq!(invoice.min_final_cltv_expiry(), MIN_FINAL_CLTV_EXPIRY as u64);
+ // If no `min_final_cltv_expiry_delta` is specified, then it should be `MIN_FINAL_CLTV_EXPIRY_DELTA`.
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64);
assert_eq!(invoice.description(), InvoiceDescription::Direct(&Description("test".to_string())));
assert_eq!(invoice.expiry_time(), Duration::from_secs(non_default_invoice_expiry_secs.into()));
let route_params = RouteParameters {
payment_params,
final_value_msat: invoice.amount_milli_satoshis().unwrap(),
- final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32,
+ final_cltv_expiry_delta: invoice.min_final_cltv_expiry_delta() as u32,
};
let first_hops = nodes[0].node.list_usable_channels();
let network_graph = &node_cfgs[0].network_graph;
assert_eq!(events.len(), 2);
}
+ fn do_create_invoice_min_final_cltv_delta(with_custom_delta: bool) {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+ let custom_min_final_cltv_expiry_delta = Some(50);
+
+ let invoice = crate::utils::create_invoice_from_channelmanager_and_duration_since_epoch(
+ &nodes[1].node, nodes[1].keys_manager, nodes[1].logger, Currency::BitcoinTestnet,
+ Some(10_000), "".into(), Duration::from_secs(1234567), 3600,
+ if with_custom_delta { custom_min_final_cltv_expiry_delta } else { None },
+ ).unwrap();
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), if with_custom_delta {
+ custom_min_final_cltv_expiry_delta.unwrap() + 3 /* Buffer */} else { MIN_FINAL_CLTV_EXPIRY_DELTA } as u64);
+ }
+
+ #[test]
+ fn test_create_invoice_custom_min_final_cltv_delta() {
+ do_create_invoice_min_final_cltv_delta(true);
+ do_create_invoice_min_final_cltv_delta(false);
+ }
+
+ #[test]
+ fn create_invoice_min_final_cltv_delta_equals_htlc_fail_buffer() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+ let custom_min_final_cltv_expiry_delta = Some(21);
+
+ let invoice = crate::utils::create_invoice_from_channelmanager_and_duration_since_epoch(
+ &nodes[1].node, nodes[1].keys_manager, nodes[1].logger, Currency::BitcoinTestnet,
+ Some(10_000), "".into(), Duration::from_secs(1234567), 3600,
+ custom_min_final_cltv_expiry_delta,
+ ).unwrap();
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64);
+ }
+
#[test]
fn test_create_invoice_with_description_hash() {
let chanmon_cfgs = create_chanmon_cfgs(2);
let description_hash = crate::Sha256(Hash::hash("Testing description_hash".as_bytes()));
let invoice = crate::utils::create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
&nodes[1].node, nodes[1].keys_manager, nodes[1].logger, Currency::BitcoinTestnet,
- Some(10_000), description_hash, Duration::from_secs(1234567), 3600
+ Some(10_000), description_hash, Duration::from_secs(1234567), 3600, None,
).unwrap();
assert_eq!(invoice.amount_pico_btc(), Some(100_000));
- assert_eq!(invoice.min_final_cltv_expiry(), MIN_FINAL_CLTV_EXPIRY as u64);
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64);
assert_eq!(invoice.description(), InvoiceDescription::Hash(&crate::Sha256(Sha256::hash("Testing description_hash".as_bytes()))));
}
let invoice = crate::utils::create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash(
&nodes[1].node, nodes[1].keys_manager, nodes[1].logger, Currency::BitcoinTestnet,
Some(10_000), "test".to_string(), Duration::from_secs(1234567), 3600,
- payment_hash
+ payment_hash, None,
).unwrap();
assert_eq!(invoice.amount_pico_btc(), Some(100_000));
- assert_eq!(invoice.min_final_cltv_expiry(), MIN_FINAL_CLTV_EXPIRY as u64);
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64);
assert_eq!(invoice.description(), InvoiceDescription::Direct(&Description("test".to_string())));
assert_eq!(invoice.payment_hash(), &sha256::Hash::from_slice(&payment_hash.0[..]).unwrap());
}
let invoice = create_invoice_from_channelmanager_and_duration_since_epoch(
&invoice_node.node, invoice_node.keys_manager, invoice_node.logger,
Currency::BitcoinTestnet, invoice_amt, "test".to_string(), Duration::from_secs(1234567),
- 3600).unwrap();
+ 3600, None).unwrap();
let hints = invoice.private_routes();
for hint in hints {
let invoice =
crate::utils::create_phantom_invoice::<&test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestLogger>(
Some(payment_amt), payment_hash, "test".to_string(), non_default_invoice_expiry_secs,
- route_hints, &nodes[1].keys_manager, &nodes[1].keys_manager, &nodes[1].logger, Currency::BitcoinTestnet
+ route_hints, &nodes[1].keys_manager, &nodes[1].keys_manager, &nodes[1].logger,
+ Currency::BitcoinTestnet, None,
).unwrap();
let (payment_hash, payment_secret) = (PaymentHash(invoice.payment_hash().into_inner()), *invoice.payment_secret());
let payment_preimage = if user_generated_pmt_hash {
nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap()
};
- assert_eq!(invoice.min_final_cltv_expiry(), MIN_FINAL_CLTV_EXPIRY as u64);
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64);
assert_eq!(invoice.description(), InvoiceDescription::Direct(&Description("test".to_string())));
assert_eq!(invoice.route_hints().len(), 2);
assert_eq!(invoice.expiry_time(), Duration::from_secs(non_default_invoice_expiry_secs.into()));
let params = RouteParameters {
payment_params,
final_value_msat: invoice.amount_milli_satoshis().unwrap(),
- final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32,
+ final_cltv_expiry_delta: invoice.min_final_cltv_expiry_delta() as u32,
};
let first_hops = nodes[0].node.list_usable_channels();
let network_graph = &node_cfgs[0].network_graph;
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 2, 100000, 10001);
let payment_amt = 20_000;
- let (payment_hash, _payment_secret) = nodes[1].node.create_inbound_payment(Some(payment_amt), 3600).unwrap();
+ let (payment_hash, _payment_secret) = nodes[1].node.create_inbound_payment(Some(payment_amt), 3600, None).unwrap();
let route_hints = vec![
nodes[1].node.get_phantom_route_hints(),
nodes[2].node.get_phantom_route_hints(),
];
- let invoice = crate::utils::create_phantom_invoice::<&test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestLogger>(Some(payment_amt), Some(payment_hash), "test".to_string(), 3600, route_hints, &nodes[1].keys_manager, &nodes[1].keys_manager, &nodes[1].logger, Currency::BitcoinTestnet).unwrap();
+ let invoice = crate::utils::create_phantom_invoice::<&test_utils::TestKeysInterface,
+ &test_utils::TestKeysInterface, &test_utils::TestLogger>(Some(payment_amt), Some(payment_hash),
+ "test".to_string(), 3600, route_hints, &nodes[1].keys_manager, &nodes[1].keys_manager,
+ &nodes[1].logger, Currency::BitcoinTestnet, None).unwrap();
let chan_0_1 = &nodes[1].node.list_usable_channels()[0];
assert_eq!(invoice.route_hints()[0].0[0].htlc_minimum_msat, chan_0_1.inbound_htlc_minimum_msat);
&test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestLogger,
>(
Some(payment_amt), None, non_default_invoice_expiry_secs, description_hash,
- route_hints, &nodes[1].keys_manager, &nodes[1].keys_manager, &nodes[1].logger, Currency::BitcoinTestnet
+ route_hints, &nodes[1].keys_manager, &nodes[1].keys_manager, &nodes[1].logger,
+ Currency::BitcoinTestnet, None,
)
.unwrap();
assert_eq!(invoice.amount_pico_btc(), Some(200_000));
- assert_eq!(invoice.min_final_cltv_expiry(), MIN_FINAL_CLTV_EXPIRY as u64);
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64);
assert_eq!(invoice.expiry_time(), Duration::from_secs(non_default_invoice_expiry_secs.into()));
assert_eq!(invoice.description(), InvoiceDescription::Hash(&crate::Sha256(Sha256::hash("Description hash phantom invoice".as_bytes()))));
}
+ #[test]
+ #[cfg(feature = "std")]
+ fn create_phantom_invoice_with_custom_payment_hash_and_custom_min_final_cltv_delta() {
+ let chanmon_cfgs = create_chanmon_cfgs(3);
+ let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
+ let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
+
+ let payment_amt = 20_000;
+ let route_hints = vec![
+ nodes[1].node.get_phantom_route_hints(),
+ nodes[2].node.get_phantom_route_hints(),
+ ];
+ let user_payment_preimage = PaymentPreimage([1; 32]);
+ let payment_hash = Some(PaymentHash(Sha256::hash(&user_payment_preimage.0[..]).into_inner()));
+ let non_default_invoice_expiry_secs = 4200;
+ let min_final_cltv_expiry_delta = Some(100);
+ let invoice = crate::utils::create_phantom_invoice::<&test_utils::TestKeysInterface,
+ &test_utils::TestKeysInterface, &test_utils::TestLogger>(Some(payment_amt), payment_hash,
+ "".to_string(), non_default_invoice_expiry_secs, route_hints, &nodes[1].keys_manager, &nodes[1].keys_manager,
+ &nodes[1].logger, Currency::BitcoinTestnet, min_final_cltv_expiry_delta).unwrap();
+ assert_eq!(invoice.amount_pico_btc(), Some(200_000));
+ assert_eq!(invoice.min_final_cltv_expiry_delta(), (min_final_cltv_expiry_delta.unwrap() + 3) as u64);
+ assert_eq!(invoice.expiry_time(), Duration::from_secs(non_default_invoice_expiry_secs.into()));
+ }
+
#[test]
#[cfg(feature = "std")]
fn test_multi_node_hints_includes_single_channels_to_participating_nodes() {
.map(|route_hint| route_hint.phantom_scid)
.collect::<HashSet<u64>>();
- let invoice = crate::utils::create_phantom_invoice::<&test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestLogger>(invoice_amt, None, "test".to_string(), 3600, phantom_route_hints, &invoice_node.keys_manager, &invoice_node.keys_manager, &invoice_node.logger, Currency::BitcoinTestnet).unwrap();
+ let invoice = crate::utils::create_phantom_invoice::<&test_utils::TestKeysInterface,
+ &test_utils::TestKeysInterface, &test_utils::TestLogger>(invoice_amt, None, "test".to_string(),
+ 3600, phantom_route_hints, &invoice_node.keys_manager, &invoice_node.keys_manager,
+ &invoice_node.logger, Currency::BitcoinTestnet, None).unwrap();
let invoice_hints = invoice.private_routes();
}
assert!(chan_ids_to_match.is_empty(), "Unmatched short channel ids: {:?}", chan_ids_to_match);
}
+
+ #[test]
+ fn test_create_invoice_fails_with_invalid_custom_min_final_cltv_expiry_delta() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+ let result = crate::utils::create_invoice_from_channelmanager_and_duration_since_epoch(
+ &nodes[1].node, nodes[1].keys_manager, nodes[1].logger, Currency::BitcoinTestnet,
+ Some(10_000), "Some description".into(), Duration::from_secs(1234567), 3600, Some(MIN_FINAL_CLTV_EXPIRY_DELTA - 4),
+ );
+ match result {
+ Err(SignOrCreationError::CreationError(CreationError::MinFinalCltvExpiryDeltaTooShort)) => {},
+ _ => panic!(),
+ }
+ }
}
"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f"
).unwrap())
.expiry_time(Duration::from_secs(604800))
- .min_final_cltv_expiry(10)
+ .min_final_cltv_expiry_delta(10)
.description("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items".to_owned())
.private_route(RouteHint(vec![RouteHintHop {
src_node_id: PublicKey::from_slice(&hex::decode(
use crate::chain::chaininterface::{BroadcasterInterface, FeeEstimator};
use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, Balance, MonitorEvent, TransactionOutputs, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::{OutPoint, TransactionData};
-use crate::chain::keysinterface::Sign;
+use crate::chain::keysinterface::WriteableEcdsaChannelSigner;
use crate::util::atomic_counter::AtomicCounter;
use crate::util::logger::Logger;
use crate::util::errors::APIError;
pub(crate) fn from_monitor_update(update: &ChannelMonitorUpdate) -> Self {
Self { contents: UpdateOrigin::OffChain(update.update_id) }
}
- pub(crate) fn from_new_monitor<ChannelSigner: Sign>(monitor: &ChannelMonitor<ChannelSigner>) -> Self {
+ pub(crate) fn from_new_monitor<ChannelSigner: WriteableEcdsaChannelSigner>(monitor: &ChannelMonitor<ChannelSigner>) -> Self {
Self { contents: UpdateOrigin::OffChain(monitor.get_latest_update_id()) }
}
}
/// [`ChannelMonitorUpdateStatus::PermanentFailure`], in which case the channel will likely be
/// closed without broadcasting the latest state. See
/// [`ChannelMonitorUpdateStatus::PermanentFailure`] for more details.
-pub trait Persist<ChannelSigner: Sign> {
+pub trait Persist<ChannelSigner: WriteableEcdsaChannelSigner> {
/// Persist a new channel's data in response to a [`chain::Watch::watch_channel`] call. This is
/// called by [`ChannelManager`] for new channels, or may be called directly, e.g. on startup.
///
fn update_persisted_channel(&self, channel_id: OutPoint, update: Option<&ChannelMonitorUpdate>, data: &ChannelMonitor<ChannelSigner>, update_id: MonitorUpdateId) -> ChannelMonitorUpdateStatus;
}
-struct MonitorHolder<ChannelSigner: Sign> {
+struct MonitorHolder<ChannelSigner: WriteableEcdsaChannelSigner> {
monitor: ChannelMonitor<ChannelSigner>,
/// The full set of pending monitor updates for this Channel.
///
last_chain_persist_height: AtomicUsize,
}
-impl<ChannelSigner: Sign> MonitorHolder<ChannelSigner> {
+impl<ChannelSigner: WriteableEcdsaChannelSigner> MonitorHolder<ChannelSigner> {
fn has_pending_offchain_updates(&self, pending_monitor_updates_lock: &MutexGuard<Vec<MonitorUpdateId>>) -> bool {
pending_monitor_updates_lock.iter().any(|update_id|
if let UpdateOrigin::OffChain(_) = update_id.contents { true } else { false })
///
/// Note that this holds a mutex in [`ChainMonitor`] and may block other events until it is
/// released.
-pub struct LockedChannelMonitor<'a, ChannelSigner: Sign> {
+pub struct LockedChannelMonitor<'a, ChannelSigner: WriteableEcdsaChannelSigner> {
lock: RwLockReadGuard<'a, HashMap<OutPoint, MonitorHolder<ChannelSigner>>>,
funding_txo: OutPoint,
}
-impl<ChannelSigner: Sign> Deref for LockedChannelMonitor<'_, ChannelSigner> {
+impl<ChannelSigner: WriteableEcdsaChannelSigner> Deref for LockedChannelMonitor<'_, ChannelSigner> {
type Target = ChannelMonitor<ChannelSigner>;
fn deref(&self) -> &ChannelMonitor<ChannelSigner> {
&self.lock.get(&self.funding_txo).expect("Checked at construction").monitor
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [module-level documentation]: crate::chain::chainmonitor
-pub struct ChainMonitor<ChannelSigner: Sign, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref>
+pub struct ChainMonitor<ChannelSigner: WriteableEcdsaChannelSigner, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref>
where C::Target: chain::Filter,
T::Target: BroadcasterInterface,
F::Target: FeeEstimator,
highest_chain_height: AtomicUsize,
}
-impl<ChannelSigner: Sign, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref> ChainMonitor<ChannelSigner, C, T, F, L, P>
+impl<ChannelSigner: WriteableEcdsaChannelSigner, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref> ChainMonitor<ChannelSigner, C, T, F, L, P>
where C::Target: chain::Filter,
T::Target: BroadcasterInterface,
F::Target: FeeEstimator,
}
}
-impl<ChannelSigner: Sign, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref>
+impl<ChannelSigner: WriteableEcdsaChannelSigner, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref>
chain::Listen for ChainMonitor<ChannelSigner, C, T, F, L, P>
where
C::Target: chain::Filter,
}
}
-impl<ChannelSigner: Sign, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref>
+impl<ChannelSigner: WriteableEcdsaChannelSigner, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref>
chain::Confirm for ChainMonitor<ChannelSigner, C, T, F, L, P>
where
C::Target: chain::Filter,
}
}
-impl<ChannelSigner: Sign, C: Deref , T: Deref , F: Deref , L: Deref , P: Deref >
+impl<ChannelSigner: WriteableEcdsaChannelSigner, C: Deref , T: Deref , F: Deref , L: Deref , P: Deref >
chain::Watch<ChannelSigner> for ChainMonitor<ChannelSigner, C, T, F, L, P>
where C::Target: chain::Filter,
T::Target: BroadcasterInterface,
}
}
-impl<ChannelSigner: Sign, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref> events::EventsProvider for ChainMonitor<ChannelSigner, C, T, F, L, P>
+impl<ChannelSigner: WriteableEcdsaChannelSigner, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref> events::EventsProvider for ChainMonitor<ChannelSigner, C, T, F, L, P>
where C::Target: chain::Filter,
T::Target: BroadcasterInterface,
F::Target: FeeEstimator,
use crate::chain::{BestBlock, WatchedOutput};
use crate::chain::chaininterface::{BroadcasterInterface, FeeEstimator, LowerBoundedFeeEstimator};
use crate::chain::transaction::{OutPoint, TransactionData};
-use crate::chain::keysinterface::{SpendableOutputDescriptor, StaticPaymentOutputDescriptor, DelayedPaymentOutputDescriptor, Sign, SignerProvider, EntropySource};
+use crate::chain::keysinterface::{SpendableOutputDescriptor, StaticPaymentOutputDescriptor, DelayedPaymentOutputDescriptor, WriteableEcdsaChannelSigner, SignerProvider, EntropySource};
#[cfg(anchors)]
use crate::chain::onchaintx::ClaimEvent;
use crate::chain::onchaintx::OnchainTxHandler;
/// the "reorg path" (ie disconnecting blocks until you find a common ancestor from both the
/// returned block hash and the the current chain and then reconnecting blocks to get to the
/// best chain) upon deserializing the object!
-pub struct ChannelMonitor<Signer: Sign> {
+pub struct ChannelMonitor<Signer: WriteableEcdsaChannelSigner> {
#[cfg(test)]
pub(crate) inner: Mutex<ChannelMonitorImpl<Signer>>,
#[cfg(not(test))]
inner: Mutex<ChannelMonitorImpl<Signer>>,
}
-pub(crate) struct ChannelMonitorImpl<Signer: Sign> {
+pub(crate) struct ChannelMonitorImpl<Signer: WriteableEcdsaChannelSigner> {
latest_update_id: u64,
commitment_transaction_number_obscure_factor: u64,
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
/// Used only in testing and fuzzing to check serialization roundtrips don't change the underlying
/// object
-impl<Signer: Sign> PartialEq for ChannelMonitor<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> PartialEq for ChannelMonitor<Signer> {
fn eq(&self, other: &Self) -> bool {
let inner = self.inner.lock().unwrap();
let other = other.inner.lock().unwrap();
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
/// Used only in testing and fuzzing to check serialization roundtrips don't change the underlying
/// object
-impl<Signer: Sign> PartialEq for ChannelMonitorImpl<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> PartialEq for ChannelMonitorImpl<Signer> {
fn eq(&self, other: &Self) -> bool {
if self.latest_update_id != other.latest_update_id ||
self.commitment_transaction_number_obscure_factor != other.commitment_transaction_number_obscure_factor ||
}
}
-impl<Signer: Sign> Writeable for ChannelMonitor<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> Writeable for ChannelMonitor<Signer> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), Error> {
self.inner.lock().unwrap().write(writer)
}
const SERIALIZATION_VERSION: u8 = 1;
const MIN_SERIALIZATION_VERSION: u8 = 1;
-impl<Signer: Sign> Writeable for ChannelMonitorImpl<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> Writeable for ChannelMonitorImpl<Signer> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), Error> {
write_ver_prefix!(writer, SERIALIZATION_VERSION, MIN_SERIALIZATION_VERSION);
}
}
-impl<Signer: Sign> ChannelMonitor<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
/// For lockorder enforcement purposes, we need to have a single site which constructs the
/// `inner` mutex, otherwise cases where we lock two monitors at the same time (eg in our
/// PartialEq implementation) we may decide a lockorder violation has occurred.
}
}
-impl<Signer: Sign> ChannelMonitorImpl<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitorImpl<Signer> {
/// Helper for get_claimable_balances which does the work for an individual HTLC, generating up
/// to one `Balance` for the HTLC.
fn get_htlc_balance(&self, htlc: &HTLCOutputInCommitment, holder_commitment: bool,
}
}
-impl<Signer: Sign> ChannelMonitor<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
/// Gets the balances in this channel which are either claimable by us if we were to
/// force-close the channel now or which are claimable on-chain (possibly awaiting
/// confirmation).
vec![Vec::new(), Vec::new(), Vec::new(), Vec::new(), deliberately_bogus_accepted_htlc_witness_program().into()].into()
}
-impl<Signer: Sign> ChannelMonitorImpl<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitorImpl<Signer> {
/// Inserts a revocation secret into this channel monitor. Prunes old preimages if neither
/// needed by holder commitment transactions HTCLs nor by counterparty ones. Unless we haven't already seen
/// counterparty commitment transaction's secret, they are de facto pruned (we can use revocation key).
}
}
-impl<Signer: Sign, T: Deref, F: Deref, L: Deref> chain::Listen for (ChannelMonitor<Signer>, T, F, L)
+impl<Signer: WriteableEcdsaChannelSigner, T: Deref, F: Deref, L: Deref> chain::Listen for (ChannelMonitor<Signer>, T, F, L)
where
T::Target: BroadcasterInterface,
F::Target: FeeEstimator,
}
}
-impl<Signer: Sign, T: Deref, F: Deref, L: Deref> chain::Confirm for (ChannelMonitor<Signer>, T, F, L)
+impl<Signer: WriteableEcdsaChannelSigner, T: Deref, F: Deref, L: Deref> chain::Confirm for (ChannelMonitor<Signer>, T, F, L)
where
T::Target: BroadcasterInterface,
F::Target: FeeEstimator,
/// The revocation point specific to the commitment transaction which was broadcast. Used to
/// derive the witnessScript for this output.
pub revocation_pubkey: PublicKey,
- /// Arbitrary identification information returned by a call to [`BaseSign::channel_keys_id`].
+ /// Arbitrary identification information returned by a call to [`ChannelSigner::channel_keys_id`].
/// This may be useful in re-deriving keys used in the channel to spend the output.
pub channel_keys_id: [u8; 32],
/// The value of the channel which this output originated from, possibly indirectly.
pub outpoint: OutPoint,
/// The output which is referenced by the given outpoint.
pub output: TxOut,
- /// Arbitrary identification information returned by a call to [`BaseSign::channel_keys_id`].
+ /// Arbitrary identification information returned by a call to [`ChannelSigner::channel_keys_id`].
/// This may be useful in re-deriving keys used in the channel to spend the output.
pub channel_keys_id: [u8; 32],
/// The value of the channel which this transactions spends.
///
/// To derive the delayed payment key which is used to sign this input, you must pass the
/// holder [`InMemorySigner::delayed_payment_base_key`] (i.e., the private key which corresponds to the
- /// [`ChannelPublicKeys::delayed_payment_basepoint`] in [`BaseSign::pubkeys`]) and the provided
+ /// [`ChannelPublicKeys::delayed_payment_basepoint`] in [`ChannelSigner::pubkeys`]) and the provided
/// [`DelayedPaymentOutputDescriptor::per_commitment_point`] to [`chan_utils::derive_private_key`]. The public key can be
/// generated without the secret key using [`chan_utils::derive_public_key`] and only the
- /// [`ChannelPublicKeys::delayed_payment_basepoint`] which appears in [`BaseSign::pubkeys`].
+ /// [`ChannelPublicKeys::delayed_payment_basepoint`] which appears in [`ChannelSigner::pubkeys`].
///
/// To derive the [`DelayedPaymentOutputDescriptor::revocation_pubkey`] provided here (which is
/// used in the witness script generation), you must pass the counterparty
/// [`ChannelPublicKeys::revocation_basepoint`] (which appears in the call to
- /// [`BaseSign::provide_channel_parameters`]) and the provided
+ /// [`ChannelSigner::provide_channel_parameters`]) and the provided
/// [`DelayedPaymentOutputDescriptor::per_commitment_point`] to
/// [`chan_utils::derive_public_revocation_key`].
///
/// [`chan_utils::get_revokeable_redeemscript`].
DelayedPaymentOutput(DelayedPaymentOutputDescriptor),
/// An output to a P2WPKH, spendable exclusively by our payment key (i.e., the private key
- /// which corresponds to the `payment_point` in [`BaseSign::pubkeys`]). The witness
+ /// which corresponds to the `payment_point` in [`ChannelSigner::pubkeys`]). The witness
/// in the spending input is, thus, simply:
/// ```bitcoin
/// <BIP 143 signature> <payment key>
(2, StaticPaymentOutput),
);
-/// A trait to sign Lightning channel transactions as described in
-/// [BOLT 3](https://github.com/lightning/bolts/blob/master/03-transactions.md).
-///
-/// Signing services could be implemented on a hardware wallet and should implement signing
-/// policies in order to be secure. Please refer to the [VLS Policy
-/// Controls](https://gitlab.com/lightning-signer/validating-lightning-signer/-/blob/main/docs/policy-controls.md)
-/// for an example of such policies.
-pub trait BaseSign {
+/// A trait to handle Lightning channel key material without concretizing the channel type or
+/// the signature mechanism.
+pub trait ChannelSigner {
/// Gets the per-commitment point for a specific commitment number
///
/// Note that the commitment number starts at `(1 << 48) - 1` and counts backwards.
fn get_per_commitment_point(&self, idx: u64, secp_ctx: &Secp256k1<secp256k1::All>) -> PublicKey;
+
/// Gets the commitment secret for a specific commitment number as part of the revocation process
///
/// An external signer implementation should error here if the commitment was already signed
/// Note that the commitment number starts at `(1 << 48) - 1` and counts backwards.
// TODO: return a Result so we can signal a validation error
fn release_commitment_secret(&self, idx: u64) -> [u8; 32];
+
/// Validate the counterparty's signatures on the holder commitment transaction and HTLCs.
///
/// This is required in order for the signer to make sure that releasing a commitment
/// irrelevant or duplicate preimages.
fn validate_holder_commitment(&self, holder_tx: &HolderCommitmentTransaction,
preimages: Vec<PaymentPreimage>) -> Result<(), ()>;
+
/// Returns the holder's channel public keys and basepoints.
fn pubkeys(&self) -> &ChannelPublicKeys;
+
/// Returns an arbitrary identifier describing the set of keys which are provided back to you in
/// some [`SpendableOutputDescriptor`] types. This should be sufficient to identify this
- /// [`BaseSign`] object uniquely and lookup or re-derive its keys.
+ /// [`EcdsaChannelSigner`] object uniquely and lookup or re-derive its keys.
fn channel_keys_id(&self) -> [u8; 32];
+
+ /// Set the counterparty static channel data, including basepoints,
+ /// `counterparty_selected`/`holder_selected_contest_delay` and funding outpoint.
+ ///
+ /// This data is static, and will never change for a channel once set. For a given [`ChannelSigner`]
+ /// instance, LDK will call this method exactly once - either immediately after construction
+ /// (not including if done via [`SignerProvider::read_chan_signer`]) or when the funding
+ /// information has been generated.
+ ///
+ /// channel_parameters.is_populated() MUST be true.
+ fn provide_channel_parameters(&mut self, channel_parameters: &ChannelTransactionParameters);
+}
+
+/// A trait to sign Lightning channel transactions as described in
+/// [BOLT 3](https://github.com/lightning/bolts/blob/master/03-transactions.md).
+///
+/// Signing services could be implemented on a hardware wallet and should implement signing
+/// policies in order to be secure. Please refer to the [VLS Policy
+/// Controls](https://gitlab.com/lightning-signer/validating-lightning-signer/-/blob/main/docs/policy-controls.md)
+/// for an example of such policies.
+pub trait EcdsaChannelSigner: ChannelSigner {
/// Create a signature for a counterparty's commitment transaction and associated HTLC transactions.
///
/// Note that if signing fails or is rejected, the channel will be force-closed.
fn sign_channel_announcement_with_funding_key(
&self, msg: &UnsignedChannelAnnouncement, secp_ctx: &Secp256k1<secp256k1::All>
) -> Result<Signature, ()>;
- /// Set the counterparty static channel data, including basepoints,
- /// `counterparty_selected`/`holder_selected_contest_delay` and funding outpoint.
- ///
- /// This data is static, and will never change for a channel once set. For a given [`BaseSign`]
- /// instance, LDK will call this method exactly once - either immediately after construction
- /// (not including if done via [`SignerProvider::read_chan_signer`]) or when the funding
- /// information has been generated.
- ///
- /// channel_parameters.is_populated() MUST be true.
- fn provide_channel_parameters(&mut self, channel_parameters: &ChannelTransactionParameters);
}
/// A writeable signer.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelMonitor`]: crate::chain::channelmonitor::ChannelMonitor
-pub trait Sign: BaseSign + Writeable {}
+pub trait WriteableEcdsaChannelSigner: EcdsaChannelSigner + Writeable {}
/// Specifies the recipient of an invoice.
///
/// A trait that can return signer instances for individual channels.
pub trait SignerProvider {
- /// A type which implements [`Sign`] which will be returned by [`Self::derive_channel_signer`].
- type Signer : Sign;
+ /// A type which implements [`WriteableEcdsaChannelSigner`] which will be returned by [`Self::derive_channel_signer`].
+ type Signer : WriteableEcdsaChannelSigner;
/// Generates a unique `channel_keys_id` that can be used to obtain a [`Self::Signer`] through
/// [`SignerProvider::derive_channel_signer`]. The `user_channel_id` is provided to allow
/// To derive a new `Signer`, a fresh `channel_keys_id` should be obtained through
/// [`SignerProvider::generate_channel_keys_id`]. Otherwise, an existing `Signer` can be
/// re-derived from its `channel_keys_id`, which can be obtained through its trait method
- /// [`BaseSign::channel_keys_id`].
+ /// [`ChannelSigner::channel_keys_id`].
fn derive_channel_signer(&self, channel_value_satoshis: u64, channel_keys_id: [u8; 32]) -> Self::Signer;
/// Reads a [`Signer`] for this [`SignerProvider`] from the given input stream.
/// This is only called during deserialization of other objects which contain
- /// [`Sign`]-implementing objects (i.e., [`ChannelMonitor`]s and [`ChannelManager`]s).
+ /// [`WriteableEcdsaChannelSigner`]-implementing objects (i.e., [`ChannelMonitor`]s and [`ChannelManager`]s).
/// The bytes are exactly those which `<Self::Signer as Writeable>::write()` writes, and
/// contain no versioning scheme. You may wish to include your own version prefix and ensure
/// you've read all of the provided bytes to ensure no corruption occurred.
}
#[derive(Clone)]
-/// A simple implementation of [`Sign`] that just keeps the private keys in memory.
+/// A simple implementation of [`WriteableEcdsaChannelSigner`] that just keeps the private keys in memory.
///
/// This implementation performs no policy checks and is insufficient by itself as
/// a secure external signer.
/// Returns the counterparty's pubkeys.
///
- /// Will panic if [`BaseSign::provide_channel_parameters`] has not been called before.
+ /// Will panic if [`ChannelSigner::provide_channel_parameters`] has not been called before.
pub fn counterparty_pubkeys(&self) -> &ChannelPublicKeys { &self.get_channel_parameters().counterparty_parameters.as_ref().unwrap().pubkeys }
/// Returns the `contest_delay` value specified by our counterparty and applied on holder-broadcastable
/// transactions, i.e., the amount of time that we have to wait to recover our funds if we
/// broadcast a transaction.
///
- /// Will panic if [`BaseSign::provide_channel_parameters`] has not been called before.
+ /// Will panic if [`ChannelSigner::provide_channel_parameters`] has not been called before.
pub fn counterparty_selected_contest_delay(&self) -> u16 { self.get_channel_parameters().counterparty_parameters.as_ref().unwrap().selected_contest_delay }
/// Returns the `contest_delay` value specified by us and applied on transactions broadcastable
/// by our counterparty, i.e., the amount of time that they have to wait to recover their funds
/// if they broadcast a transaction.
///
- /// Will panic if [`BaseSign::provide_channel_parameters`] has not been called before.
+ /// Will panic if [`ChannelSigner::provide_channel_parameters`] has not been called before.
pub fn holder_selected_contest_delay(&self) -> u16 { self.get_channel_parameters().holder_selected_contest_delay }
/// Returns whether the holder is the initiator.
///
- /// Will panic if [`BaseSign::provide_channel_parameters`] has not been called before.
+ /// Will panic if [`ChannelSigner::provide_channel_parameters`] has not been called before.
pub fn is_outbound(&self) -> bool { self.get_channel_parameters().is_outbound_from_holder }
/// Funding outpoint
///
- /// Will panic if [`BaseSign::provide_channel_parameters`] has not been called before.
+ /// Will panic if [`ChannelSigner::provide_channel_parameters`] has not been called before.
pub fn funding_outpoint(&self) -> &OutPoint { self.get_channel_parameters().funding_outpoint.as_ref().unwrap() }
/// Returns a [`ChannelTransactionParameters`] for this channel, to be used when verifying or
/// building transactions.
///
- /// Will panic if [`BaseSign::provide_channel_parameters`] has not been called before.
+ /// Will panic if [`ChannelSigner::provide_channel_parameters`] has not been called before.
pub fn get_channel_parameters(&self) -> &ChannelTransactionParameters {
self.channel_parameters.as_ref().unwrap()
}
/// Returns whether anchors should be used.
///
- /// Will panic if [`BaseSign::provide_channel_parameters`] has not been called before.
+ /// Will panic if [`ChannelSigner::provide_channel_parameters`] has not been called before.
pub fn opt_anchors(&self) -> bool {
self.get_channel_parameters().opt_anchors.is_some()
}
}
}
-impl BaseSign for InMemorySigner {
+impl ChannelSigner for InMemorySigner {
fn get_per_commitment_point(&self, idx: u64, secp_ctx: &Secp256k1<secp256k1::All>) -> PublicKey {
let commitment_secret = SecretKey::from_slice(&chan_utils::build_commitment_secret(&self.commitment_seed, idx)).unwrap();
PublicKey::from_secret_key(secp_ctx, &commitment_secret)
fn channel_keys_id(&self) -> [u8; 32] { self.channel_keys_id }
+ fn provide_channel_parameters(&mut self, channel_parameters: &ChannelTransactionParameters) {
+ assert!(self.channel_parameters.is_none() || self.channel_parameters.as_ref().unwrap() == channel_parameters);
+ if self.channel_parameters.is_some() {
+ // The channel parameters were already set and they match, return early.
+ return;
+ }
+ assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated");
+ self.channel_parameters = Some(channel_parameters.clone());
+ }
+}
+
+impl EcdsaChannelSigner for InMemorySigner {
fn sign_counterparty_commitment(&self, commitment_tx: &CommitmentTransaction, _preimages: Vec<PaymentPreimage>, secp_ctx: &Secp256k1<secp256k1::All>) -> Result<(Signature, Vec<Signature>), ()> {
let trusted_tx = commitment_tx.trust();
let keys = trusted_tx.keys();
let msghash = hash_to_message!(&Sha256dHash::hash(&msg.encode()[..])[..]);
Ok(sign(secp_ctx, &msghash, &self.funding_key))
}
-
- fn provide_channel_parameters(&mut self, channel_parameters: &ChannelTransactionParameters) {
- assert!(self.channel_parameters.is_none() || self.channel_parameters.as_ref().unwrap() == channel_parameters);
- if self.channel_parameters.is_some() {
- // The channel parameters were already set and they match, return early.
- return;
- }
- assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated");
- self.channel_parameters = Some(channel_parameters.clone());
- }
}
const SERIALIZATION_VERSION: u8 = 1;
const MIN_SERIALIZATION_VERSION: u8 = 1;
-impl Sign for InMemorySigner {}
+impl WriteableEcdsaChannelSigner for InMemorySigner {}
impl Writeable for InMemorySigner {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), Error> {
Err(_) => panic!("Your rng is busted"),
}
}
- /// Derive an old [`Sign`] containing per-channel secrets based on a key derivation parameters.
+ /// Derive an old [`WriteableEcdsaChannelSigner`] containing per-channel secrets based on a key derivation parameters.
pub fn derive_channel_keys(&self, channel_value_satoshis: u64, params: &[u8; 32]) -> InMemorySigner {
let chan_id = u64::from_be_bytes(params[0..8].try_into().unwrap());
let mut unique_start = Sha256::engine();
}
}
-// Ensure that BaseSign can have a vtable
+// Ensure that EcdsaChannelSigner can have a vtable
#[test]
pub fn dyn_sign() {
- let _signer: Box<dyn BaseSign>;
+ let _signer: Box<dyn EcdsaChannelSigner>;
}
use bitcoin::secp256k1::PublicKey;
use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, MonitorEvent};
-use crate::chain::keysinterface::Sign;
+use crate::chain::keysinterface::WriteableEcdsaChannelSigner;
use crate::chain::transaction::{OutPoint, TransactionData};
use crate::prelude::*;
/// multiple instances.
///
/// [`PermanentFailure`]: ChannelMonitorUpdateStatus::PermanentFailure
-pub trait Watch<ChannelSigner: Sign> {
+pub trait Watch<ChannelSigner: WriteableEcdsaChannelSigner> {
/// Watches a channel identified by `funding_txo` using `monitor`.
///
/// Implementations are responsible for watching the chain for the funding transaction along
use bitcoin::secp256k1::{Secp256k1, ecdsa::Signature};
use bitcoin::secp256k1;
-use crate::chain::keysinterface::{BaseSign, EntropySource, SignerProvider};
+use crate::chain::keysinterface::{ChannelSigner, EntropySource, SignerProvider};
use crate::ln::msgs::DecodeError;
use crate::ln::PaymentPreimage;
#[cfg(anchors)]
use crate::chain::chaininterface::ConfirmationTarget;
use crate::chain::chaininterface::{FeeEstimator, BroadcasterInterface, LowerBoundedFeeEstimator};
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, CLTV_SHARED_CLAIM_BUFFER};
-use crate::chain::keysinterface::Sign;
+use crate::chain::keysinterface::WriteableEcdsaChannelSigner;
#[cfg(anchors)]
use crate::chain::package::PackageSolvingData;
use crate::chain::package::PackageTemplate;
/// OnchainTxHandler receives claiming requests, aggregates them if it's sound, broadcast and
/// do RBF bumping if possible.
-pub struct OnchainTxHandler<ChannelSigner: Sign> {
+pub struct OnchainTxHandler<ChannelSigner: WriteableEcdsaChannelSigner> {
destination_script: Script,
holder_commitment: HolderCommitmentTransaction,
// holder_htlc_sigs and prev_holder_htlc_sigs are in the order as they appear in the commitment
const SERIALIZATION_VERSION: u8 = 1;
const MIN_SERIALIZATION_VERSION: u8 = 1;
-impl<ChannelSigner: Sign> OnchainTxHandler<ChannelSigner> {
+impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
pub(crate) fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
write_ver_prefix!(writer, SERIALIZATION_VERSION, MIN_SERIALIZATION_VERSION);
}
}
-impl<ChannelSigner: Sign> OnchainTxHandler<ChannelSigner> {
+impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
pub(crate) fn new(destination_script: Script, signer: ChannelSigner, channel_parameters: ChannelTransactionParameters, holder_commitment: HolderCommitmentTransaction, secp_ctx: Secp256k1<secp256k1::All>) -> Self {
OnchainTxHandler {
destination_script,
use crate::ln::chan_utils;
use crate::ln::msgs::DecodeError;
use crate::chain::chaininterface::{FeeEstimator, ConfirmationTarget, MIN_RELAY_FEE_SAT_PER_1000_WEIGHT};
-use crate::chain::keysinterface::Sign;
+use crate::chain::keysinterface::WriteableEcdsaChannelSigner;
#[cfg(anchors)]
use crate::chain::onchaintx::ExternalHTLCClaim;
use crate::chain::onchaintx::OnchainTxHandler;
_ => { mem::discriminant(self) == mem::discriminant(&input) }
}
}
- fn finalize_input<Signer: Sign>(&self, bumped_tx: &mut Transaction, i: usize, onchain_handler: &mut OnchainTxHandler<Signer>) -> bool {
+ fn finalize_input<Signer: WriteableEcdsaChannelSigner>(&self, bumped_tx: &mut Transaction, i: usize, onchain_handler: &mut OnchainTxHandler<Signer>) -> bool {
match self {
PackageSolvingData::RevokedOutput(ref outp) => {
let chan_keys = TxCreationKeys::derive_new(&onchain_handler.secp_ctx, &outp.per_commitment_point, &outp.counterparty_delayed_payment_base_key, &outp.counterparty_htlc_base_key, &onchain_handler.signer.pubkeys().revocation_basepoint, &onchain_handler.signer.pubkeys().htlc_basepoint);
}
true
}
- fn get_finalized_tx<Signer: Sign>(&self, outpoint: &BitcoinOutPoint, onchain_handler: &mut OnchainTxHandler<Signer>) -> Option<Transaction> {
+ fn get_finalized_tx<Signer: WriteableEcdsaChannelSigner>(&self, outpoint: &BitcoinOutPoint, onchain_handler: &mut OnchainTxHandler<Signer>) -> Option<Transaction> {
match self {
PackageSolvingData::HolderHTLCOutput(ref outp) => {
debug_assert!(!outp.opt_anchors());
inputs_weight + witnesses_weight + transaction_weight + output_weight
}
#[cfg(anchors)]
- pub(crate) fn construct_malleable_package_with_external_funding<Signer: Sign>(
+ pub(crate) fn construct_malleable_package_with_external_funding<Signer: WriteableEcdsaChannelSigner>(
&self, onchain_handler: &mut OnchainTxHandler<Signer>,
) -> Option<Vec<ExternalHTLCClaim>> {
debug_assert!(self.requires_external_funding());
}
htlcs
}
- pub(crate) fn finalize_malleable_package<L: Deref, Signer: Sign>(
+ pub(crate) fn finalize_malleable_package<L: Deref, Signer: WriteableEcdsaChannelSigner>(
&self, onchain_handler: &mut OnchainTxHandler<Signer>, value: u64, destination_script: Script, logger: &L
) -> Option<Transaction> where L::Target: Logger {
debug_assert!(self.is_malleable());
log_debug!(logger, "Finalized transaction {} ready to broadcast", bumped_tx.txid());
Some(bumped_tx)
}
- pub(crate) fn finalize_untractable_package<L: Deref, Signer: Sign>(
+ pub(crate) fn finalize_untractable_package<L: Deref, Signer: WriteableEcdsaChannelSigner>(
&self, onchain_handler: &mut OnchainTxHandler<Signer>, logger: &L,
) -> Option<Transaction> where L::Target: Logger {
debug_assert!(!self.is_malleable());
use crate::ln::chan_utils::{get_htlc_redeemscript, get_to_countersignatory_with_anchors_redeemscript, CommitmentTransaction, TxCreationKeys, ChannelTransactionParameters, CounterpartyChannelTransactionParameters, HTLCOutputInCommitment};
use bitcoin::secp256k1::{PublicKey, SecretKey, Secp256k1};
use crate::util::test_utils;
- use crate::chain::keysinterface::{BaseSign, SignerProvider};
+ use crate::chain::keysinterface::{ChannelSigner, SignerProvider};
use bitcoin::{Network, Txid};
use bitcoin::hashes::Hash;
use crate::ln::PaymentHash;
use crate::chain::chaininterface::{FeeEstimator, ConfirmationTarget, LowerBoundedFeeEstimator};
use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::{OutPoint, TransactionData};
-use crate::chain::keysinterface::{Sign, EntropySource, BaseSign, NodeSigner, Recipient, SignerProvider};
+use crate::chain::keysinterface::{WriteableEcdsaChannelSigner, EntropySource, ChannelSigner, SignerProvider, NodeSigner, Recipient};
use crate::util::events::ClosureReason;
use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer, VecWriter};
use crate::util::logger::Logger;
//
// Holder designates channel data owned for the benefice of the user client.
// Counterparty designates channel data owned by the another channel participant entity.
-pub(super) struct Channel<Signer: Sign> {
+pub(super) struct Channel<Signer: ChannelSigner> {
config: LegacyChannelConfig,
// Track the previous `ChannelConfig` so that we can continue forwarding HTLCs that were
};
}
-impl<Signer: Sign> Channel<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> Channel<Signer> {
/// Returns the value to use for `holder_max_htlc_value_in_flight_msat` as a percentage of the
/// `channel_value_satoshis` in msat, set through
/// [`ChannelHandshakeConfig::max_inbound_htlc_value_in_flight_percent_of_channel`]
}
}
-impl<Signer: Sign> Writeable for Channel<Signer> {
+impl<Signer: WriteableEcdsaChannelSigner> Writeable for Channel<Signer> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
// Note that we write out as if remove_uncommitted_htlcs_and_mark_paused had just been
// called.
use crate::ln::chan_utils::{htlc_success_tx_weight, htlc_timeout_tx_weight};
use crate::chain::BestBlock;
use crate::chain::chaininterface::{FeeEstimator, LowerBoundedFeeEstimator, ConfirmationTarget};
- use crate::chain::keysinterface::{BaseSign, InMemorySigner, EntropySource, SignerProvider};
+ use crate::chain::keysinterface::{ChannelSigner, InMemorySigner, EntropySource, SignerProvider};
use crate::chain::transaction::OutPoint;
use crate::util::config::UserConfig;
use crate::util::enforcing_trait_impls::EnforcingSigner;
use bitcoin::hashes::hex::FromHex;
use bitcoin::hash_types::Txid;
use bitcoin::secp256k1::Message;
- use crate::chain::keysinterface::BaseSign;
+ use crate::chain::keysinterface::EcdsaChannelSigner;
use crate::ln::PaymentPreimage;
use crate::ln::channel::{HTLCOutputInCommitment ,TxCreationKeys};
use crate::ln::chan_utils::{ChannelPublicKeys, HolderCommitmentTransaction, CounterpartyChannelTransactionParameters};
use crate::ln::outbound_payment;
use crate::ln::outbound_payment::{OutboundPayments, PendingOutboundPayment};
use crate::ln::wire::Encode;
-use crate::chain::keysinterface::{EntropySource, KeysManager, NodeSigner, Recipient, Sign, SignerProvider};
+use crate::chain::keysinterface::{EntropySource, KeysManager, NodeSigner, Recipient, SignerProvider, ChannelSigner};
use crate::util::config::{UserConfig, ChannelConfig};
use crate::util::events::{Event, EventHandler, EventsProvider, MessageSendEvent, MessageSendEventsProvider, ClosureReason, HTLCDestination};
use crate::util::events;
}
/// State we hold per-peer.
-pub(super) struct PeerState<Signer: Sign> {
+pub(super) struct PeerState<Signer: ChannelSigner> {
/// `temporary_channel_id` or `channel_id` -> `channel`.
///
/// Holds all channels where the peer is the counterparty. Once a channel has been assigned a
pub(super) const CLTV_FAR_FAR_AWAY: u32 = 14 * 24 * 6;
/// Minimum CLTV difference between the current block height and received inbound payments.
-/// Invoices generated for payment to us must set their `min_final_cltv_expiry` field to at least
+/// Invoices generated for payment to us must set their `min_final_cltv_expiry_delta` field to at least
/// this value.
// Note that we fail if exactly HTLC_FAIL_BACK_BUFFER + 1 was used, so we need to add one for
// any payments to succeed. Further, we don't want payments to fail if a block was found while
// a payment was being routed, so we add an extra block to be safe.
-pub const MIN_FINAL_CLTV_EXPIRY: u32 = HTLC_FAIL_BACK_BUFFER + 3;
+pub const MIN_FINAL_CLTV_EXPIRY_DELTA: u16 = HTLC_FAIL_BACK_BUFFER as u16 + 3;
// Check that our CLTV_EXPIRY is at least CLTV_CLAIM_BUFFER + ANTI_REORG_DELAY + LATENCY_GRACE_PERIOD_BLOCKS,
// ie that if the next-hop peer fails the HTLC within
// final_expiry_too_soon
// We have to have some headroom to broadcast on chain if we have the preimage, so make sure
// we have at least HTLC_FAIL_BACK_BUFFER blocks to go.
+ //
// Also, ensure that, in the case of an unknown preimage for the received payment hash, our
// payment logic has enough time to fail the HTLC backward before our onchain logic triggers a
// channel closure (see HTLC_FAIL_BACK_BUFFER rationale).
match claimable_htlc.onion_payload {
OnionPayload::Invoice { .. } => {
let payment_data = payment_data.unwrap();
- let payment_preimage = match inbound_payment::verify(payment_hash, &payment_data, self.highest_seen_timestamp.load(Ordering::Acquire) as u64, &self.inbound_payment_key, &self.logger) {
- Ok(payment_preimage) => payment_preimage,
+ let (payment_preimage, min_final_cltv_expiry_delta) = match inbound_payment::verify(payment_hash, &payment_data, self.highest_seen_timestamp.load(Ordering::Acquire) as u64, &self.inbound_payment_key, &self.logger) {
+ Ok(result) => result,
Err(()) => {
+ log_trace!(self.logger, "Failing new HTLC with payment_hash {} as payment verification failed", log_bytes!(payment_hash.0));
fail_htlc!(claimable_htlc, payment_hash);
continue
}
};
+ if let Some(min_final_cltv_expiry_delta) = min_final_cltv_expiry_delta {
+ let expected_min_expiry_height = (self.current_best_block().height() + min_final_cltv_expiry_delta as u32) as u64;
+ if (cltv_expiry as u64) < expected_min_expiry_height {
+ log_trace!(self.logger, "Failing new HTLC with payment_hash {} as its CLTV expiry was too soon (had {}, earliest expected {})",
+ log_bytes!(payment_hash.0), cltv_expiry, expected_min_expiry_height);
+ fail_htlc!(claimable_htlc, payment_hash);
+ continue;
+ }
+ }
check_total_value!(payment_data, payment_preimage);
},
OnionPayload::Spontaneous(preimage) => {
///
/// Errors if `min_value_msat` is greater than total bitcoin supply.
///
+ /// If `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable
+ /// on versions of LDK prior to 0.0.114.
+ ///
/// [`claim_funds`]: Self::claim_funds
/// [`PaymentClaimable`]: events::Event::PaymentClaimable
/// [`PaymentClaimable::payment_preimage`]: events::Event::PaymentClaimable::payment_preimage
/// [`create_inbound_payment_for_hash`]: Self::create_inbound_payment_for_hash
- pub fn create_inbound_payment(&self, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32) -> Result<(PaymentHash, PaymentSecret), ()> {
- inbound_payment::create(&self.inbound_payment_key, min_value_msat, invoice_expiry_delta_secs, &self.entropy_source, self.highest_seen_timestamp.load(Ordering::Acquire) as u64)
+ pub fn create_inbound_payment(&self, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32,
+ min_final_cltv_expiry_delta: Option<u16>) -> Result<(PaymentHash, PaymentSecret), ()> {
+ inbound_payment::create(&self.inbound_payment_key, min_value_msat, invoice_expiry_delta_secs,
+ &self.entropy_source, self.highest_seen_timestamp.load(Ordering::Acquire) as u64,
+ min_final_cltv_expiry_delta)
}
/// Legacy version of [`create_inbound_payment`]. Use this method if you wish to share
/// If you need exact expiry semantics, you should enforce them upon receipt of
/// [`PaymentClaimable`].
///
- /// Note that invoices generated for inbound payments should have their `min_final_cltv_expiry`
- /// set to at least [`MIN_FINAL_CLTV_EXPIRY`].
+ /// Note that invoices generated for inbound payments should have their `min_final_cltv_expiry_delta`
+ /// set to at least [`MIN_FINAL_CLTV_EXPIRY_DELTA`].
///
/// Note that a malicious eavesdropper can intuit whether an inbound payment was created by
/// `create_inbound_payment` or `create_inbound_payment_for_hash` based on runtime.
///
/// Errors if `min_value_msat` is greater than total bitcoin supply.
///
+ /// If `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable
+ /// on versions of LDK prior to 0.0.114.
+ ///
/// [`create_inbound_payment`]: Self::create_inbound_payment
/// [`PaymentClaimable`]: events::Event::PaymentClaimable
- pub fn create_inbound_payment_for_hash(&self, payment_hash: PaymentHash, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32) -> Result<PaymentSecret, ()> {
- inbound_payment::create_from_hash(&self.inbound_payment_key, min_value_msat, payment_hash, invoice_expiry_delta_secs, self.highest_seen_timestamp.load(Ordering::Acquire) as u64)
+ pub fn create_inbound_payment_for_hash(&self, payment_hash: PaymentHash, min_value_msat: Option<u64>,
+ invoice_expiry_delta_secs: u32, min_final_cltv_expiry: Option<u16>) -> Result<PaymentSecret, ()> {
+ inbound_payment::create_from_hash(&self.inbound_payment_key, min_value_msat, payment_hash,
+ invoice_expiry_delta_secs, self.highest_seen_timestamp.load(Ordering::Acquire) as u64,
+ min_final_cltv_expiry)
}
/// Legacy version of [`create_inbound_payment_for_hash`]. Use this method if you wish to share
payment_preimage: match pending_inbound_payments.get(&payment_hash) {
Some(inbound_payment) => inbound_payment.payment_preimage,
None => match inbound_payment::verify(payment_hash, &hop_data, 0, &expanded_inbound_key, &args.logger) {
- Ok(payment_preimage) => payment_preimage,
+ Ok((payment_preimage, _)) => payment_preimage,
Err(()) => {
log_error!(args.logger, "Failed to read claimable payment data for HTLC with payment hash {} - was not a pending inbound payment and didn't match our payment key", log_bytes!(payment_hash.0));
return Err(DecodeError::InvalidValue);
payment_preimage.0[0..8].copy_from_slice(&payment_count.to_le_bytes());
payment_count += 1;
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).into_inner());
- let payment_secret = $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200).unwrap();
+ let payment_secret = $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None).unwrap();
$node_a.send_payment(&route, payment_hash, &Some(payment_secret), PaymentId(payment_hash.0)).unwrap();
let payment_event = SendEvent::from_event($node_a.get_and_clear_pending_msg_events().pop().unwrap());
}
};
($dest_node: expr, $min_value_msat: expr) => {
+ {
+ crate::get_payment_preimage_hash!($dest_node, $min_value_msat, None)
+ }
+ };
+ ($dest_node: expr, $min_value_msat: expr, $min_final_cltv_expiry_delta: expr) => {
{
use bitcoin::hashes::Hash as _;
let mut payment_count = $dest_node.network_payment_count.borrow_mut();
*payment_count += 1;
let payment_hash = $crate::ln::PaymentHash(
bitcoin::hashes::sha256::Hash::hash(&payment_preimage.0[..]).into_inner());
- let payment_secret = $dest_node.node.create_inbound_payment_for_hash(payment_hash, $min_value_msat, 7200).unwrap();
+ let payment_secret = $dest_node.node.create_inbound_payment_for_hash(payment_hash, $min_value_msat, 7200, $min_final_cltv_expiry_delta).unwrap();
(payment_preimage, payment_hash, payment_secret)
}
- }
+ };
}
#[macro_export]
use crate::chain::channelmonitor;
use crate::chain::channelmonitor::{CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY};
use crate::chain::transaction::OutPoint;
-use crate::chain::keysinterface::{BaseSign, EntropySource};
+use crate::chain::keysinterface::{ChannelSigner, EcdsaChannelSigner, EntropySource};
use crate::ln::{PaymentPreimage, PaymentSecret, PaymentHash};
use crate::ln::channel::{commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC, CONCURRENT_INBOUND_HTLC_FEE_BUFFER, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MIN_AFFORDABLE_HTLC_COUNT};
use crate::ln::channelmanager::{self, PaymentId, RAACommitmentOrder, PaymentSendFailure, BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA};
let (payment_preimage, payment_hash, _) = route_payment(&nodes[0], &vec!(&nodes[1])[..], 900_000);
let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[0], 800_000);
- let node_a_payment_secret = nodes[0].node.create_inbound_payment_for_hash(payment_hash, None, 7200).unwrap();
+ let node_a_payment_secret = nodes[0].node.create_inbound_payment_for_hash(payment_hash, None, 7200, None).unwrap();
send_along_route_with_secret(&nodes[1], route, &[&[&nodes[0]]], 800_000, payment_hash, node_a_payment_secret);
// Provide preimage to node 0 by claiming payment
let (our_payment_preimage, duplicate_payment_hash, _) = route_payment(&nodes[0], &[&nodes[1], &nodes[2]], 900_000);
- let payment_secret = nodes[3].node.create_inbound_payment_for_hash(duplicate_payment_hash, None, 7200).unwrap();
+ let payment_secret = nodes[3].node.create_inbound_payment_for_hash(duplicate_payment_hash, None, 7200, None).unwrap();
// We reduce the final CLTV here by a somewhat arbitrary constant to keep it under the one-byte
// script push size limit so that the below script length checks match
// ACCEPTED_HTLC_SCRIPT_WEIGHT.
let (_, payment_hash_2, _) = route_payment(&nodes[0], &[&nodes[2], &nodes[3], &nodes[4]], ds_dust_limit*1000); // not added < dust limit + HTLC tx fee
let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], ds_dust_limit*1000);
// 2nd HTLC:
- send_along_route_with_secret(&nodes[1], route.clone(), &[&[&nodes[2], &nodes[3], &nodes[5]]], ds_dust_limit*1000, payment_hash_1, nodes[5].node.create_inbound_payment_for_hash(payment_hash_1, None, 7200).unwrap()); // not added < dust limit + HTLC tx fee
+ send_along_route_with_secret(&nodes[1], route.clone(), &[&[&nodes[2], &nodes[3], &nodes[5]]], ds_dust_limit*1000, payment_hash_1, nodes[5].node.create_inbound_payment_for_hash(payment_hash_1, None, 7200, None).unwrap()); // not added < dust limit + HTLC tx fee
// 3rd HTLC:
- send_along_route_with_secret(&nodes[1], route, &[&[&nodes[2], &nodes[3], &nodes[5]]], ds_dust_limit*1000, payment_hash_2, nodes[5].node.create_inbound_payment_for_hash(payment_hash_2, None, 7200).unwrap()); // not added < dust limit + HTLC tx fee
+ send_along_route_with_secret(&nodes[1], route, &[&[&nodes[2], &nodes[3], &nodes[5]]], ds_dust_limit*1000, payment_hash_2, nodes[5].node.create_inbound_payment_for_hash(payment_hash_2, None, 7200, None).unwrap()); // not added < dust limit + HTLC tx fee
// 4th HTLC:
let (_, payment_hash_3, _) = route_payment(&nodes[0], &[&nodes[2], &nodes[3], &nodes[4]], 1000000);
// 5th HTLC:
let (_, payment_hash_4, _) = route_payment(&nodes[0], &[&nodes[2], &nodes[3], &nodes[4]], 1000000);
let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], 1000000);
// 6th HTLC:
- send_along_route_with_secret(&nodes[1], route.clone(), &[&[&nodes[2], &nodes[3], &nodes[5]]], 1000000, payment_hash_3, nodes[5].node.create_inbound_payment_for_hash(payment_hash_3, None, 7200).unwrap());
+ send_along_route_with_secret(&nodes[1], route.clone(), &[&[&nodes[2], &nodes[3], &nodes[5]]], 1000000, payment_hash_3, nodes[5].node.create_inbound_payment_for_hash(payment_hash_3, None, 7200, None).unwrap());
// 7th HTLC:
- send_along_route_with_secret(&nodes[1], route, &[&[&nodes[2], &nodes[3], &nodes[5]]], 1000000, payment_hash_4, nodes[5].node.create_inbound_payment_for_hash(payment_hash_4, None, 7200).unwrap());
+ send_along_route_with_secret(&nodes[1], route, &[&[&nodes[2], &nodes[3], &nodes[5]]], 1000000, payment_hash_4, nodes[5].node.create_inbound_payment_for_hash(payment_hash_4, None, 7200, None).unwrap());
// 8th HTLC:
let (_, payment_hash_5, _) = route_payment(&nodes[0], &[&nodes[2], &nodes[3], &nodes[4]], 1000000);
// 9th HTLC:
let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], ds_dust_limit*1000);
- send_along_route_with_secret(&nodes[1], route, &[&[&nodes[2], &nodes[3], &nodes[5]]], ds_dust_limit*1000, payment_hash_5, nodes[5].node.create_inbound_payment_for_hash(payment_hash_5, None, 7200).unwrap()); // not added < dust limit + HTLC tx fee
+ send_along_route_with_secret(&nodes[1], route, &[&[&nodes[2], &nodes[3], &nodes[5]]], ds_dust_limit*1000, payment_hash_5, nodes[5].node.create_inbound_payment_for_hash(payment_hash_5, None, 7200, None).unwrap()); // not added < dust limit + HTLC tx fee
// 10th HTLC:
let (_, payment_hash_6, _) = route_payment(&nodes[0], &[&nodes[2], &nodes[3], &nodes[4]], ds_dust_limit*1000); // not added < dust limit + HTLC tx fee
// 11th HTLC:
let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], 1000000);
- send_along_route_with_secret(&nodes[1], route, &[&[&nodes[2], &nodes[3], &nodes[5]]], 1000000, payment_hash_6, nodes[5].node.create_inbound_payment_for_hash(payment_hash_6, None, 7200).unwrap());
+ send_along_route_with_secret(&nodes[1], route, &[&[&nodes[2], &nodes[3], &nodes[5]]], 1000000, payment_hash_6, nodes[5].node.create_inbound_payment_for_hash(payment_hash_6, None, 7200, None).unwrap());
// Double-check that six of the new HTLC were added
// We now have six HTLCs pending over the dust limit and six HTLCs under the dust limit (ie,
let payment_params = PaymentParameters::from_node_id(nodes[1].node.get_our_node_id()).with_features(nodes[1].node.invoice_features());
let route = get_route(&nodes[0].node.get_our_node_id(), &payment_params, &nodes[0].network_graph.read_only(), None, 10_000, TEST_FINAL_CLTV, nodes[0].logger, &scorer, &random_seed_bytes).unwrap();
let (_, our_payment_hash, _) = get_payment_preimage_hash!(nodes[0]);
- let our_payment_secret = nodes[1].node.create_inbound_payment_for_hash(our_payment_hash, Some(100_000), 7200).unwrap();
+ let our_payment_secret = nodes[1].node.create_inbound_payment_for_hash(our_payment_hash, Some(100_000), 7200, None).unwrap();
nodes[0].node.send_payment(&route, our_payment_hash, &Some(our_payment_secret), PaymentId(our_payment_hash.0)).unwrap();
check_added_monitors!(nodes[0], 1);
create_announced_chan_between_nodes(&nodes, 0, 1).0.contents.short_channel_id;
{
- let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(100_000), 7200).unwrap();
+ let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(100_000), 7200, None).unwrap();
let (route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000);
nodes[0].node.send_payment(&route, payment_hash, &Some(payment_secret), PaymentId(payment_hash.0)).unwrap();
check_added_monitors!(nodes[0], 1);
let random_payment_hash = PaymentHash([42; 32]);
let random_payment_secret = PaymentSecret([43; 32]);
- let (our_payment_hash, our_payment_secret) = nodes[1].node.create_inbound_payment(Some(100_000), 2).unwrap();
+ let (our_payment_hash, our_payment_secret) = nodes[1].node.create_inbound_payment(Some(100_000), 2, None).unwrap();
let (route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000);
// All the below cases should end up being handled exactly identically, so we macro the
_ => panic!("Unexpected event"),
};
}
+
+fn do_payment_with_custom_min_final_cltv_expiry(valid_delta: bool, use_user_hash: bool) {
+ let mut chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+ let min_final_cltv_expiry_delta = 120;
+ let final_cltv_expiry_delta = if valid_delta { min_final_cltv_expiry_delta + 2 } else {
+ min_final_cltv_expiry_delta - 2 };
+ let recv_value = 100_000;
+
+ create_chan_between_nodes(&nodes[0], &nodes[1]);
+
+ let payment_parameters = PaymentParameters::from_node_id(nodes[1].node.get_our_node_id());
+ let (payment_hash, payment_preimage, payment_secret) = if use_user_hash {
+ let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash!(nodes[1],
+ Some(recv_value), Some(min_final_cltv_expiry_delta));
+ (payment_hash, payment_preimage, payment_secret)
+ } else {
+ let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(recv_value), 7200, Some(min_final_cltv_expiry_delta)).unwrap();
+ (payment_hash, nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap(), payment_secret)
+ };
+ let route = get_route!(nodes[0], payment_parameters, recv_value, final_cltv_expiry_delta as u32).unwrap();
+ nodes[0].node.send_payment(&route, payment_hash, &Some(payment_secret), PaymentId(payment_hash.0)).unwrap();
+ check_added_monitors!(nodes[0], 1);
+ let mut events = nodes[0].node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), 1);
+ let mut payment_event = SendEvent::from_event(events.pop().unwrap());
+ nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &payment_event.msgs[0]);
+ commitment_signed_dance!(nodes[1], nodes[0], payment_event.commitment_msg, false);
+ expect_pending_htlcs_forwardable!(nodes[1]);
+
+ if valid_delta {
+ expect_payment_claimable!(nodes[1], payment_hash, payment_secret, recv_value, if use_user_hash {
+ None } else { Some(payment_preimage) }, nodes[1].node.get_our_node_id());
+
+ claim_payment(&nodes[0], &vec!(&nodes[1])[..], payment_preimage);
+ } else {
+ expect_pending_htlcs_forwardable_and_htlc_handling_failed!(nodes[1], vec![HTLCDestination::FailedPayment { payment_hash }]);
+
+ check_added_monitors!(nodes[1], 1);
+
+ let fail_updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
+ nodes[0].node.handle_update_fail_htlc(&nodes[1].node.get_our_node_id(), &fail_updates.update_fail_htlcs[0]);
+ commitment_signed_dance!(nodes[0], nodes[1], fail_updates.commitment_signed, false, true);
+
+ expect_payment_failed!(nodes[0], payment_hash, true);
+ }
+}
+
+#[test]
+fn test_payment_with_custom_min_cltv_expiry_delta() {
+ do_payment_with_custom_min_final_cltv_expiry(false, false);
+ do_payment_with_custom_min_final_cltv_expiry(false, true);
+ do_payment_with_custom_min_final_cltv_expiry(true, false);
+ do_payment_with_custom_min_final_cltv_expiry(true, true);
+}
enum Method {
LdkPaymentHash = 0,
UserPaymentHash = 1,
+ LdkPaymentHashCustomFinalCltv = 2,
+ UserPaymentHashCustomFinalCltv = 3,
}
impl Method {
match bits {
bits if bits == Method::LdkPaymentHash as u8 => Ok(Method::LdkPaymentHash),
bits if bits == Method::UserPaymentHash as u8 => Ok(Method::UserPaymentHash),
+ bits if bits == Method::LdkPaymentHashCustomFinalCltv as u8 => Ok(Method::LdkPaymentHashCustomFinalCltv),
+ bits if bits == Method::UserPaymentHashCustomFinalCltv as u8 => Ok(Method::UserPaymentHashCustomFinalCltv),
unknown => Err(unknown),
}
}
}
+fn min_final_cltv_expiry_delta_from_metadata(bytes: [u8; METADATA_LEN]) -> u16 {
+ let expiry_bytes = &bytes[AMT_MSAT_LEN..];
+ u16::from_be_bytes([expiry_bytes[0], expiry_bytes[1]])
+}
+
/// Equivalent to [`crate::ln::channelmanager::ChannelManager::create_inbound_payment`], but no
/// `ChannelManager` is required. Useful for generating invoices for [phantom node payments] without
/// a `ChannelManager`.
///
/// `current_time` is a Unix timestamp representing the current time.
///
+/// Note that if `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable
+/// on versions of LDK prior to 0.0.114.
+///
/// [phantom node payments]: crate::chain::keysinterface::PhantomKeysManager
/// [`NodeSigner::get_inbound_payment_key_material`]: crate::chain::keysinterface::NodeSigner::get_inbound_payment_key_material
-pub fn create<ES: Deref>(keys: &ExpandedKey, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64) -> Result<(PaymentHash, PaymentSecret), ()>
+pub fn create<ES: Deref>(keys: &ExpandedKey, min_value_msat: Option<u64>,
+ invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64,
+ min_final_cltv_expiry_delta: Option<u16>) -> Result<(PaymentHash, PaymentSecret), ()>
where ES::Target: EntropySource
{
- let metadata_bytes = construct_metadata_bytes(min_value_msat, Method::LdkPaymentHash, invoice_expiry_delta_secs, current_time)?;
+ let metadata_bytes = construct_metadata_bytes(min_value_msat, if min_final_cltv_expiry_delta.is_some() {
+ Method::LdkPaymentHashCustomFinalCltv
+ } else {
+ Method::LdkPaymentHash
+ }, invoice_expiry_delta_secs, current_time, min_final_cltv_expiry_delta)?;
let mut iv_bytes = [0 as u8; IV_LEN];
let rand_bytes = entropy_source.get_secure_random_bytes();
///
/// See [`create`] for information on the `keys` and `current_time` parameters.
///
+/// Note that if `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable
+/// on versions of LDK prior to 0.0.114.
+///
/// [phantom node payments]: crate::chain::keysinterface::PhantomKeysManager
-pub fn create_from_hash(keys: &ExpandedKey, min_value_msat: Option<u64>, payment_hash: PaymentHash, invoice_expiry_delta_secs: u32, current_time: u64) -> Result<PaymentSecret, ()> {
- let metadata_bytes = construct_metadata_bytes(min_value_msat, Method::UserPaymentHash, invoice_expiry_delta_secs, current_time)?;
+pub fn create_from_hash(keys: &ExpandedKey, min_value_msat: Option<u64>, payment_hash: PaymentHash,
+ invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option<u16>) -> Result<PaymentSecret, ()> {
+ let metadata_bytes = construct_metadata_bytes(min_value_msat, if min_final_cltv_expiry_delta.is_some() {
+ Method::UserPaymentHashCustomFinalCltv
+ } else {
+ Method::UserPaymentHash
+ }, invoice_expiry_delta_secs, current_time, min_final_cltv_expiry_delta)?;
let mut hmac = HmacEngine::<Sha256>::new(&keys.user_pmt_hash_key);
hmac.input(&metadata_bytes);
Ok(construct_payment_secret(&iv_bytes, &metadata_bytes, &keys.metadata_key))
}
-fn construct_metadata_bytes(min_value_msat: Option<u64>, payment_type: Method, invoice_expiry_delta_secs: u32, highest_seen_timestamp: u64) -> Result<[u8; METADATA_LEN], ()> {
+fn construct_metadata_bytes(min_value_msat: Option<u64>, payment_type: Method,
+ invoice_expiry_delta_secs: u32, highest_seen_timestamp: u64, min_final_cltv_expiry_delta: Option<u16>) -> Result<[u8; METADATA_LEN], ()> {
if min_value_msat.is_some() && min_value_msat.unwrap() > MAX_VALUE_MSAT {
return Err(());
}
// than two hours in the future. Thus, we add two hours here as a buffer to ensure we
// absolutely never fail a payment too early.
// Note that we assume that received blocks have reasonably up-to-date timestamps.
- let expiry_bytes = (highest_seen_timestamp + invoice_expiry_delta_secs as u64 + 7200).to_be_bytes();
+ let expiry_timestamp = highest_seen_timestamp + invoice_expiry_delta_secs as u64 + 7200;
+ let mut expiry_bytes = expiry_timestamp.to_be_bytes();
+
+ // `min_value_msat` should fit in (64 bits - 3 payment type bits =) 61 bits as an unsigned integer.
+ // This should leave us with a maximum value greater than the 21M BTC supply cap anyway.
+ if min_value_msat.is_some() && min_value_msat.unwrap() > ((1u64 << 61) - 1) { return Err(()); }
+
+ // `expiry_timestamp` should fit in (64 bits - 2 delta bytes =) 48 bits as an unsigned integer.
+ // Bitcoin's block header timestamps are actually `u32`s, so we're technically already limited to
+ // the much smaller maximum timestamp of `u32::MAX` for now, but we check the u64 `expiry_timestamp`
+ // for future-proofing.
+ if min_final_cltv_expiry_delta.is_some() && expiry_timestamp > ((1u64 << 48) - 1) { return Err(()); }
+
+ if let Some(min_final_cltv_expiry_delta) = min_final_cltv_expiry_delta {
+ let bytes = min_final_cltv_expiry_delta.to_be_bytes();
+ expiry_bytes[0] |= bytes[0];
+ expiry_bytes[1] |= bytes[1];
+ }
let mut metadata_bytes: [u8; METADATA_LEN] = [0; METADATA_LEN];
+
metadata_bytes[..AMT_MSAT_LEN].copy_from_slice(&min_amt_msat_bytes);
metadata_bytes[AMT_MSAT_LEN..].copy_from_slice(&expiry_bytes);
/// secret (and, if supplied by LDK, our payment preimage) to include encrypted metadata about the
/// payment.
///
-/// The metadata is constructed as:
+/// For payments without a custom `min_final_cltv_expiry_delta`, the metadata is constructed as:
/// payment method (3 bits) || payment amount (8 bytes - 3 bits) || expiry (8 bytes)
-/// and encrypted using a key derived from [`NodeSigner::get_inbound_payment_key_material`].
+///
+/// For payments including a custom `min_final_cltv_expiry_delta`, the metadata is constructed as:
+/// payment method (3 bits) || payment amount (8 bytes - 3 bits) || min_final_cltv_expiry_delta (2 bytes) || expiry (6 bytes)
+///
+/// In both cases the result is then encrypted using a key derived from [`NodeSigner::get_inbound_payment_key_material`].
///
/// Then on payment receipt, we verify in this method that the payment preimage and payment secret
/// match what was constructed.
/// [`NodeSigner::get_inbound_payment_key_material`]: crate::chain::keysinterface::NodeSigner::get_inbound_payment_key_material
/// [`create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment
/// [`create_inbound_payment_for_hash`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash
-pub(super) fn verify<L: Deref>(payment_hash: PaymentHash, payment_data: &msgs::FinalOnionHopData, highest_seen_timestamp: u64, keys: &ExpandedKey, logger: &L) -> Result<Option<PaymentPreimage>, ()>
+pub(super) fn verify<L: Deref>(payment_hash: PaymentHash, payment_data: &msgs::FinalOnionHopData,
+ highest_seen_timestamp: u64, keys: &ExpandedKey, logger: &L) -> Result<
+ (Option<PaymentPreimage>, Option<u16>), ()>
where L::Target: Logger
{
let (iv_bytes, metadata_bytes) = decrypt_metadata(payment_data.payment_secret, keys);
let payment_type_res = Method::from_bits((metadata_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET);
let mut amt_msat_bytes = [0; AMT_MSAT_LEN];
+ let mut expiry_bytes = [0; METADATA_LEN - AMT_MSAT_LEN];
amt_msat_bytes.copy_from_slice(&metadata_bytes[..AMT_MSAT_LEN]);
+ expiry_bytes.copy_from_slice(&metadata_bytes[AMT_MSAT_LEN..]);
// Zero out the bits reserved to indicate the payment type.
amt_msat_bytes[0] &= 0b00011111;
- let min_amt_msat: u64 = u64::from_be_bytes(amt_msat_bytes.into());
- let expiry = u64::from_be_bytes(metadata_bytes[AMT_MSAT_LEN..].try_into().unwrap());
+ let mut min_final_cltv_expiry_delta = None;
- // Make sure to check to check the HMAC before doing the other checks below, to mitigate timing
- // attacks.
+ // Make sure to check the HMAC before doing the other checks below, to mitigate timing attacks.
let mut payment_preimage = None;
+
match payment_type_res {
- Ok(Method::UserPaymentHash) => {
+ Ok(Method::UserPaymentHash) | Ok(Method::UserPaymentHashCustomFinalCltv) => {
let mut hmac = HmacEngine::<Sha256>::new(&keys.user_pmt_hash_key);
hmac.input(&metadata_bytes[..]);
hmac.input(&payment_hash.0);
return Err(())
}
},
- Ok(Method::LdkPaymentHash) => {
+ Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => {
match derive_ldk_payment_preimage(payment_hash, &iv_bytes, &metadata_bytes, keys) {
Ok(preimage) => payment_preimage = Some(preimage),
Err(bad_preimage_bytes) => {
}
}
+ match payment_type_res {
+ Ok(Method::UserPaymentHashCustomFinalCltv) | Ok(Method::LdkPaymentHashCustomFinalCltv) => {
+ min_final_cltv_expiry_delta = Some(min_final_cltv_expiry_delta_from_metadata(metadata_bytes));
+ // Zero out first two bytes of expiry reserved for `min_final_cltv_expiry_delta`.
+ expiry_bytes[0] &= 0;
+ expiry_bytes[1] &= 0;
+ }
+ _ => {}
+ }
+
+ let min_amt_msat: u64 = u64::from_be_bytes(amt_msat_bytes.into());
+ let expiry = u64::from_be_bytes(expiry_bytes.try_into().unwrap());
+
if payment_data.total_msat < min_amt_msat {
log_trace!(logger, "Failing HTLC with payment_hash {} due to total_msat {} being less than the minimum amount of {} msat", log_bytes!(payment_hash.0), payment_data.total_msat, min_amt_msat);
return Err(())
return Err(())
}
- Ok(payment_preimage)
+ Ok((payment_preimage, min_final_cltv_expiry_delta))
}
pub(super) fn get_payment_preimage(payment_hash: PaymentHash, payment_secret: PaymentSecret, keys: &ExpandedKey) -> Result<PaymentPreimage, APIError> {
let (iv_bytes, metadata_bytes) = decrypt_metadata(payment_secret, keys);
match Method::from_bits((metadata_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET) {
- Ok(Method::LdkPaymentHash) => {
+ Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => {
derive_ldk_payment_preimage(payment_hash, &iv_bytes, &metadata_bytes, keys)
.map_err(|bad_preimage_bytes| APIError::APIMisuseError {
err: format!("Payment hash {} did not match decoded preimage {}", log_bytes!(payment_hash.0), log_bytes!(bad_preimage_bytes))
})
},
- Ok(Method::UserPaymentHash) => Err(APIError::APIMisuseError {
+ Ok(Method::UserPaymentHash) | Ok(Method::UserPaymentHashCustomFinalCltv) => Err(APIError::APIMisuseError {
err: "Expected payment type to be LdkPaymentHash, instead got UserPaymentHash".to_string()
}),
Err(other) => Err(APIError::APIMisuseError { err: format!("Unknown payment type: {}", other) }),
let amt_msat = 60_000;
let expiry_secs = 60 * 60;
- let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(amt_msat), expiry_secs).unwrap();
+ let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(amt_msat), expiry_secs, None).unwrap();
let payment_params = PaymentParameters::from_node_id(nodes[1].node.get_our_node_id())
.with_features(nodes[1].node.invoice_features());
route_params.final_cltv_expiry_delta, nodes[0].logger, &scorer, &random_seed_bytes
).unwrap();
- let (payment_hash, payment_secret) = nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60).unwrap();
+ let (payment_hash, payment_secret) = nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None).unwrap();
nodes[0].node.send_payment(&route, payment_hash, &Some(payment_secret), PaymentId(payment_hash.0)).unwrap();
let payment_event = {
{
--- /dev/null
+// This file is Copyright its original authors, visible in version control
+// history.
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Data structures and encoding for `invoice` messages.
+//!
+//! An [`Invoice`] can be built from a parsed [`InvoiceRequest`] for the "offer to be paid" flow or
+//! from a [`Refund`] as an "offer for money" flow. The expected recipient of the payment then sends
+//! the invoice to the intended payer, who will then pay it.
+//!
+//! 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;
+//!
+//! use bitcoin::hashes::Hash;
+//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
+//! use core::convert::{Infallible, TryFrom};
+//! use lightning::offers::invoice_request::InvoiceRequest;
+//! use lightning::offers::refund::Refund;
+//! use lightning::util::ser::Writeable;
+//!
+//! # use lightning::ln::PaymentHash;
+//! # use lightning::offers::invoice::BlindedPayInfo;
+//! # use lightning::onion_message::BlindedPath;
+//! #
+//! # fn create_payment_paths() -> Vec<(BlindedPath, BlindedPayInfo)> { unimplemented!() }
+//! # fn create_payment_hash() -> PaymentHash { unimplemented!() }
+//! #
+//! # fn parse_invoice_request(bytes: Vec<u8>) -> Result<(), lightning::offers::parse::ParseError> {
+//! let payment_paths = create_payment_paths();
+//! let payment_hash = create_payment_hash();
+//! let secp_ctx = Secp256k1::new();
+//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?);
+//! let pubkey = PublicKey::from(keys);
+//! let wpubkey_hash = bitcoin::util::key::PublicKey::new(pubkey).wpubkey_hash().unwrap();
+//! let mut buffer = Vec::new();
+//!
+//! // Invoice for the "offer to be paid" flow.
+//! InvoiceRequest::try_from(bytes)?
+//! .respond_with(payment_paths, payment_hash)?
+//! .relative_expiry(3600)
+//! .allow_mpp()
+//! .fallback_v0_p2wpkh(&wpubkey_hash)
+//! .build()?
+//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
+//! .expect("failed verifying signature")
+//! .write(&mut buffer)
+//! .unwrap();
+//! # Ok(())
+//! # }
+//!
+//! # fn parse_refund(bytes: Vec<u8>) -> Result<(), lightning::offers::parse::ParseError> {
+//! # let payment_paths = create_payment_paths();
+//! # let payment_hash = create_payment_hash();
+//! # let secp_ctx = Secp256k1::new();
+//! # let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?);
+//! # let pubkey = PublicKey::from(keys);
+//! # let wpubkey_hash = bitcoin::util::key::PublicKey::new(pubkey).wpubkey_hash().unwrap();
+//! # let mut buffer = Vec::new();
+//!
+//! // Invoice for the "offer for money" flow.
+//! "lnr1qcp4256ypq"
+//! .parse::<Refund>()?
+//! .respond_with(payment_paths, payment_hash, pubkey)?
+//! .relative_expiry(3600)
+//! .allow_mpp()
+//! .fallback_v0_p2wpkh(&wpubkey_hash)
+//! .build()?
+//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
+//! .expect("failed verifying signature")
+//! .write(&mut buffer)
+//! .unwrap();
+//! # Ok(())
+//! # }
+//!
+//! ```
+
+use bitcoin::blockdata::constants::ChainHash;
+use bitcoin::hash_types::{WPubkeyHash, WScriptHash};
+use bitcoin::hashes::Hash;
+use bitcoin::network::constants::Network;
+use bitcoin::secp256k1::{Message, PublicKey};
+use bitcoin::secp256k1::schnorr::Signature;
+use bitcoin::util::address::{Address, Payload, WitnessVersion};
+use bitcoin::util::schnorr::TweakedPublicKey;
+use core::convert::TryFrom;
+use core::time::Duration;
+use crate::io;
+use crate::ln::PaymentHash;
+use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
+use crate::ln::msgs::DecodeError;
+use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
+use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, WithoutSignatures, self};
+use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef};
+use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
+use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef};
+use crate::offers::refund::{Refund, RefundContents};
+use crate::onion_message::BlindedPath;
+use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer};
+
+use crate::prelude::*;
+
+#[cfg(feature = "std")]
+use std::time::SystemTime;
+
+const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
+
+const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
+
+/// Builds an [`Invoice`] from either:
+/// - an [`InvoiceRequest`] for the "offer to be paid" flow or
+/// - a [`Refund`] for the "offer for money" flow.
+///
+/// See [module-level documentation] for usage.
+///
+/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+/// [`Refund`]: crate::offers::refund::Refund
+/// [module-level documentation]: self
+pub struct InvoiceBuilder<'a> {
+ invreq_bytes: &'a Vec<u8>,
+ invoice: InvoiceContents,
+}
+
+impl<'a> InvoiceBuilder<'a> {
+ pub(super) fn for_offer(
+ invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
+ created_at: Duration, payment_hash: PaymentHash
+ ) -> Result<Self, SemanticError> {
+ let amount_msats = match invoice_request.amount_msats() {
+ Some(amount_msats) => amount_msats,
+ None => match invoice_request.contents.offer.amount() {
+ Some(Amount::Bitcoin { amount_msats }) => {
+ amount_msats * invoice_request.quantity().unwrap_or(1)
+ },
+ Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency),
+ None => return Err(SemanticError::MissingAmount),
+ },
+ };
+
+ let contents = InvoiceContents::ForOffer {
+ invoice_request: invoice_request.contents.clone(),
+ 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(),
+ },
+ };
+
+ Self::new(&invoice_request.bytes, contents)
+ }
+
+ pub(super) fn for_refund(
+ refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration,
+ payment_hash: PaymentHash, signing_pubkey: PublicKey
+ ) -> Result<Self, SemanticError> {
+ let contents = InvoiceContents::ForRefund {
+ refund: refund.contents.clone(),
+ fields: InvoiceFields {
+ payment_paths, created_at, relative_expiry: None, payment_hash,
+ amount_msats: refund.amount_msats(), fallbacks: None,
+ features: Bolt12InvoiceFeatures::empty(), signing_pubkey,
+ },
+ };
+
+ Self::new(&refund.bytes, contents)
+ }
+
+ fn new(invreq_bytes: &'a Vec<u8>, contents: InvoiceContents) -> Result<Self, SemanticError> {
+ if contents.fields().payment_paths.is_empty() {
+ return Err(SemanticError::MissingPaths);
+ }
+
+ Ok(Self { invreq_bytes, invoice: contents })
+ }
+
+ /// Sets the [`Invoice::relative_expiry`] as seconds since [`Invoice::created_at`]. Any expiry
+ /// that has already passed is valid and can be checked for using [`Invoice::is_expired`].
+ ///
+ /// Successive calls to this method will override the previous setting.
+ pub fn relative_expiry(mut self, relative_expiry_secs: u32) -> Self {
+ let relative_expiry = Duration::from_secs(relative_expiry_secs as u64);
+ self.invoice.fields_mut().relative_expiry = Some(relative_expiry);
+ self
+ }
+
+ /// Adds a P2WSH address to [`Invoice::fallbacks`].
+ ///
+ /// Successive calls to this method will add another address. Caller is responsible for not
+ /// adding duplicate addresses and only calling if capable of receiving to P2WSH addresses.
+ pub fn fallback_v0_p2wsh(mut self, script_hash: &WScriptHash) -> Self {
+ let address = FallbackAddress {
+ version: WitnessVersion::V0.to_num(),
+ program: Vec::from(&script_hash.into_inner()[..]),
+ };
+ self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address);
+ self
+ }
+
+ /// Adds a P2WPKH address to [`Invoice::fallbacks`].
+ ///
+ /// Successive calls to this method will add another address. Caller is responsible for not
+ /// adding duplicate addresses and only calling if capable of receiving to P2WPKH addresses.
+ pub fn fallback_v0_p2wpkh(mut self, pubkey_hash: &WPubkeyHash) -> Self {
+ let address = FallbackAddress {
+ version: WitnessVersion::V0.to_num(),
+ program: Vec::from(&pubkey_hash.into_inner()[..]),
+ };
+ self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address);
+ self
+ }
+
+ /// Adds a P2TR address to [`Invoice::fallbacks`].
+ ///
+ /// Successive calls to this method will add another address. Caller is responsible for not
+ /// adding duplicate addresses and only calling if capable of receiving to P2TR addresses.
+ pub fn fallback_v1_p2tr_tweaked(mut self, output_key: &TweakedPublicKey) -> Self {
+ let address = FallbackAddress {
+ version: WitnessVersion::V1.to_num(),
+ program: Vec::from(&output_key.serialize()[..]),
+ };
+ self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address);
+ self
+ }
+
+ /// Sets [`Invoice::features`] to indicate MPP may be used. Otherwise, MPP is disallowed.
+ pub fn allow_mpp(mut self) -> Self {
+ self.invoice.fields_mut().features.set_basic_mpp_optional();
+ self
+ }
+
+ /// Builds an unsigned [`Invoice`] after checking for valid semantics. It can be signed by
+ /// [`UnsignedInvoice::sign`].
+ pub fn build(self) -> Result<UnsignedInvoice<'a>, SemanticError> {
+ #[cfg(feature = "std")] {
+ if self.invoice.is_offer_or_refund_expired() {
+ return Err(SemanticError::AlreadyExpired);
+ }
+ }
+
+ let InvoiceBuilder { invreq_bytes, invoice } = self;
+ Ok(UnsignedInvoice { invreq_bytes, invoice })
+ }
+}
+
+/// A semantically valid [`Invoice`] that hasn't been signed.
+pub struct UnsignedInvoice<'a> {
+ invreq_bytes: &'a Vec<u8>,
+ invoice: InvoiceContents,
+}
+
+impl<'a> UnsignedInvoice<'a> {
+ /// Signs the invoice using the given function.
+ pub fn sign<F, E>(self, sign: F) -> Result<Invoice, SignError<E>>
+ where
+ F: FnOnce(&Message) -> Result<Signature, E>
+ {
+ // Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may
+ // have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or
+ // `RefundContents`.
+ let (_, _, _, invoice_tlv_stream) = self.invoice.as_tlv_stream();
+ let invoice_request_bytes = WithoutSignatures(self.invreq_bytes);
+ let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream);
+
+ let mut bytes = Vec::new();
+ unsigned_tlv_stream.write(&mut bytes).unwrap();
+
+ let pubkey = self.invoice.fields().signing_pubkey;
+ let signature = merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?;
+
+ // Append the signature TLV record to the bytes.
+ let signature_tlv_stream = SignatureTlvStreamRef {
+ signature: Some(&signature),
+ };
+ signature_tlv_stream.write(&mut bytes).unwrap();
+
+ Ok(Invoice {
+ bytes,
+ contents: self.invoice,
+ signature,
+ })
+ }
+}
+
+/// An `Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`].
+///
+/// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent
+/// directly after scanning a refund. It includes all the information needed to pay a recipient.
+///
+/// [`Offer`]: crate::offers::offer::Offer
+/// [`Refund`]: crate::offers::refund::Refund
+/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+pub struct Invoice {
+ bytes: Vec<u8>,
+ contents: InvoiceContents,
+ signature: Signature,
+}
+
+/// The contents of an [`Invoice`] for responding to either an [`Offer`] or a [`Refund`].
+///
+/// [`Offer`]: crate::offers::offer::Offer
+/// [`Refund`]: crate::offers::refund::Refund
+enum InvoiceContents {
+ /// Contents for an [`Invoice`] corresponding to an [`Offer`].
+ ///
+ /// [`Offer`]: crate::offers::offer::Offer
+ ForOffer {
+ invoice_request: InvoiceRequestContents,
+ fields: InvoiceFields,
+ },
+ /// Contents for an [`Invoice`] corresponding to a [`Refund`].
+ ///
+ /// [`Refund`]: crate::offers::refund::Refund
+ ForRefund {
+ refund: RefundContents,
+ fields: InvoiceFields,
+ },
+}
+
+/// Invoice-specific fields for an `invoice` message.
+struct InvoiceFields {
+ payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
+ created_at: Duration,
+ relative_expiry: Option<Duration>,
+ payment_hash: PaymentHash,
+ amount_msats: u64,
+ fallbacks: Option<Vec<FallbackAddress>>,
+ features: Bolt12InvoiceFeatures,
+ signing_pubkey: PublicKey,
+}
+
+impl Invoice {
+ /// Paths to the recipient originating from publicly reachable nodes, including information
+ /// needed for routing payments across them. Blinded paths provide recipient privacy by
+ /// obfuscating its node id.
+ pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
+ &self.contents.fields().payment_paths[..]
+ }
+
+ /// Duration since the Unix epoch when the invoice was created.
+ pub fn created_at(&self) -> Duration {
+ self.contents.fields().created_at
+ }
+
+ /// Duration since [`Invoice::created_at`] when the invoice has expired and therefore should no
+ /// longer be paid.
+ pub fn relative_expiry(&self) -> Duration {
+ self.contents.fields().relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY)
+ }
+
+ /// Whether the invoice has expired.
+ #[cfg(feature = "std")]
+ pub fn is_expired(&self) -> bool {
+ let absolute_expiry = self.created_at().checked_add(self.relative_expiry());
+ match absolute_expiry {
+ Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
+ Ok(elapsed) => elapsed > seconds_from_epoch,
+ Err(_) => false,
+ },
+ None => false,
+ }
+ }
+
+ /// SHA256 hash of the payment preimage that will be given in return for paying the invoice.
+ pub fn payment_hash(&self) -> PaymentHash {
+ self.contents.fields().payment_hash
+ }
+
+ /// The minimum amount required for a successful payment of the invoice.
+ pub fn amount_msats(&self) -> u64 {
+ self.contents.fields().amount_msats
+ }
+
+ /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to
+ /// least-preferred.
+ pub fn fallbacks(&self) -> Vec<Address> {
+ let network = match self.network() {
+ None => return Vec::new(),
+ Some(network) => network,
+ };
+
+ let to_valid_address = |address: &FallbackAddress| {
+ let version = match WitnessVersion::try_from(address.version) {
+ Ok(version) => version,
+ Err(_) => return None,
+ };
+
+ let program = &address.program;
+ if program.len() < 2 || program.len() > 40 {
+ return None;
+ }
+
+ let address = Address {
+ payload: Payload::WitnessProgram {
+ version,
+ program: address.program.clone(),
+ },
+ network,
+ };
+
+ if !address.is_standard() && version == WitnessVersion::V0 {
+ return None;
+ }
+
+ Some(address)
+ };
+
+ self.contents.fields().fallbacks
+ .as_ref()
+ .map(|fallbacks| fallbacks.iter().filter_map(to_valid_address).collect())
+ .unwrap_or_else(Vec::new)
+ }
+
+ fn network(&self) -> Option<Network> {
+ let chain = self.contents.chain();
+ if chain == ChainHash::using_genesis_block(Network::Bitcoin) {
+ Some(Network::Bitcoin)
+ } else if chain == ChainHash::using_genesis_block(Network::Testnet) {
+ Some(Network::Testnet)
+ } else if chain == ChainHash::using_genesis_block(Network::Signet) {
+ Some(Network::Signet)
+ } else if chain == ChainHash::using_genesis_block(Network::Regtest) {
+ Some(Network::Regtest)
+ } else {
+ None
+ }
+ }
+
+ /// Features pertaining to paying an invoice.
+ pub fn features(&self) -> &Bolt12InvoiceFeatures {
+ &self.contents.fields().features
+ }
+
+ /// The public key used to sign invoices.
+ pub fn signing_pubkey(&self) -> PublicKey {
+ self.contents.fields().signing_pubkey
+ }
+
+ /// Signature of the invoice using [`Invoice::signing_pubkey`].
+ pub fn signature(&self) -> Signature {
+ self.signature
+ }
+
+ #[cfg(test)]
+ fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
+ let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) =
+ self.contents.as_tlv_stream();
+ let signature_tlv_stream = SignatureTlvStreamRef {
+ signature: Some(&self.signature),
+ };
+ (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
+ signature_tlv_stream)
+ }
+}
+
+impl InvoiceContents {
+ /// Whether the original offer or refund has expired.
+ #[cfg(feature = "std")]
+ fn is_offer_or_refund_expired(&self) -> bool {
+ match self {
+ InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.offer.is_expired(),
+ InvoiceContents::ForRefund { refund, .. } => refund.is_expired(),
+ }
+ }
+
+ fn chain(&self) -> ChainHash {
+ match self {
+ InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(),
+ InvoiceContents::ForRefund { refund, .. } => refund.chain(),
+ }
+ }
+
+ fn fields(&self) -> &InvoiceFields {
+ match self {
+ InvoiceContents::ForOffer { fields, .. } => fields,
+ InvoiceContents::ForRefund { fields, .. } => fields,
+ }
+ }
+
+ fn fields_mut(&mut self) -> &mut InvoiceFields {
+ match self {
+ InvoiceContents::ForOffer { fields, .. } => fields,
+ InvoiceContents::ForRefund { fields, .. } => fields,
+ }
+ }
+
+ fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {
+ let (payer, offer, invoice_request) = match self {
+ InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(),
+ InvoiceContents::ForRefund { refund, .. } => refund.as_tlv_stream(),
+ };
+ let invoice = self.fields().as_tlv_stream();
+
+ (payer, offer, invoice_request, invoice)
+ }
+}
+
+impl InvoiceFields {
+ fn as_tlv_stream(&self) -> InvoiceTlvStreamRef {
+ let features = {
+ if self.features == Bolt12InvoiceFeatures::empty() { None }
+ else { Some(&self.features) }
+ };
+
+ InvoiceTlvStreamRef {
+ paths: Some(Iterable(self.payment_paths.iter().map(|(path, _)| path))),
+ blindedpay: Some(Iterable(self.payment_paths.iter().map(|(_, payinfo)| payinfo))),
+ created_at: Some(self.created_at.as_secs()),
+ relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
+ payment_hash: Some(&self.payment_hash),
+ amount: Some(self.amount_msats),
+ fallbacks: self.fallbacks.as_ref(),
+ features,
+ node_id: Some(&self.signing_pubkey),
+ }
+ }
+}
+
+impl Writeable for Invoice {
+ fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+ WithoutLength(&self.bytes).write(writer)
+ }
+}
+
+impl Writeable for InvoiceContents {
+ fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+ self.as_tlv_stream().write(writer)
+ }
+}
+
+impl TryFrom<Vec<u8>> for Invoice {
+ type Error = ParseError;
+
+ fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+ let parsed_invoice = ParsedMessage::<FullInvoiceTlvStream>::try_from(bytes)?;
+ Invoice::try_from(parsed_invoice)
+ }
+}
+
+tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
+ (160, paths: (Vec<BlindedPath>, WithoutLength, Iterable<'a, BlindedPathIter<'a>, BlindedPath>)),
+ (162, blindedpay: (Vec<BlindedPayInfo>, WithoutLength, Iterable<'a, BlindedPayInfoIter<'a>, BlindedPayInfo>)),
+ (164, created_at: (u64, HighZeroBytesDroppedBigSize)),
+ (166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)),
+ (168, payment_hash: PaymentHash),
+ (170, amount: (u64, HighZeroBytesDroppedBigSize)),
+ (172, fallbacks: (Vec<FallbackAddress>, WithoutLength)),
+ (174, features: (Bolt12InvoiceFeatures, WithoutLength)),
+ (176, node_id: PublicKey),
+});
+
+type BlindedPathIter<'a> = core::iter::Map<
+ core::slice::Iter<'a, (BlindedPath, BlindedPayInfo)>,
+ for<'r> fn(&'r (BlindedPath, BlindedPayInfo)) -> &'r BlindedPath,
+>;
+
+type BlindedPayInfoIter<'a> = core::iter::Map<
+ core::slice::Iter<'a, (BlindedPath, BlindedPayInfo)>,
+ for<'r> fn(&'r (BlindedPath, BlindedPayInfo)) -> &'r BlindedPayInfo,
+>;
+
+/// 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,
+}
+
+impl_writeable!(BlindedPayInfo, {
+ fee_base_msat,
+ fee_proportional_millionths,
+ cltv_expiry_delta,
+ htlc_minimum_msat,
+ htlc_maximum_msat,
+ features
+});
+
+/// Wire representation for an on-chain fallback address.
+#[derive(Clone, Debug, PartialEq)]
+pub(super) struct FallbackAddress {
+ version: u8,
+ program: Vec<u8>,
+}
+
+impl_writeable!(FallbackAddress, { version, program });
+
+type FullInvoiceTlvStream =
+ (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream);
+
+#[cfg(test)]
+type FullInvoiceTlvStreamRef<'a> = (
+ PayerTlvStreamRef<'a>,
+ OfferTlvStreamRef<'a>,
+ InvoiceRequestTlvStreamRef<'a>,
+ InvoiceTlvStreamRef<'a>,
+ SignatureTlvStreamRef<'a>,
+);
+
+impl SeekReadable for FullInvoiceTlvStream {
+ fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
+ let payer = SeekReadable::read(r)?;
+ let offer = SeekReadable::read(r)?;
+ let invoice_request = SeekReadable::read(r)?;
+ let invoice = SeekReadable::read(r)?;
+ let signature = SeekReadable::read(r)?;
+
+ Ok((payer, offer, invoice_request, invoice, signature))
+ }
+}
+
+type PartialInvoiceTlvStream =
+ (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream);
+
+type PartialInvoiceTlvStreamRef<'a> = (
+ PayerTlvStreamRef<'a>,
+ OfferTlvStreamRef<'a>,
+ InvoiceRequestTlvStreamRef<'a>,
+ InvoiceTlvStreamRef<'a>,
+);
+
+impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Invoice {
+ type Error = ParseError;
+
+ fn try_from(invoice: ParsedMessage<FullInvoiceTlvStream>) -> Result<Self, Self::Error> {
+ let ParsedMessage { bytes, tlv_stream } = invoice;
+ let (
+ payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
+ SignatureTlvStream { signature },
+ ) = tlv_stream;
+ let contents = InvoiceContents::try_from(
+ (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream)
+ )?;
+
+ let signature = match signature {
+ None => return Err(ParseError::InvalidSemantics(SemanticError::MissingSignature)),
+ Some(signature) => signature,
+ };
+ let pubkey = contents.fields().signing_pubkey;
+ merkle::verify_signature(&signature, SIGNATURE_TAG, &bytes, pubkey)?;
+
+ Ok(Invoice { bytes, contents, signature })
+ }
+}
+
+impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
+ type Error = SemanticError;
+
+ fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result<Self, Self::Error> {
+ let (
+ payer_tlv_stream,
+ offer_tlv_stream,
+ invoice_request_tlv_stream,
+ InvoiceTlvStream {
+ paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks,
+ features, node_id,
+ },
+ ) = tlv_stream;
+
+ let payment_paths = match (paths, blindedpay) {
+ (None, _) => return Err(SemanticError::MissingPaths),
+ (_, None) => return Err(SemanticError::InvalidPayInfo),
+ (Some(paths), _) if paths.is_empty() => return Err(SemanticError::MissingPaths),
+ (Some(paths), Some(blindedpay)) if paths.len() != blindedpay.len() => {
+ return Err(SemanticError::InvalidPayInfo);
+ },
+ (Some(paths), Some(blindedpay)) => {
+ paths.into_iter().zip(blindedpay.into_iter()).collect::<Vec<_>>()
+ },
+ };
+
+ let created_at = match created_at {
+ None => return Err(SemanticError::MissingCreationTime),
+ Some(timestamp) => Duration::from_secs(timestamp),
+ };
+
+ let relative_expiry = relative_expiry
+ .map(Into::<u64>::into)
+ .map(Duration::from_secs);
+
+ let payment_hash = match payment_hash {
+ None => return Err(SemanticError::MissingPaymentHash),
+ Some(payment_hash) => payment_hash,
+ };
+
+ let amount_msats = match amount {
+ None => return Err(SemanticError::MissingAmount),
+ Some(amount) => amount,
+ };
+
+ let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty);
+
+ let signing_pubkey = match node_id {
+ None => return Err(SemanticError::MissingSigningPubkey),
+ Some(node_id) => node_id,
+ };
+
+ let fields = InvoiceFields {
+ payment_paths, created_at, relative_expiry, payment_hash, amount_msats, fallbacks,
+ features, signing_pubkey,
+ };
+
+ match offer_tlv_stream.node_id {
+ Some(expected_signing_pubkey) => {
+ if fields.signing_pubkey != expected_signing_pubkey {
+ return Err(SemanticError::InvalidSigningPubkey);
+ }
+
+ let invoice_request = InvoiceRequestContents::try_from(
+ (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
+ )?;
+ Ok(InvoiceContents::ForOffer { invoice_request, fields })
+ },
+ None => {
+ let refund = RefundContents::try_from(
+ (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
+ )?;
+ Ok(InvoiceContents::ForRefund { refund, fields })
+ },
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{DEFAULT_RELATIVE_EXPIRY, BlindedPayInfo, 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::util::address::{Address, Payload, WitnessVersion};
+ use bitcoin::util::schnorr::TweakedPublicKey;
+ use core::convert::{Infallible, TryFrom};
+ use core::time::Duration;
+ use crate::ln::PaymentHash;
+ use crate::ln::msgs::DecodeError;
+ use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
+ use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
+ use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self};
+ use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef};
+ use crate::offers::parse::{ParseError, SemanticError};
+ use crate::offers::payer::PayerTlvStreamRef;
+ use crate::offers::refund::RefundBuilder;
+ use crate::onion_message::{BlindedHop, BlindedPath};
+ 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>;
+ }
+
+ impl<'a> ToBytes for FullInvoiceTlvStreamRef<'a> {
+ fn to_bytes(&self) -> Vec<u8> {
+ let mut buffer = Vec::new();
+ self.0.write(&mut buffer).unwrap();
+ self.1.write(&mut buffer).unwrap();
+ self.2.write(&mut buffer).unwrap();
+ self.3.write(&mut buffer).unwrap();
+ self.4.write(&mut buffer).unwrap();
+ buffer
+ }
+ }
+
+ 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();
+ let payment_hash = payment_hash();
+ let now = now();
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths.clone(), payment_hash, now).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ assert_eq!(invoice.bytes, buffer.as_slice());
+ assert_eq!(invoice.payment_paths(), payment_paths.as_slice());
+ assert_eq!(invoice.created_at(), now);
+ assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY);
+ #[cfg(feature = "std")]
+ assert!(!invoice.is_expired());
+ assert_eq!(invoice.payment_hash(), payment_hash);
+ assert_eq!(invoice.amount_msats(), 1000);
+ assert_eq!(invoice.fallbacks(), vec![]);
+ assert_eq!(invoice.features(), &Bolt12InvoiceFeatures::empty());
+ assert_eq!(invoice.signing_pubkey(), recipient_pubkey());
+ assert!(
+ merkle::verify_signature(
+ &invoice.signature, SIGNATURE_TAG, &invoice.bytes, recipient_pubkey()
+ ).is_ok()
+ );
+
+ assert_eq!(
+ invoice.as_tlv_stream(),
+ (
+ PayerTlvStreamRef { metadata: Some(&vec![1; 32]) },
+ OfferTlvStreamRef {
+ chains: None,
+ metadata: None,
+ currency: None,
+ amount: Some(1000),
+ description: Some(&String::from("foo")),
+ features: None,
+ absolute_expiry: None,
+ paths: None,
+ issuer: None,
+ quantity_max: None,
+ node_id: Some(&recipient_pubkey()),
+ },
+ InvoiceRequestTlvStreamRef {
+ chain: None,
+ amount: None,
+ features: None,
+ quantity: None,
+ payer_id: Some(&payer_pubkey()),
+ payer_note: None,
+ },
+ InvoiceTlvStreamRef {
+ paths: Some(Iterable(payment_paths.iter().map(|(path, _)| path))),
+ blindedpay: Some(Iterable(payment_paths.iter().map(|(_, payinfo)| payinfo))),
+ created_at: Some(now.as_secs()),
+ relative_expiry: None,
+ payment_hash: Some(&payment_hash),
+ amount: Some(1000),
+ fallbacks: None,
+ features: None,
+ node_id: Some(&recipient_pubkey()),
+ },
+ SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
+ ),
+ );
+
+ if let Err(e) = Invoice::try_from(buffer) {
+ panic!("error parsing invoice: {:?}", e);
+ }
+ }
+
+ #[test]
+ fn builds_invoice_for_refund_with_defaults() {
+ let payment_paths = payment_paths();
+ let payment_hash = payment_hash();
+ 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()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ assert_eq!(invoice.bytes, buffer.as_slice());
+ assert_eq!(invoice.payment_paths(), payment_paths.as_slice());
+ assert_eq!(invoice.created_at(), now);
+ assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY);
+ #[cfg(feature = "std")]
+ assert!(!invoice.is_expired());
+ assert_eq!(invoice.payment_hash(), payment_hash);
+ assert_eq!(invoice.amount_msats(), 1000);
+ assert_eq!(invoice.fallbacks(), vec![]);
+ assert_eq!(invoice.features(), &Bolt12InvoiceFeatures::empty());
+ assert_eq!(invoice.signing_pubkey(), recipient_pubkey());
+ assert!(
+ merkle::verify_signature(
+ &invoice.signature, SIGNATURE_TAG, &invoice.bytes, recipient_pubkey()
+ ).is_ok()
+ );
+
+ assert_eq!(
+ invoice.as_tlv_stream(),
+ (
+ PayerTlvStreamRef { metadata: Some(&vec![1; 32]) },
+ OfferTlvStreamRef {
+ chains: None,
+ metadata: None,
+ currency: None,
+ amount: None,
+ description: Some(&String::from("foo")),
+ features: None,
+ absolute_expiry: None,
+ paths: None,
+ issuer: None,
+ quantity_max: None,
+ node_id: None,
+ },
+ InvoiceRequestTlvStreamRef {
+ chain: None,
+ amount: Some(1000),
+ features: None,
+ quantity: None,
+ payer_id: Some(&payer_pubkey()),
+ payer_note: None,
+ },
+ InvoiceTlvStreamRef {
+ paths: Some(Iterable(payment_paths.iter().map(|(path, _)| path))),
+ blindedpay: Some(Iterable(payment_paths.iter().map(|(_, payinfo)| payinfo))),
+ created_at: Some(now.as_secs()),
+ relative_expiry: None,
+ payment_hash: Some(&payment_hash),
+ amount: Some(1000),
+ fallbacks: None,
+ features: None,
+ node_id: Some(&recipient_pubkey()),
+ },
+ SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
+ ),
+ );
+
+ if let Err(e) = Invoice::try_from(buffer) {
+ panic!("error parsing invoice: {:?}", e);
+ }
+ }
+
+ #[cfg(feature = "std")]
+ #[test]
+ fn builds_invoice_from_refund_with_expiration() {
+ let future_expiry = Duration::from_secs(u64::max_value());
+ let past_expiry = Duration::from_secs(0);
+
+ 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()
+ .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()
+ .build()
+ {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, SemanticError::AlreadyExpired),
+ }
+ }
+
+ #[test]
+ fn builds_invoice_with_relative_expiry() {
+ let now = now();
+ let one_hour = Duration::from_secs(3600);
+
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now).unwrap()
+ .relative_expiry(one_hour.as_secs() as u32)
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
+ #[cfg(feature = "std")]
+ assert!(!invoice.is_expired());
+ assert_eq!(invoice.relative_expiry(), one_hour);
+ assert_eq!(tlv_stream.relative_expiry, Some(one_hour.as_secs() as u32));
+
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().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()
+ .relative_expiry(one_hour.as_secs() as u32 - 1)
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
+ #[cfg(feature = "std")]
+ assert!(invoice.is_expired());
+ assert_eq!(invoice.relative_expiry(), one_hour - Duration::from_secs(1));
+ assert_eq!(tlv_stream.relative_expiry, Some(one_hour.as_secs() as u32 - 1));
+ }
+
+ #[test]
+ fn builds_invoice_with_amount_from_request() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .amount_msats(1001).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
+ assert_eq!(invoice.amount_msats(), 1001);
+ assert_eq!(tlv_stream.amount, Some(1001));
+ }
+
+ #[test]
+ fn builds_invoice_with_fallback_address() {
+ let script = Script::new();
+ let pubkey = bitcoin::util::key::PublicKey::new(recipient_pubkey());
+ let x_only_pubkey = XOnlyPublicKey::from_keypair(&recipient_keys()).0;
+ let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey);
+
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(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()
+ .sign(recipient_sign).unwrap();
+ let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
+ assert_eq!(
+ invoice.fallbacks(),
+ vec![
+ Address::p2wsh(&script, Network::Bitcoin),
+ Address::p2wpkh(&pubkey, Network::Bitcoin).unwrap(),
+ Address::p2tr_tweaked(tweaked_pubkey, Network::Bitcoin),
+ ],
+ );
+ assert_eq!(
+ tlv_stream.fallbacks,
+ Some(&vec![
+ FallbackAddress {
+ version: WitnessVersion::V0.to_num(),
+ program: Vec::from(&script.wscript_hash().into_inner()[..]),
+ },
+ FallbackAddress {
+ version: WitnessVersion::V0.to_num(),
+ program: Vec::from(&pubkey.wpubkey_hash().unwrap().into_inner()[..]),
+ },
+ FallbackAddress {
+ version: WitnessVersion::V1.to_num(),
+ program: Vec::from(&tweaked_pubkey.serialize()[..]),
+ },
+ ])
+ );
+ }
+
+ #[test]
+ fn builds_invoice_with_allow_mpp() {
+ let mut features = Bolt12InvoiceFeatures::empty();
+ features.set_basic_mpp_optional();
+
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .allow_mpp()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
+ assert_eq!(invoice.features(), &features);
+ assert_eq!(tlv_stream.features, Some(&features));
+ }
+
+ #[test]
+ fn fails_signing_invoice() {
+ match OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(|_| Err(()))
+ {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, SignError::Signing(())),
+ }
+
+ match OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign)
+ {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, SignError::Verification(secp256k1::Error::InvalidSignature)),
+ }
+ }
+
+ #[test]
+ fn parses_invoice_with_payment_paths() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ if let Err(e) = Invoice::try_from(buffer) {
+ panic!("error parsing invoice: {:?}", e);
+ }
+
+ let mut tlv_stream = invoice.as_tlv_stream();
+ tlv_stream.3.paths = None;
+
+ match Invoice::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaths)),
+ }
+
+ let mut tlv_stream = invoice.as_tlv_stream();
+ tlv_stream.3.blindedpay = None;
+
+ match Invoice::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidPayInfo)),
+ }
+
+ let empty_payment_paths = vec![];
+ let mut tlv_stream = invoice.as_tlv_stream();
+ tlv_stream.3.paths = Some(Iterable(empty_payment_paths.iter().map(|(path, _)| path)));
+
+ match Invoice::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaths)),
+ }
+
+ let mut payment_paths = payment_paths();
+ payment_paths.pop();
+ let mut tlv_stream = invoice.as_tlv_stream();
+ tlv_stream.3.blindedpay = Some(Iterable(payment_paths.iter().map(|(_, payinfo)| payinfo)));
+
+ match Invoice::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidPayInfo)),
+ }
+ }
+
+ #[test]
+ fn parses_invoice_with_created_at() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ if let Err(e) = Invoice::try_from(buffer) {
+ panic!("error parsing invoice: {:?}", e);
+ }
+
+ let mut tlv_stream = invoice.as_tlv_stream();
+ tlv_stream.3.created_at = None;
+
+ match Invoice::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingCreationTime));
+ },
+ }
+ }
+
+ #[test]
+ fn parses_invoice_with_relative_expiry() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .relative_expiry(3600)
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ match Invoice::try_from(buffer) {
+ Ok(invoice) => assert_eq!(invoice.relative_expiry(), Duration::from_secs(3600)),
+ Err(e) => panic!("error parsing invoice: {:?}", e),
+ }
+ }
+
+ #[test]
+ fn parses_invoice_with_payment_hash() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ if let Err(e) = Invoice::try_from(buffer) {
+ panic!("error parsing invoice: {:?}", e);
+ }
+
+ let mut tlv_stream = invoice.as_tlv_stream();
+ tlv_stream.3.payment_hash = None;
+
+ match Invoice::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaymentHash));
+ },
+ }
+ }
+
+ #[test]
+ fn parses_invoice_with_amount() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ if let Err(e) = Invoice::try_from(buffer) {
+ panic!("error parsing invoice: {:?}", e);
+ }
+
+ let mut tlv_stream = invoice.as_tlv_stream();
+ tlv_stream.3.amount = None;
+
+ match Invoice::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingAmount)),
+ }
+ }
+
+ #[test]
+ fn parses_invoice_with_allow_mpp() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .allow_mpp()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ match Invoice::try_from(buffer) {
+ Ok(invoice) => {
+ let mut features = Bolt12InvoiceFeatures::empty();
+ features.set_basic_mpp_optional();
+ assert_eq!(invoice.features(), &features);
+ },
+ Err(e) => panic!("error parsing invoice: {:?}", e),
+ }
+ }
+
+ #[test]
+ fn parses_invoice_with_fallback_address() {
+ let script = Script::new();
+ let pubkey = bitcoin::util::key::PublicKey::new(recipient_pubkey());
+ let x_only_pubkey = XOnlyPublicKey::from_keypair(&recipient_keys()).0;
+ let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey);
+
+ let offer = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap();
+ let invoice_request = offer
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap();
+ let mut unsigned_invoice = invoice_request
+ .respond_with(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();
+ // Non-standard addresses
+ fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 41] });
+ fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 1] });
+ fallbacks.push(FallbackAddress { version: 17, program: vec![0u8; 40] });
+ // Standard address
+ fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 33] });
+ fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 40] });
+
+ let invoice = unsigned_invoice.sign(recipient_sign).unwrap();
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ match Invoice::try_from(buffer) {
+ Ok(invoice) => {
+ assert_eq!(
+ invoice.fallbacks(),
+ vec![
+ Address::p2wsh(&script, Network::Bitcoin),
+ Address::p2wpkh(&pubkey, Network::Bitcoin).unwrap(),
+ Address::p2tr_tweaked(tweaked_pubkey, Network::Bitcoin),
+ Address {
+ payload: Payload::WitnessProgram {
+ version: WitnessVersion::V1,
+ program: vec![0u8; 33],
+ },
+ network: Network::Bitcoin,
+ },
+ Address {
+ payload: Payload::WitnessProgram {
+ version: WitnessVersion::V2,
+ program: vec![0u8; 40],
+ },
+ network: Network::Bitcoin,
+ },
+ ],
+ );
+ },
+ Err(e) => panic!("error parsing invoice: {:?}", e),
+ }
+ }
+
+ #[test]
+ fn parses_invoice_with_node_id() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ if let Err(e) = Invoice::try_from(buffer) {
+ panic!("error parsing invoice: {:?}", e);
+ }
+
+ let mut tlv_stream = invoice.as_tlv_stream();
+ tlv_stream.3.node_id = None;
+
+ match Invoice::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSigningPubkey));
+ },
+ }
+
+ let invalid_pubkey = payer_pubkey();
+ let mut tlv_stream = invoice.as_tlv_stream();
+ tlv_stream.3.node_id = Some(&invalid_pubkey);
+
+ match Invoice::try_from(tlv_stream.to_bytes()) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidSigningPubkey));
+ },
+ }
+ }
+
+ #[test]
+ fn fails_parsing_invoice_without_signature() {
+ let mut buffer = Vec::new();
+ OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .invoice
+ .write(&mut buffer).unwrap();
+
+ match Invoice::try_from(buffer) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSignature)),
+ }
+ }
+
+ #[test]
+ fn fails_parsing_invoice_with_invalid_signature() {
+ let mut invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+ let last_signature_byte = invoice.bytes.last_mut().unwrap();
+ *last_signature_byte = last_signature_byte.wrapping_add(1);
+
+ let mut buffer = Vec::new();
+ invoice.write(&mut buffer).unwrap();
+
+ match Invoice::try_from(buffer) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => {
+ assert_eq!(e, ParseError::InvalidSignature(secp256k1::Error::InvalidSignature));
+ },
+ }
+ }
+
+ #[test]
+ fn fails_parsing_invoice_with_extra_tlv_records() {
+ let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+ .amount_msats(1000)
+ .build().unwrap()
+ .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+ .build().unwrap()
+ .sign(payer_sign).unwrap()
+ .respond_with(payment_paths(), payment_hash(), now()).unwrap()
+ .build().unwrap()
+ .sign(recipient_sign).unwrap();
+
+ let mut encoded_invoice = Vec::new();
+ invoice.write(&mut encoded_invoice).unwrap();
+ BigSize(1002).write(&mut encoded_invoice).unwrap();
+ BigSize(32).write(&mut encoded_invoice).unwrap();
+ [42u8; 32].write(&mut encoded_invoice).unwrap();
+
+ match Invoice::try_from(encoded_invoice) {
+ Ok(_) => panic!("expected error"),
+ Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)),
+ }
+ }
+}
//!
//! An [`InvoiceRequest`] can be built from a parsed [`Offer`] as an "offer to be paid". It is
//! typically constructed by a customer and sent to the merchant who had published the corresponding
-//! offer. The recipient of the request responds with an `Invoice`.
+//! offer. The recipient of the request responds with an [`Invoice`].
//!
//! For an "offer for money" (e.g., refund, ATM withdrawal), where an offer doesn't exist as a
//! precursor, see [`Refund`].
//!
+//! [`Invoice`]: crate::offers::invoice::Invoice
//! [`Refund`]: crate::offers::refund::Refund
//!
//! ```ignore
use bitcoin::secp256k1::schnorr::Signature;
use core::convert::TryFrom;
use crate::io;
+use crate::ln::PaymentHash;
use crate::ln::features::InvoiceRequestFeatures;
use crate::ln::msgs::DecodeError;
+use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self};
use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
+use crate::onion_message::BlindedPath;
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
use crate::util::string::PrintableString;
}
}
-/// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`].
+/// An `InvoiceRequest` is a request for an [`Invoice`] formulated from an [`Offer`].
///
/// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request
/// specifies these such that its recipient can send an invoice for payment.
///
+/// [`Invoice`]: crate::offers::invoice::Invoice
/// [`Offer`]: crate::offers::offer::Offer
#[derive(Clone, Debug)]
pub struct InvoiceRequest {
pub(super) bytes: Vec<u8>,
- contents: InvoiceRequestContents,
+ pub(super) contents: InvoiceRequestContents,
signature: Signature,
}
-/// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`.
+/// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`].
+///
+/// [`Invoice`]: crate::offers::invoice::Invoice
#[derive(Clone, Debug)]
pub(super) struct InvoiceRequestContents {
payer: PayerContents,
- offer: OfferContents,
+ pub(super) offer: OfferContents,
chain: Option<ChainHash>,
amount_msats: Option<u64>,
features: InvoiceRequestFeatures,
self.signature
}
+ /// Creates an [`Invoice`] for the request 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.
+ ///
+ /// The caller is expected to remember the preimage of `payment_hash` in order to claim a payment
+ /// for the invoice.
+ ///
+ /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
+ /// must contain one or more elements.
+ ///
+ /// Errors if the request contains unknown required features.
+ ///
+ /// [`Duration`]: core::time::Duration
+ /// [`Invoice`]: crate::offers::invoice::Invoice
+ /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at
+ pub fn respond_with(
+ &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
+ #[cfg(any(test, not(feature = "std")))]
+ created_at: core::time::Duration
+ ) -> Result<InvoiceBuilder, SemanticError> {
+ if self.features().requires_unknown_bits() {
+ return Err(SemanticError::UnknownRequiredFeatures);
+ }
+
+ #[cfg(all(not(test), feature = "std"))]
+ 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_offer(self, payment_paths, created_at, payment_hash)
+ }
+
#[cfg(test)]
fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef {
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) =
}
impl InvoiceRequestContents {
- fn chain(&self) -> ChainHash {
+ pub(super) fn chain(&self) -> ChainHash {
self.chain.unwrap_or_else(|| self.offer.implied_chain())
}
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
use bitcoin::secp256k1::schnorr::Signature;
use crate::io;
-use crate::util::ser::{BigSize, Readable};
+use crate::util::ser::{BigSize, Readable, Writeable, Writer};
use crate::prelude::*;
/// Computes a merkle root hash for the given data, which must be a well-formed TLV stream
/// containing at least one TLV record.
fn root_hash(data: &[u8]) -> sha256::Hash {
- let mut tlv_stream = TlvStream::new(&data[..]).peekable();
let nonce_tag = tagged_hash_engine(sha256::Hash::from_engine({
+ let first_tlv_record = TlvStream::new(&data[..]).next().unwrap();
let mut engine = sha256::Hash::engine();
engine.input("LnNonce".as_bytes());
- engine.input(tlv_stream.peek().unwrap().record_bytes);
+ engine.input(first_tlv_record.record_bytes);
engine
}));
let leaf_tag = tagged_hash_engine(sha256::Hash::hash("LnLeaf".as_bytes()));
let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes()));
let mut leaves = Vec::new();
- for record in tlv_stream {
- if !SIGNATURE_TYPES.contains(&record.r#type) {
- leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record));
- leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes));
- }
+ let tlv_stream = TlvStream::new(&data[..]);
+ for record in tlv_stream.skip_signatures() {
+ leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record.record_bytes));
+ leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes));
}
// Calculate the merkle root hash in place.
data: io::Cursor::new(data),
}
}
+
+ fn skip_signatures(self) -> core::iter::Filter<TlvStream<'a>, fn(&TlvRecord) -> bool> {
+ self.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type))
+ }
}
/// A slice into a [`TlvStream`] for a record.
record_bytes: &'a [u8],
}
-impl AsRef<[u8]> for TlvRecord<'_> {
- fn as_ref(&self) -> &[u8] { &self.record_bytes }
-}
-
impl<'a> Iterator for TlvStream<'a> {
type Item = TlvRecord<'a>;
}
}
+/// Encoding for a pre-serialized TLV stream that excludes any signature TLV records.
+///
+/// Panics if the wrapped bytes are not a well-formed TLV stream.
+pub(super) struct WithoutSignatures<'a>(pub &'a Vec<u8>);
+
+impl<'a> Writeable for WithoutSignatures<'a> {
+ #[inline]
+ fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+ let tlv_stream = TlvStream::new(&self.0[..]);
+ for record in tlv_stream.skip_signatures() {
+ writer.write_all(record.record_bytes)?;
+ }
+ Ok(())
+ }
+}
+
#[cfg(test)]
mod tests {
+ use super::{TlvStream, WithoutSignatures};
+
use bitcoin::hashes::{Hash, sha256};
use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
use core::convert::Infallible;
use crate::offers::offer::{Amount, OfferBuilder};
use crate::offers::invoice_request::InvoiceRequest;
use crate::offers::parse::Bech32Encode;
+ use crate::util::ser::Writeable;
#[test]
fn calculates_merkle_root_hash() {
);
}
+ #[test]
+ fn skips_encoding_signature_tlv_records() {
+ let secp_ctx = Secp256k1::new();
+ let recipient_pubkey = {
+ let secret_key = SecretKey::from_slice(&[41; 32]).unwrap();
+ KeyPair::from_secret_key(&secp_ctx, &secret_key).public_key()
+ };
+ let payer_keys = {
+ let secret_key = SecretKey::from_slice(&[42; 32]).unwrap();
+ KeyPair::from_secret_key(&secp_ctx, &secret_key)
+ };
+
+ let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey)
+ .amount_msats(100)
+ .build_unchecked()
+ .request_invoice(vec![0; 8], payer_keys.public_key()).unwrap()
+ .build_unchecked()
+ .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &payer_keys)))
+ .unwrap();
+
+ let mut bytes_without_signature = Vec::new();
+ WithoutSignatures(&invoice_request.bytes).write(&mut bytes_without_signature).unwrap();
+
+ assert_ne!(bytes_without_signature, invoice_request.bytes);
+ assert_eq!(
+ TlvStream::new(&bytes_without_signature).count(),
+ TlvStream::new(&invoice_request.bytes).count() - 1,
+ );
+ }
+
impl AsRef<[u8]> for InvoiceRequest {
fn as_ref(&self) -> &[u8] {
&self.bytes
//!
//! Offers are a flexible protocol for Lightning payments.
+pub mod invoice;
pub mod invoice_request;
mod merkle;
pub mod offer;
/// An `Offer` is a potentially long-lived proposal for payment of a good or service.
///
/// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a
-/// customer may request an `Invoice` for a specific quantity and using an amount sufficient to
+/// customer may request an [`Invoice`] for a specific quantity and using an amount sufficient to
/// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`].
///
/// Offers may be denominated in currency other than bitcoin but are ultimately paid using the
/// Through the use of [`BlindedPath`]s, offers provide recipient privacy.
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+/// [`Invoice`]: crate::offers::invoice::Invoice
#[derive(Clone, Debug)]
pub struct Offer {
// The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
pub(super) contents: OfferContents,
}
-/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`.
+/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an [`Invoice`].
///
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+/// [`Invoice`]: crate::offers::invoice::Invoice
#[derive(Clone, Debug)]
pub(super) struct OfferContents {
chains: Option<Vec<ChainHash>>,
/// Whether the offer has expired.
#[cfg(feature = "std")]
pub fn is_expired(&self) -> bool {
- match self.absolute_expiry() {
- Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
- Ok(elapsed) => elapsed > seconds_from_epoch,
- Err(_) => false,
- },
- None => false,
- }
+ self.contents.is_expired()
}
/// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be
/// The public key used by the recipient to sign invoices.
pub fn signing_pubkey(&self) -> PublicKey {
- self.contents.signing_pubkey
+ self.contents.signing_pubkey()
}
/// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which
self.chains().contains(&chain)
}
+ #[cfg(feature = "std")]
+ pub(super) fn is_expired(&self) -> bool {
+ match self.absolute_expiry {
+ Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
+ Ok(elapsed) => elapsed > seconds_from_epoch,
+ Err(_) => false,
+ },
+ None => false,
+ }
+ }
+
pub fn amount(&self) -> Option<&Amount> {
self.amount.as_ref()
}
}
}
+ pub(super) fn signing_pubkey(&self) -> PublicKey {
+ self.signing_pubkey
+ }
+
pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
let (currency, amount) = match &self.amount {
None => (None, None),
MissingDescription,
/// A signing pubkey was not provided.
MissingSigningPubkey,
+ /// A signing pubkey was provided but a different one was expected.
+ InvalidSigningPubkey,
/// A signing pubkey was provided but was not expected.
UnexpectedSigningPubkey,
/// A quantity was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
MissingPayerId,
+ /// Blinded paths were expected but were missing.
+ MissingPaths,
+ /// The blinded payinfo given does not match the number of blinded path hops.
+ InvalidPayInfo,
+ /// An invoice creation time was expected but was missing.
+ MissingCreationTime,
+ /// An invoice payment hash was expected but was missing.
+ MissingPaymentHash,
/// A signature was expected but was missing.
MissingSignature,
}
//! Data structures and encoding for refunds.
//!
//! A [`Refund`] is an "offer for money" and is typically constructed by a merchant and presented
-//! directly to the customer. The recipient responds with an `Invoice` to be paid.
+//! directly to the customer. The recipient responds with an [`Invoice`] to be paid.
//!
//! This is an [`InvoiceRequest`] produced *not* in response to an [`Offer`].
//!
+//! [`Invoice`]: crate::offers::invoice::Invoice
//! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
//! [`Offer`]: crate::offers::offer::Offer
//!
use core::str::FromStr;
use core::time::Duration;
use crate::io;
+use crate::ln::PaymentHash;
use crate::ln::features::InvoiceRequestFeatures;
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
+use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
}
}
-/// A `Refund` is a request to send an `Invoice` without a preceding [`Offer`].
+/// A `Refund` is a request to send an [`Invoice`] without a preceding [`Offer`].
///
/// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to
/// recoup their funds. A refund may be used more generally as an "offer for money", such as with a
/// bitcoin ATM.
///
+/// [`Invoice`]: crate::offers::invoice::Invoice
/// [`Offer`]: crate::offers::offer::Offer
#[derive(Clone, Debug)]
pub struct Refund {
- bytes: Vec<u8>,
- contents: RefundContents,
+ pub(super) bytes: Vec<u8>,
+ pub(super) contents: RefundContents,
}
-/// The contents of a [`Refund`], which may be shared with an `Invoice`.
+/// The contents of a [`Refund`], which may be shared with an [`Invoice`].
+///
+/// [`Invoice`]: crate::offers::invoice::Invoice
#[derive(Clone, Debug)]
-struct RefundContents {
+pub(super) struct RefundContents {
payer: PayerContents,
// offer fields
metadata: Option<Vec<u8>>,
/// Whether the refund has expired.
#[cfg(feature = "std")]
pub fn is_expired(&self) -> bool {
- match self.absolute_expiry() {
- Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
- Ok(elapsed) => elapsed > seconds_from_epoch,
- Err(_) => false,
- },
- None => false,
- }
+ self.contents.is_expired()
}
/// The issuer of the refund, possibly beginning with `user@domain` or `domain`. Intended to be
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.
+ ///
+ /// 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.
+ ///
+ /// The caller is expected to remember the preimage of `payment_hash` in order to
+ /// claim a payment for the invoice.
+ ///
+ /// The `signing_pubkey` is required to sign the invoice since refunds are not in response to an
+ /// offer, which does have a `signing_pubkey`.
+ ///
+ /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
+ /// must contain one or more elements.
+ ///
+ /// Errors if the request contains unknown required features.
+ ///
+ /// [`Invoice`]: crate::offers::invoice::Invoice
+ /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at
+ pub fn respond_with(
+ &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
+ signing_pubkey: PublicKey,
+ #[cfg(any(test, not(feature = "std")))]
+ created_at: Duration
+ ) -> Result<InvoiceBuilder, SemanticError> {
+ if self.features().requires_unknown_bits() {
+ return Err(SemanticError::UnknownRequiredFeatures);
+ }
+
+ #[cfg(all(not(test), feature = "std"))]
+ 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)
+ }
+
#[cfg(test)]
fn as_tlv_stream(&self) -> RefundTlvStreamRef {
self.contents.as_tlv_stream()
}
impl RefundContents {
- fn chain(&self) -> ChainHash {
+ #[cfg(feature = "std")]
+ pub(super) fn is_expired(&self) -> bool {
+ match self.absolute_expiry {
+ Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
+ Ok(elapsed) => elapsed > seconds_from_epoch,
+ Err(_) => false,
+ },
+ None => false,
+ }
+ }
+
+ pub(super) fn chain(&self) -> ChainHash {
self.chain.unwrap_or_else(|| self.implied_chain())
}
use crate::chain;
use crate::ln::channelmanager;
use crate::ln::chan_utils::make_funding_redeemscript;
+ #[cfg(feature = "std")]
use crate::ln::features::InitFeatures;
use crate::routing::gossip::{P2PGossipSync, NetworkGraph, NetworkUpdate, NodeAlias, MAX_EXCESS_BYTES_FOR_RELAY, NodeId, RoutingFees, ChannelUpdateInfo, ChannelInfo, NodeAnnouncementInfo, NodeInfo};
use crate::ln::msgs::{RoutingMessageHandler, UnsignedNodeAnnouncement, NodeAnnouncement,
use crate::ln::channel::{ANCHOR_OUTPUT_VALUE_SATOSHI, MIN_CHAN_DUST_LIMIT_SATOSHIS};
use crate::ln::chan_utils::{HTLCOutputInCommitment, ChannelPublicKeys, HolderCommitmentTransaction, CommitmentTransaction, ChannelTransactionParameters, TrustedCommitmentTransaction, ClosingTransaction};
use crate::ln::{chan_utils, msgs, PaymentPreimage};
-use crate::chain::keysinterface::{Sign, InMemorySigner, BaseSign};
+use crate::chain::keysinterface::{WriteableEcdsaChannelSigner, InMemorySigner, ChannelSigner, EcdsaChannelSigner};
use crate::prelude::*;
use core::cmp;
}
}
-impl BaseSign for EnforcingSigner {
+impl ChannelSigner for EnforcingSigner {
fn get_per_commitment_point(&self, idx: u64, secp_ctx: &Secp256k1<secp256k1::All>) -> PublicKey {
self.inner.get_per_commitment_point(idx, secp_ctx)
}
}
fn pubkeys(&self) -> &ChannelPublicKeys { self.inner.pubkeys() }
+
fn channel_keys_id(&self) -> [u8; 32] { self.inner.channel_keys_id() }
+ fn provide_channel_parameters(&mut self, channel_parameters: &ChannelTransactionParameters) {
+ self.inner.provide_channel_parameters(channel_parameters)
+ }
+}
+
+impl EcdsaChannelSigner for EnforcingSigner {
fn sign_counterparty_commitment(&self, commitment_tx: &CommitmentTransaction, preimages: Vec<PaymentPreimage>, secp_ctx: &Secp256k1<secp256k1::All>) -> Result<(Signature, Vec<Signature>), ()> {
self.verify_counterparty_commitment_tx(commitment_tx, secp_ctx);
) -> Result<Signature, ()> {
self.inner.sign_channel_announcement_with_funding_key(msg, secp_ctx)
}
-
- fn provide_channel_parameters(&mut self, channel_parameters: &ChannelTransactionParameters) {
- self.inner.provide_channel_parameters(channel_parameters)
- }
}
-impl Sign for EnforcingSigner {}
+impl WriteableEcdsaChannelSigner for EnforcingSigner {}
impl Writeable for EnforcingSigner {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), Error> {
use crate::chain;
use crate::chain::chaininterface::{BroadcasterInterface, FeeEstimator};
use crate::chain::chainmonitor::{Persist, MonitorUpdateId};
-use crate::chain::keysinterface::{EntropySource, NodeSigner, Sign, SignerProvider};
+use crate::chain::keysinterface::{EntropySource, NodeSigner, WriteableEcdsaChannelSigner, SignerProvider};
use crate::chain::transaction::OutPoint;
use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate};
use crate::ln::channelmanager::ChannelManager;
}
}
-impl<ChannelSigner: Sign, K: KVStorePersister> Persist<ChannelSigner> for K {
+impl<ChannelSigner: WriteableEcdsaChannelSigner, K: KVStorePersister> Persist<ChannelSigner> for K {
// TODO: We really need a way for the persister to inform the user that its time to crash/shut
// down once these start returning failure.
// A PermanentFailure implies we should probably just shut down the node since we're
fn from(v: &'a Vec<T>) -> Self { Self(v) }
}
+#[derive(Debug)]
+pub(crate) struct Iterable<'a, I: Iterator<Item = &'a T> + Clone, T: 'a>(pub I);
+
+impl<'a, I: Iterator<Item = &'a T> + Clone, T: 'a + Writeable> Writeable for Iterable<'a, I, T> {
+ #[inline]
+ fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+ for ref v in self.0.clone() {
+ v.write(writer)?;
+ }
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+impl<'a, I: Iterator<Item = &'a T> + Clone, T: 'a + PartialEq> PartialEq for Iterable<'a, I, T> {
+ fn eq(&self, other: &Self) -> bool {
+ self.0.clone().collect::<Vec<_>>() == other.0.clone().collect::<Vec<_>>()
+ }
+}
+
macro_rules! impl_for_map {
($ty: ident, $keybound: ident, $constr: expr) => {
impl<K, V> Writeable for $ty<K, V>
}
}
+impl<A: Readable, B: Readable, C: Readable, D: Readable> Readable for (A, B, C, D) {
+ fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
+ let a: A = Readable::read(r)?;
+ let b: B = Readable::read(r)?;
+ let c: C = Readable::read(r)?;
+ let d: D = Readable::read(r)?;
+ Ok((a, b, c, d))
+ }
+}
+impl<A: Writeable, B: Writeable, C: Writeable, D: Writeable> Writeable for (A, B, C, D) {
+ fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
+ self.0.write(w)?;
+ self.1.write(w)?;
+ self.2.write(w)?;
+ self.3.write(w)
+ }
+}
+
impl Writeable for () {
fn write<W: Writer>(&self, _: &mut W) -> Result<(), io::Error> {
Ok(())
($reader: expr, $field: ident, (option: $trait: ident $(, $read_arg: expr)?)) => {{
$field = Some($trait::read(&mut $reader $(, $read_arg)*)?);
}};
+ ($reader: expr, $field: ident, (option, encoding: ($fieldty: ty, $encoding: ident, $encoder:ty))) => {{
+ $crate::_decode_tlv!($reader, $field, (option, encoding: ($fieldty, $encoding)));
+ }};
($reader: expr, $field: ident, (option, encoding: ($fieldty: ty, $encoding: ident))) => {{
$field = {
let field: $encoding<$fieldty> = ser::Readable::read(&mut $reader)?;
)*
}
- #[derive(Debug, PartialEq)]
+ #[cfg_attr(test, derive(PartialEq))]
+ #[derive(Debug)]
pub(super) struct $nameref<'a> {
$(
pub(super) $field: Option<tlv_record_ref_type!($fieldty)>,
macro_rules! tlv_record_type {
(($type:ty, $wrapper:ident)) => { $type };
+ (($type:ty, $wrapper:ident, $encoder:ty)) => { $type };
($type:ty) => { $type };
}
((u32, $wrapper: ident)) => { u32 };
((u64, $wrapper: ident)) => { u64 };
(($type:ty, $wrapper:ident)) => { &'a $type };
+ (($type:ty, $wrapper:ident, $encoder:ty)) => { $encoder };
($type:ty) => { &'a $type };
}
self.update_rets.lock().unwrap().push_back(next_ret);
}
}
-impl<Signer: keysinterface::Sign> chainmonitor::Persist<Signer> for TestPersister {
+impl<Signer: keysinterface::WriteableEcdsaChannelSigner> chainmonitor::Persist<Signer> for TestPersister {
fn persist_new_channel(&self, _funding_txo: OutPoint, _data: &channelmonitor::ChannelMonitor<Signer>, _id: MonitorUpdateId) -> chain::ChannelMonitorUpdateStatus {
if let Some(update_ret) = self.update_rets.lock().unwrap().pop_front() {
return update_ret
--- /dev/null
+## API Updates
+- The functions `inbound_payment::{create, create_from_hash}` and
+ `channelmanager::{create_inbound_payment, create_inbound_payment_for_hash}` now accept a
+ `min_final_cltv_expiry_delta` argument. This encodes the `min_final_cltv_expiry_delta` in the
+ payment secret metadata bytes to be validated on payment receipt.
+
+## Backwards Compatibility
+- If `min_final_cltv_expiry_delta` set for any of `inbound_payment::{create, create_from_hash}` or
+ `channelmanager::{create_inbound_payment, create_inbound_payment_for_hash}` then the payment will
+ not be receivable on versions of LDK prior to 0.0.114.
+