Skip to content

Require min_final_cltv_expiry in invoices #898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 73 additions & 30 deletions lightning-invoice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ const SYSTEM_TIME_MAX_UNIX_TIMESTAMP: u64 = std::i32::MAX as u64;
/// it should be rather low as long as we still have to support 32bit time representations
const MAX_EXPIRY_TIME: u64 = 60 * 60 * 24 * 356;

/// Default expiry time as defined by [BOLT 11].
///
/// [BOLT 11]: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
const DEFAULT_EXPIRY_TIME: u64 = 3600;

/// Default minimum final CLTV expiry as defined by [BOLT 11].
///
/// [BOLT 11]: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
const DEFAULT_MIN_FINAL_CLTV_EXPIRY: u64 = 18;

/// This function is used as a static assert for the size of `SystemTime`. If the crate fails to
/// compile due to it this indicates that your system uses unexpected bounds for `SystemTime`. You
/// can remove this functions and run the test `test_system_time_bounds_assumptions`. In any case,
Expand Down Expand Up @@ -136,6 +146,7 @@ pub fn check_platform() {
/// .description("Coins pls!".into())
/// .payment_hash(payment_hash)
/// .current_timestamp()
/// .min_final_cltv_expiry(144)
/// .build_signed(|hash| {
/// Secp256k1::new().sign_recoverable(hash, &private_key)
/// })
Expand All @@ -154,7 +165,7 @@ pub fn check_platform() {
///
/// (C-not exported) as we likely need to manually select one set of boolean type parameters.
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool> {
pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> {
currency: Currency,
amount: Option<u64>,
si_prefix: Option<SiPrefix>,
Expand All @@ -165,6 +176,7 @@ pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool> {
phantom_d: std::marker::PhantomData<D>,
phantom_h: std::marker::PhantomData<H>,
phantom_t: std::marker::PhantomData<T>,
phantom_c: std::marker::PhantomData<C>,
}

/// Represents a syntactically and semantically correct lightning BOLT11 invoice.
Expand Down Expand Up @@ -416,7 +428,7 @@ pub mod constants {
pub const TAG_FEATURES: u8 = 5;
}

impl InvoiceBuilder<tb::False, tb::False, tb::False> {
impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False> {
/// Construct new, empty `InvoiceBuilder`. All necessary fields have to be filled first before
/// `InvoiceBuilder::build(self)` becomes available.
pub fn new(currrency: Currency) -> Self {
Expand All @@ -431,14 +443,15 @@ impl InvoiceBuilder<tb::False, tb::False, tb::False> {
phantom_d: std::marker::PhantomData,
phantom_h: std::marker::PhantomData,
phantom_t: std::marker::PhantomData,
phantom_c: std::marker::PhantomData,
}
}
}

impl<D: tb::Bool, H: tb::Bool, T: tb::Bool> InvoiceBuilder<D, H, T> {
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T, C> {
/// Helper function to set the completeness flags.
fn set_flags<DN: tb::Bool, HN: tb::Bool, TN: tb::Bool>(self) -> InvoiceBuilder<DN, HN, TN> {
InvoiceBuilder::<DN, HN, TN> {
fn set_flags<DN: tb::Bool, HN: tb::Bool, TN: tb::Bool, CN: tb::Bool>(self) -> InvoiceBuilder<DN, HN, TN, CN> {
InvoiceBuilder::<DN, HN, TN, CN> {
currency: self.currency,
amount: self.amount,
si_prefix: self.si_prefix,
Expand All @@ -449,6 +462,7 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool> InvoiceBuilder<D, H, T> {
phantom_d: std::marker::PhantomData,
phantom_h: std::marker::PhantomData,
phantom_t: std::marker::PhantomData,
phantom_c: std::marker::PhantomData,
}
}

Expand Down Expand Up @@ -484,12 +498,6 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool> InvoiceBuilder<D, H, T> {
self
}

/// Sets `min_final_cltv_expiry`.
pub fn min_final_cltv_expiry(mut self, min_final_cltv_expiry: u64) -> Self {
self.tagged_fields.push(TaggedField::MinFinalCltvExpiry(MinFinalCltvExpiry(min_final_cltv_expiry)));
self
}

/// Adds a fallback address.
pub fn fallback(mut self, fallback: Fallback) -> Self {
self.tagged_fields.push(TaggedField::Fallback(fallback));
Expand All @@ -513,7 +521,7 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool> InvoiceBuilder<D, H, T> {
}
}

impl<D: tb::Bool, H: tb::Bool> InvoiceBuilder<D, H, tb::True> {
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, tb::True, C> {
/// Builds a `RawInvoice` if no `CreationError` occurred while construction any of the fields.
pub fn build_raw(self) -> Result<RawInvoice, CreationError> {

Expand Down Expand Up @@ -546,9 +554,9 @@ impl<D: tb::Bool, H: tb::Bool> InvoiceBuilder<D, H, tb::True> {
}
}

impl<H: tb::Bool, T: tb::Bool> InvoiceBuilder<tb::False, H, T> {
impl<H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<tb::False, H, T, C> {
/// Set the description. This function is only available if no description (hash) was set.
pub fn description(mut self, description: String) -> InvoiceBuilder<tb::True, H, T> {
pub fn description(mut self, description: String) -> InvoiceBuilder<tb::True, H, T, C> {
match Description::new(description) {
Ok(d) => self.tagged_fields.push(TaggedField::Description(d)),
Err(e) => self.error = Some(e),
Expand All @@ -557,23 +565,23 @@ impl<H: tb::Bool, T: tb::Bool> InvoiceBuilder<tb::False, H, T> {
}

/// Set the description hash. This function is only available if no description (hash) was set.
pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder<tb::True, H, T> {
pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder<tb::True, H, T, C> {
self.tagged_fields.push(TaggedField::DescriptionHash(Sha256(description_hash)));
self.set_flags()
}
}

impl<D: tb::Bool, T: tb::Bool> InvoiceBuilder<D, tb::False, T> {
impl<D: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, tb::False, T, C> {
/// Set the payment hash. This function is only available if no payment hash was set.
pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder<D, tb::True, T> {
pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder<D, tb::True, T, C> {
self.tagged_fields.push(TaggedField::PaymentHash(Sha256(hash)));
self.set_flags()
}
}

impl<D: tb::Bool, H: tb::Bool> InvoiceBuilder<D, H, tb::False> {
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, tb::False, C> {
/// Sets the timestamp.
pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder<D, H, tb::True> {
pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder<D, H, tb::True, C> {
match PositiveTimestamp::from_system_time(time) {
Ok(t) => self.timestamp = Some(t),
Err(e) => self.error = Some(e),
Expand All @@ -583,14 +591,22 @@ impl<D: tb::Bool, H: tb::Bool> InvoiceBuilder<D, H, tb::False> {
}

/// Sets the timestamp to the current UNIX timestamp.
pub fn current_timestamp(mut self) -> InvoiceBuilder<D, H, tb::True> {
pub fn current_timestamp(mut self) -> InvoiceBuilder<D, H, tb::True, C> {
let now = PositiveTimestamp::from_system_time(SystemTime::now());
self.timestamp = Some(now.expect("for the foreseeable future this shouldn't happen"));
self.set_flags()
}
}

impl InvoiceBuilder<tb::True, tb::True, tb::True> {
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool> InvoiceBuilder<D, H, T, tb::False> {
/// Sets `min_final_cltv_expiry`.
pub fn min_final_cltv_expiry(mut self, min_final_cltv_expiry: u64) -> InvoiceBuilder<D, H, T, tb::True> {
self.tagged_fields.push(TaggedField::MinFinalCltvExpiry(MinFinalCltvExpiry(min_final_cltv_expiry)));
self.set_flags()
}
}

impl InvoiceBuilder<tb::True, tb::True, tb::True, tb::True> {
/// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail
/// and MUST produce a recoverable signature valid for the given hash and if applicable also for
/// the included payee public key.
Expand Down Expand Up @@ -1041,16 +1057,19 @@ impl Invoice {
self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0
}

/// Returns the invoice's expiry time if present
/// Returns the invoice's expiry time, if present, otherwise [`DEFAULT_EXPIRY_TIME`].
pub fn expiry_time(&self) -> Duration {
self.signed_invoice.expiry_time()
.map(|x| x.0)
.unwrap_or(Duration::from_secs(3600))
.unwrap_or(Duration::from_secs(DEFAULT_EXPIRY_TIME))
}

/// Returns the invoice's `min_cltv_expiry` time if present
pub fn min_final_cltv_expiry(&self) -> Option<u64> {
self.signed_invoice.min_final_cltv_expiry().map(|x| x.0)
/// 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()
.map(|x| x.0)
.unwrap_or(DEFAULT_MIN_FINAL_CLTV_EXPIRY)
}

/// Returns a list of all fallback addresses
Expand Down Expand Up @@ -1476,7 +1495,8 @@ mod test {

let builder = InvoiceBuilder::new(Currency::Bitcoin)
.payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap())
.current_timestamp();
.current_timestamp()
.min_final_cltv_expiry(144);

let too_long_string = String::from_iter(
(0..1024).map(|_| '?')
Expand Down Expand Up @@ -1593,7 +1613,6 @@ mod test {
.payee_pub_key(public_key.clone())
.expiry_time(Duration::from_secs(54321))
.min_final_cltv_expiry(144)
.min_final_cltv_expiry(143)
.fallback(Fallback::PubKeyHash([0;20]))
.route(route_1.clone())
.route(route_2.clone())
Expand All @@ -1605,7 +1624,7 @@ mod test {
}).unwrap();

assert!(invoice.check_signature().is_ok());
assert_eq!(invoice.tagged_fields().count(), 9);
assert_eq!(invoice.tagged_fields().count(), 8);

assert_eq!(invoice.amount_pico_btc(), Some(123));
assert_eq!(invoice.currency(), Currency::BitcoinTestnet);
Expand All @@ -1615,7 +1634,7 @@ mod test {
);
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(), Some(144));
assert_eq!(invoice.min_final_cltv_expiry(), 144);
assert_eq!(invoice.fallbacks(), vec![&Fallback::PubKeyHash([0;20])]);
assert_eq!(invoice.routes(), vec![&RouteHint(route_1), &RouteHint(route_2)]);
assert_eq!(
Expand All @@ -1627,4 +1646,28 @@ mod test {
let raw_invoice = builder.build_raw().unwrap();
assert_eq!(raw_invoice, *invoice.into_signed_raw().raw_invoice())
}

#[test]
fn test_default_values() {
use ::*;
use secp256k1::Secp256k1;
use secp256k1::key::SecretKey;

let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin)
.description("Test".into())
.payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap())
.current_timestamp()
.build_raw()
.unwrap()
.sign::<_, ()>(|hash| {
let privkey = SecretKey::from_slice(&[41; 32]).unwrap();
let secp_ctx = Secp256k1::new();
Ok(secp_ctx.sign_recoverable(hash, &privkey))
})
.unwrap();
let invoice = Invoice::from_signed(signed_invoice).unwrap();

assert_eq!(invoice.min_final_cltv_expiry(), DEFAULT_MIN_FINAL_CLTV_EXPIRY);
assert_eq!(invoice.expiry_time(), Duration::from_secs(DEFAULT_EXPIRY_TIME));
}
}
9 changes: 5 additions & 4 deletions lightning-invoice/tests/ser_de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,14 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
.amount_pico_btc(20000000000)
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
.payment_secret(PaymentSecret([42; 32]))
.build_signed(|msg_hash| {
.build_raw()
.unwrap()
.sign::<_, ()>(|msg_hash| {
let privkey = SecretKey::from_slice(&[41; 32]).unwrap();
let secp_ctx = Secp256k1::new();
secp_ctx.sign_recoverable(msg_hash, &privkey)
Ok(secp_ctx.sign_recoverable(msg_hash, &privkey))
})
.unwrap()
.into_signed_raw(),
.unwrap(),
None
)
]
Expand Down