diff --git a/lightning-liquidity/Cargo.toml b/lightning-liquidity/Cargo.toml index 1cc0d988544..3c158b7dc1b 100644 --- a/lightning-liquidity/Cargo.toml +++ b/lightning-liquidity/Cargo.toml @@ -14,8 +14,9 @@ categories = ["cryptography::cryptocurrencies"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["std"] +default = ["std", "time"] std = ["lightning/std"] +time = [] backtrace = ["dep:backtrace"] [dependencies] diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs index c916cf13e09..c480bb9dcb8 100644 --- a/lightning-liquidity/src/lsps0/ser.rs +++ b/lightning-liquidity/src/lsps0/ser.rs @@ -8,6 +8,7 @@ use alloc::string::String; use core::fmt::{self, Display}; use core::str::FromStr; +use core::time::Duration; use crate::lsps0::msgs::{ LSPS0ListProtocolsRequest, LSPS0Message, LSPS0Request, LSPS0Response, @@ -29,8 +30,7 @@ use lightning::util::ser::{LengthLimitedRead, LengthReadable, WithoutLength}; use bitcoin::secp256k1::PublicKey; -#[cfg(feature = "std")] -use std::time::{SystemTime, UNIX_EPOCH}; +use crate::sync::Arc; use serde::de::{self, MapAccess, Visitor}; use serde::ser::SerializeStruct; @@ -204,12 +204,8 @@ impl LSPSDateTime { } /// Returns if the given time is in the past. - #[cfg(feature = "std")] - pub fn is_past(&self) -> bool { - let now_seconds_since_epoch = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock to be ahead of the unix epoch") - .as_secs(); + pub fn is_past(&self, time_provider: Arc) -> bool { + let now_seconds_since_epoch = time_provider.duration_since_epoch().as_secs(); let datetime_seconds_since_epoch = self.0.timestamp().try_into().expect("expiration to be ahead of unix epoch"); now_seconds_since_epoch > datetime_seconds_since_epoch @@ -784,3 +780,24 @@ pub(crate) mod u32_fee_rate { Ok(FeeRate::from_sat_per_kwu(fee_rate_sat_kwu as u64)) } } + +/// Trait defining a time provider +/// +/// This trait is used to provide the current time service operations and to convert between timestamps and durations. +pub trait TimeProvider { + /// Get the current time as a duration since the Unix epoch. + fn duration_since_epoch(&self) -> Duration; +} + +/// Default time provider using the system clock. +#[derive(Clone, Debug)] +#[cfg(feature = "time")] +pub struct DefaultTimeProvider; + +#[cfg(feature = "time")] +impl TimeProvider for DefaultTimeProvider { + fn duration_since_epoch(&self) -> Duration { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before Unix epoch") + } +} diff --git a/lightning-liquidity/src/lsps2/msgs.rs b/lightning-liquidity/src/lsps2/msgs.rs index 84875d4ab7c..65d4dcc9f89 100644 --- a/lightning-liquidity/src/lsps2/msgs.rs +++ b/lightning-liquidity/src/lsps2/msgs.rs @@ -218,12 +218,32 @@ mod tests { use super::*; use crate::alloc::string::ToString; + use crate::lsps0::ser::TimeProvider; use crate::lsps2::utils::is_valid_opening_fee_params; - + use crate::sync::Arc; + use core::cell::RefCell; use core::str::FromStr; + use core::time::Duration; + + struct MockTimeProvider { + current_time: RefCell, + } + + impl MockTimeProvider { + fn new(seconds_since_epoch: u64) -> Self { + Self { current_time: RefCell::new(Duration::from_secs(seconds_since_epoch)) } + } + } + + impl TimeProvider for MockTimeProvider { + fn duration_since_epoch(&self) -> Duration { + *self.current_time.borrow() + } + } #[test] fn into_opening_fee_params_produces_valid_promise() { + let time_provider = Arc::new(MockTimeProvider::new(1000)); let min_fee_msat = 100; let proportional = 21; let valid_until = LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(); @@ -254,11 +274,12 @@ mod tests { assert_eq!(opening_fee_params.min_payment_size_msat, min_payment_size_msat); assert_eq!(opening_fee_params.max_payment_size_msat, max_payment_size_msat); - assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret, time_provider)); } #[test] fn changing_single_field_produced_invalid_params() { + let time_provider = Arc::new(MockTimeProvider::new(1000)); let min_fee_msat = 100; let proportional = 21; let valid_until = LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(); @@ -281,11 +302,12 @@ mod tests { let mut opening_fee_params = raw.into_opening_fee_params(&promise_secret); opening_fee_params.min_fee_msat = min_fee_msat + 1; - assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret, time_provider)); } #[test] fn wrong_secret_produced_invalid_params() { + let time_provider = Arc::new(MockTimeProvider::new(1000)); let min_fee_msat = 100; let proportional = 21; let valid_until = LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(); @@ -308,13 +330,13 @@ mod tests { let other_secret = [2u8; 32]; let opening_fee_params = raw.into_opening_fee_params(&promise_secret); - assert!(!is_valid_opening_fee_params(&opening_fee_params, &other_secret)); + assert!(!is_valid_opening_fee_params(&opening_fee_params, &other_secret, time_provider)); } #[test] - #[cfg(feature = "std")] - // TODO: We need to find a way to check expiry times in no-std builds. fn expired_params_produces_invalid_params() { + // 70 years since epoch + let time_provider = Arc::new(MockTimeProvider::new(70 * 365 * 24 * 60 * 60)); // 1970 + 70 years let min_fee_msat = 100; let proportional = 21; let valid_until = LSPSDateTime::from_str("2023-05-20T08:30:45Z").unwrap(); @@ -336,7 +358,7 @@ mod tests { let promise_secret = [1u8; 32]; let opening_fee_params = raw.into_opening_fee_params(&promise_secret); - assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret, time_provider)); } #[test] diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 9b511ad8d44..d9a7504becf 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -16,8 +16,12 @@ use core::ops::Deref; use core::sync::atomic::{AtomicUsize, Ordering}; use crate::events::EventQueue; + +#[cfg(feature = "time")] +use crate::lsps0::ser::DefaultTimeProvider; + use crate::lsps0::ser::{ - LSPSMessage, LSPSProtocolMessageHandler, LSPSRequestId, LSPSResponseError, + LSPSMessage, LSPSProtocolMessageHandler, LSPSRequestId, LSPSResponseError, TimeProvider, JSONRPC_INTERNAL_ERROR_ERROR_CODE, JSONRPC_INTERNAL_ERROR_ERROR_MESSAGE, LSPS0_CLIENT_REJECTED_ERROR_CODE, }; @@ -400,18 +404,20 @@ struct OutboundJITChannel { user_channel_id: u128, opening_fee_params: LSPS2OpeningFeeParams, payment_size_msat: Option, + time_provider: Arc, } impl OutboundJITChannel { fn new( payment_size_msat: Option, opening_fee_params: LSPS2OpeningFeeParams, - user_channel_id: u128, + user_channel_id: u128, time_provider: Arc, ) -> Self { Self { user_channel_id, state: OutboundJITChannelState::new(), opening_fee_params, payment_size_msat, + time_provider, } } @@ -451,7 +457,8 @@ impl OutboundJITChannel { fn is_prunable(&self) -> bool { // We deem an OutboundJITChannel prunable if our offer expired and we haven't intercepted // any HTLCs initiating the flow yet. - let is_expired = is_expired_opening_fee_params(&self.opening_fee_params); + let is_expired = + is_expired_opening_fee_params(&self.opening_fee_params, self.time_provider.clone()); self.is_pending_initial_payment() && is_expired } } @@ -481,13 +488,16 @@ impl PeerState { self.outbound_channels_by_intercept_scid.insert(intercept_scid, channel); } - fn prune_expired_request_state(&mut self) { + fn prune_expired_request_state(&mut self, time_provider: Arc) { self.pending_requests.retain(|_, entry| { match entry { LSPS2Request::GetInfo(_) => false, LSPS2Request::Buy(request) => { // Prune any expired buy requests. - !is_expired_opening_fee_params(&request.opening_fee_params) + !is_expired_opening_fee_params( + &request.opening_fee_params, + time_provider.clone(), + ) }, } }); @@ -566,16 +576,32 @@ where peer_by_channel_id: RwLock>, total_pending_requests: AtomicUsize, config: LSPS2ServiceConfig, + time_provider: Arc, } impl LSPS2ServiceHandler where CM::Target: AChannelManager, { + #[cfg(feature = "time")] /// Constructs a `LSPS2ServiceHandler`. pub(crate) fn new( pending_messages: Arc, pending_events: Arc, channel_manager: CM, config: LSPS2ServiceConfig, + ) -> Self { + let time_provider = Arc::new(DefaultTimeProvider); + Self::new_with_custom_time_provider( + pending_messages, + pending_events, + channel_manager, + config, + time_provider, + ) + } + + pub(crate) fn new_with_custom_time_provider( + pending_messages: Arc, pending_events: Arc, channel_manager: CM, + config: LSPS2ServiceConfig, time_provider: Arc, ) -> Self { Self { pending_messages, @@ -586,6 +612,7 @@ where total_pending_requests: AtomicUsize::new(0), channel_manager, config, + time_provider, } } @@ -737,6 +764,7 @@ where buy_request.payment_size_msat, buy_request.opening_fee_params, user_channel_id, + self.time_provider.clone(), ); peer_state_lock @@ -1192,7 +1220,11 @@ where } // TODO: if payment_size_msat is specified, make sure our node has sufficient incoming liquidity from public network to receive it. - if !is_valid_opening_fee_params(¶ms.opening_fee_params, &self.config.promise_secret) { + if !is_valid_opening_fee_params( + ¶ms.opening_fee_params, + &self.config.promise_secret, + self.time_provider.clone(), + ) { let response = LSPS2Response::BuyError(LSPSResponseError { code: LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, message: "valid_until is already past OR the promise did not match the provided parameters".to_string(), @@ -1334,7 +1366,7 @@ where let is_prunable = if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) { let mut peer_state_lock = inner_state_lock.lock().unwrap(); - peer_state_lock.prune_expired_request_state(); + peer_state_lock.prune_expired_request_state(self.time_provider.clone()); peer_state_lock.is_prunable() } else { return; @@ -1349,7 +1381,7 @@ where let mut outer_state_lock = self.per_peer_state.write().unwrap(); outer_state_lock.retain(|_, inner_state_lock| { let mut peer_state_lock = inner_state_lock.lock().unwrap(); - peer_state_lock.prune_expired_request_state(); + peer_state_lock.prune_expired_request_state(self.time_provider.clone()); peer_state_lock.is_prunable() == false }); } diff --git a/lightning-liquidity/src/lsps2/utils.rs b/lightning-liquidity/src/lsps2/utils.rs index 76ceeb8f60b..d1a2526295d 100644 --- a/lightning-liquidity/src/lsps2/utils.rs +++ b/lightning-liquidity/src/lsps2/utils.rs @@ -1,5 +1,8 @@ //! Utilities for implementing the bLIP-52 / LSPS2 standard. +use crate::sync::Arc; + +use crate::lsps0::ser::TimeProvider; use crate::lsps2::msgs::LSPS2OpeningFeeParams; use crate::utils; @@ -10,8 +13,9 @@ use bitcoin::hashes::{Hash, HashEngine}; /// Determines if the given parameters are valid given the secret used to generate the promise. pub fn is_valid_opening_fee_params( fee_params: &LSPS2OpeningFeeParams, promise_secret: &[u8; 32], + time_provider: Arc, ) -> bool { - if is_expired_opening_fee_params(fee_params) { + if is_expired_opening_fee_params(fee_params, time_provider) { return false; } let mut hmac = HmacEngine::::new(promise_secret); @@ -28,17 +32,10 @@ pub fn is_valid_opening_fee_params( } /// Determines if the given parameters are expired, or still valid. -#[cfg_attr(not(feature = "std"), allow(unused_variables))] -pub fn is_expired_opening_fee_params(fee_params: &LSPS2OpeningFeeParams) -> bool { - #[cfg(feature = "std")] - { - fee_params.valid_until.is_past() - } - #[cfg(not(feature = "std"))] - { - // TODO: We need to find a way to check expiry times in no-std builds. - false - } +pub fn is_expired_opening_fee_params( + fee_params: &LSPS2OpeningFeeParams, time_provider: Arc, +) -> bool { + fee_params.valid_until.is_past(time_provider) } /// Computes the opening fee given a payment size and the fee parameters. diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index eec9a71d632..a428e7fc038 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -7,8 +7,8 @@ use crate::lsps0::client::LSPS0ClientHandler; use crate::lsps0::msgs::LSPS0Message; use crate::lsps0::ser::{ LSPSMessage, LSPSMethod, LSPSProtocolMessageHandler, LSPSRequestId, LSPSResponseError, - RawLSPSMessage, JSONRPC_INVALID_MESSAGE_ERROR_CODE, JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE, - LSPS_MESSAGE_TYPE_ID, + RawLSPSMessage, TimeProvider, JSONRPC_INVALID_MESSAGE_ERROR_CODE, + JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE, LSPS_MESSAGE_TYPE_ID, }; use crate::lsps0::service::LSPS0ServiceHandler; use crate::message_queue::{MessageQueue, ProcessMessagesCallback}; @@ -128,7 +128,7 @@ where pub fn new( entropy_source: ES, channel_manager: CM, chain_source: Option, chain_params: Option, service_config: Option, - client_config: Option, + client_config: Option, time_provider: Option>, ) -> Self where { let pending_messages = Arc::new(MessageQueue::new()); @@ -154,12 +154,32 @@ where { { supported_protocols.push(number); } - LSPS2ServiceHandler::new( - Arc::clone(&pending_messages), - Arc::clone(&pending_events), - channel_manager.clone(), - config.clone(), - ) + if time_provider.is_some() { + // Always use custom time provider if provided + LSPS2ServiceHandler::new_with_custom_time_provider( + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + channel_manager.clone(), + config.clone(), + time_provider.unwrap(), + ) + } else { + #[cfg(feature = "time")] + { + // Use default new if time feature is enabled and no custom provider + LSPS2ServiceHandler::new( + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + channel_manager.clone(), + config.clone(), + ) + } + #[cfg(not(feature = "time"))] + { + // Panic if no time provider and time feature is not enabled + panic!("A custom time_provider must be provided if the 'time' feature is not enabled."); + } + } }) }); diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index f114f7b9c89..1ff7a1e2dd5 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -460,6 +460,7 @@ pub(crate) fn create_liquidity_node( Some(chain_params), service_config, client_config, + None, )); let msg_handler = MessageHandler { chan_handler: Arc::new(test_utils::TestChannelMessageHandler::new( diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 5a3f88dacac..1ecfd6b5118 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -5,7 +5,7 @@ mod common; use common::{create_service_and_client_nodes, get_lsps_message, Node}; use lightning_liquidity::events::LiquidityEvent; -use lightning_liquidity::lsps0::ser::LSPSDateTime; +use lightning_liquidity::lsps0::ser::{DefaultTimeProvider, LSPSDateTime, TimeProvider}; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; use lightning_liquidity::lsps2::msgs::LSPS2RawOpeningFeeParams; @@ -26,6 +26,7 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1}; use bitcoin::Network; use std::str::FromStr; +use std::sync::Arc; use std::time::Duration; fn create_jit_invoice( @@ -148,7 +149,7 @@ fn invoice_generation_flow() { .liquidity_manager .handle_custom_message(get_info_response, service_node_id) .unwrap(); - + let time_provider: Arc<(dyn TimeProvider + 'static)> = Arc::new(DefaultTimeProvider); let opening_params_event = client_node.liquidity_manager.next_event().unwrap(); let opening_fee_params = match opening_params_event { LiquidityEvent::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { @@ -159,7 +160,11 @@ fn invoice_generation_flow() { assert_eq!(request_id, get_info_request_id); assert_eq!(counterparty_node_id, service_node_id); let opening_fee_params = opening_fee_params_menu.first().unwrap().clone(); - assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + assert!(is_valid_opening_fee_params( + &opening_fee_params, + &promise_secret, + time_provider + )); opening_fee_params }, _ => panic!("Unexpected event"),