lightning_liquidity/lsps2/
utils.rs

1//! Utilities for implementing the LSPS2 standard.
2
3use crate::lsps2::msgs::OpeningFeeParams;
4use crate::utils;
5
6use bitcoin::hashes::hmac::{Hmac, HmacEngine};
7use bitcoin::hashes::sha256::Hash as Sha256;
8use bitcoin::hashes::{Hash, HashEngine};
9
10#[cfg(feature = "std")]
11use std::time::{SystemTime, UNIX_EPOCH};
12
13/// Determines if the given parameters are valid given the secret used to generate the promise.
14pub fn is_valid_opening_fee_params(
15	fee_params: &OpeningFeeParams, promise_secret: &[u8; 32],
16) -> bool {
17	if is_expired_opening_fee_params(fee_params) {
18		return false;
19	}
20	let mut hmac = HmacEngine::<Sha256>::new(promise_secret);
21	hmac.input(&fee_params.min_fee_msat.to_be_bytes());
22	hmac.input(&fee_params.proportional.to_be_bytes());
23	hmac.input(fee_params.valid_until.to_rfc3339().as_bytes());
24	hmac.input(&fee_params.min_lifetime.to_be_bytes());
25	hmac.input(&fee_params.max_client_to_self_delay.to_be_bytes());
26	hmac.input(&fee_params.min_payment_size_msat.to_be_bytes());
27	hmac.input(&fee_params.max_payment_size_msat.to_be_bytes());
28	let promise_bytes = Hmac::from_engine(hmac).to_byte_array();
29	let promise = utils::hex_str(&promise_bytes[..]);
30	promise == fee_params.promise
31}
32
33/// Determines if the given parameters are expired, or still valid.
34#[cfg_attr(not(feature = "std"), allow(unused_variables))]
35pub fn is_expired_opening_fee_params(fee_params: &OpeningFeeParams) -> bool {
36	#[cfg(feature = "std")]
37	{
38		let seconds_since_epoch = SystemTime::now()
39			.duration_since(UNIX_EPOCH)
40			.expect("system clock to be ahead of the unix epoch")
41			.as_secs();
42		let valid_until_seconds_since_epoch = fee_params
43			.valid_until
44			.timestamp()
45			.try_into()
46			.expect("expiration to be ahead of unix epoch");
47		seconds_since_epoch > valid_until_seconds_since_epoch
48	}
49	#[cfg(not(feature = "std"))]
50	{
51		// TODO: We need to find a way to check expiry times in no-std builds.
52		false
53	}
54}
55
56/// Computes the opening fee given a payment size and the fee parameters.
57///
58/// Returns [`Option::None`] when the computation overflows.
59///
60/// See the [`specification`](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2#computing-the-opening_fee) for more details.
61pub fn compute_opening_fee(
62	payment_size_msat: u64, opening_fee_min_fee_msat: u64, opening_fee_proportional: u64,
63) -> Option<u64> {
64	payment_size_msat
65		.checked_mul(opening_fee_proportional)
66		.and_then(|f| f.checked_add(999999))
67		.and_then(|f| f.checked_div(1000000))
68		.map(|f| core::cmp::max(f, opening_fee_min_fee_msat))
69}
70
71#[cfg(test)]
72mod tests {
73	use super::*;
74	use proptest::prelude::*;
75
76	const MAX_VALUE_MSAT: u64 = 21_000_000_0000_0000_000;
77
78	fn arb_opening_fee_params() -> impl Strategy<Value = (u64, u64, u64)> {
79		(0u64..MAX_VALUE_MSAT, 0u64..MAX_VALUE_MSAT, 0u64..MAX_VALUE_MSAT)
80	}
81
82	proptest! {
83		#[test]
84		fn test_compute_opening_fee((payment_size_msat, opening_fee_min_fee_msat, opening_fee_proportional) in arb_opening_fee_params()) {
85			if let Some(res) = compute_opening_fee(payment_size_msat, opening_fee_min_fee_msat, opening_fee_proportional) {
86				assert!(res >= opening_fee_min_fee_msat);
87				assert_eq!(res as f32, (payment_size_msat as f32 * opening_fee_proportional as f32));
88			} else {
89				// Check we actually overflowed.
90				let max_value = u64::MAX as u128;
91				assert!((payment_size_msat as u128 * opening_fee_proportional as u128) > max_value);
92			}
93		}
94	}
95}