diff --git a/.clippy.toml b/.clippy.toml index be64409c8d..3e20ed160f 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1 +1,35 @@ -msrv = "1.63.0" \ No newline at end of file +msrv = "1.63.0" + +# Keep library clock reads behind src/time.rs so they work on wasm32. Tokio's runtime timers +# are separate. +disallowed-methods = [ + { path = "std::time::SystemTime::now", reason = "use crate::time instead" }, + { path = "std::time::SystemTime::elapsed", reason = "use crate::time instead" }, + { path = "std::time::Instant::now", reason = "use crate::time::Instant instead" }, + { path = "std::time::Instant::elapsed", reason = "use crate::time::Instant instead" }, + { path = "tokio::time::Instant::now", reason = "use crate::time::Instant for clock reads; runtime timers remain separate" }, + { path = "tokio::time::Instant::elapsed", reason = "use crate::time::Instant for elapsed measurements" }, + { path = "chrono::Utc::now", reason = "use crate::time::now_utc instead", allow-invalid = true }, + { path = "chrono::Local::now", reason = "use crate::time::now_utc instead", allow-invalid = true }, + # Dependency APIs that read the platform clock internally. Use their explicit-time variants. + { path = "bdk_wallet::Wallet::start_full_scan", reason = "use start_full_scan_at with crate::time" }, + { path = "bdk_wallet::Wallet::start_sync_with_revealed_spks", reason = "use start_sync_with_revealed_spks_at with crate::time" }, + { path = "lightning_liquidity::LiquidityManager::new", reason = "use new_with_custom_time_provider with crate::time::LdkTimeProvider" }, + { path = "lightning_invoice::InvoiceBuilder::current_timestamp", reason = "use duration_since_epoch with crate::time" }, + { path = "lightning_invoice::Bolt11Invoice::is_expired", reason = "use would_expire with crate::time" }, + { path = "lightning_invoice::Bolt11Invoice::duration_until_expiry", reason = "use expiration_remaining_from_epoch with crate::time" }, + { path = "lightning::offers::offer::Offer::is_expired", reason = "use is_expired_no_std with crate::time" }, + { path = "lightning::offers::refund::Refund::is_expired", reason = "use is_expired_no_std with crate::time" }, + { path = "lightning::offers::invoice::Bolt12Invoice::is_expired", reason = "use is_expired_no_std with crate::time" }, + { path = "lightning::offers::static_invoice::StaticInvoice::is_expired", reason = "use is_expired_no_std with crate::time" }, + { path = "lightning::offers::invoice_request::InvoiceRequest::respond_with", reason = "use respond_with_no_std with crate::time" }, + { path = "lightning::offers::invoice_request::VerifiedInvoiceRequest::respond_with", reason = "use respond_with_no_std with crate::time" }, + { path = "lightning::offers::invoice_request::VerifiedInvoiceRequest::respond_using_derived_keys", reason = "use respond_using_derived_keys_no_std with crate::time" }, + { path = "lightning::offers::refund::Refund::respond_with", reason = "use respond_with_no_std with crate::time" }, + { path = "lightning::offers::refund::Refund::respond_using_derived_keys", reason = "use respond_using_derived_keys_no_std with crate::time" }, + { path = "lightning_rapid_gossip_sync::RapidGossipSync::update_network_graph", reason = "use update_network_graph_no_std with Some(crate::time::unix_time_secs().expect(...)) to retain freshness validation" }, + { path = "lightning_rapid_gossip_sync::RapidGossipSync::sync_network_graph_with_file_path", reason = "read the snapshot and use update_network_graph_no_std with current time" }, + { path = "lightning::routing::gossip::NetworkGraph::remove_stale_channels_and_tracking", reason = "use remove_stale_channels_and_tracking_with_time with crate::time" }, + { path = "bdk_chain::bdk_core::spk_client::SyncRequest::builder", reason = "use builder_at with crate::time" }, + { path = "bdk_chain::bdk_core::spk_client::FullScanRequest::builder", reason = "use builder_at with crate::time" }, +] diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 119ee51734..f475d99b83 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -109,6 +109,33 @@ jobs: cargo clippy --lib --verbose --color always -- -A warnings -D clippy::unwrap_used -A clippy::tabs_in_doc_comments cargo clippy --lib --no-default-features --features uniffi-default --verbose --color always -- -A warnings -D clippy::unwrap_used -A clippy::tabs_in_doc_comments + clock-boundary: + name: Clock boundary + timeout-minutes: 120 + runs-on: self-hosted + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Install Rust stable toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable + - name: Add clippy component + run: rustup component add clippy + # Clock reads must go through `src/time.rs` so they work on wasm32. See `.clippy.toml`. + - name: Ban direct clock reads in library code + run: | + cargo clippy --lib --verbose --color always -- -A warnings -D clippy::disallowed_methods + cargo clippy --lib --no-default-features --features uniffi-default --verbose --color always -- -A warnings -D clippy::disallowed_methods + cargo clippy --lib --no-default-features --features chain-esplora,storage-sqlite --verbose --color always -- -A warnings -D clippy::disallowed_methods + # LDK's `std` and `time` features read the system clock internally. + - name: Check LDK clock features are disabled without ldk-std + run: | + features=$(cargo tree --target wasm32-unknown-unknown --no-default-features -e normal --prefix none --format '{p} {f}') + if grep -E '^lightning' <<< "$features" | grep -E '\b(std|time)\b'; then + echo "LDK std or time features are enabled without ldk-std" + exit 1 + fi + doc: name: Documentation timeout-minutes: 120 diff --git a/Cargo.toml b/Cargo.toml index 9f8a729655..94b6e40fcd 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,8 @@ panic = 'abort' # Abort on panic [features] default = [ + "ldk-std", + "net-tokio", "chain-esplora", "chain-electrum", "chain-bitcoind", @@ -37,23 +39,38 @@ default = [ "storage-vss", "unified-payments", ] +# LDK's `std` feature reads the system clock internally. Disable it for wasm32, where clock reads +# must go through `crate::time` and LDK's explicit-time APIs instead. Features whose LDK crates +# require `std` enable this. +ldk-std = [ + "lightning/std", + "lightning-invoice/std", + "lightning-liquidity/std", + "lightning-liquidity/time", + "lightning-background-processor/std", + "lightning-rapid-gossip-sync/std", +] +# Tokio TCP transport for peer connections. Without it, connecting to peers fails and listening +# addresses are rejected. +net-tokio = ["dep:lightning-net-tokio"] chain-esplora = [ + "ldk-std", "dep:bdk_esplora", "dep:esplora-client", "dep:ldk-esplora-client", "dep:lightning-transaction-sync", "lightning-transaction-sync/esplora-async-https", - "lightning-transaction-sync/time", ] chain-electrum = [ + "ldk-std", "dep:bdk_electrum", "dep:electrum-client", "dep:lightning-transaction-sync", "lightning-transaction-sync/electrum-rustls-ring", ] -chain-bitcoind = ["dep:lightning-block-sync"] +chain-bitcoind = ["ldk-std", "dep:lightning-block-sync"] storage-sqlite = ["dep:rusqlite"] -storage-filesystem = ["dep:lightning-persister"] +storage-filesystem = ["ldk-std", "dep:lightning-persister"] storage-vss = ["dep:vss-client", "dep:prost"] storage-postgres = ["dep:tokio-postgres", "dep:native-tls", "dep:postgres-native-tls"] storage-postgres-vendored-tls = ["storage-postgres", "native-tls/vendored"] @@ -65,6 +82,7 @@ unified-payments = [ uniffi = ["dep:uniffi"] uniffi-default = [ "uniffi", + "net-tokio", "chain-esplora", "chain-electrum", "storage-sqlite", @@ -86,16 +104,17 @@ uniffi-default = [ #lightning-macros = { version = "0.2.0" } #lightning-dns-resolver = { version = "0.3.0" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", features = ["std"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", default-features = false, features = ["grind_signatures"] } lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", default-features = false } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", optional = true } lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", features = ["tokio"], optional = true } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457" } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", default-features = false } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", default-features = false } lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", features = ["rest-client", "rpc-client", "tokio"], optional = true } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", optional = true } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", features = ["std"] } +# Measure sync durations in the node's time module, not the dependency's `time` feature. +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", default-features = false, optional = true } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", default-features = false } lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457" } lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", optional = true } @@ -113,7 +132,7 @@ bip21 = { version = "0.5", features = ["std"], default-features = false, optiona base64 = { version = "0.22.1", default-features = false, features = ["std"] } getrandom = { version = "0.3", default-features = false } -chrono = { version = "0.4", default-features = false, features = ["clock"] } +chrono = { version = "0.4", default-features = false, features = ["alloc"] } tokio = { version = "1.39", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros", "net" ] } tokio-util = { version = "0.7.10", default-features = false, features = ["rt"] } esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"], optional = true } diff --git a/src/builder.rs b/src/builder.rs index 1158044e47..195187b635 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -15,7 +15,6 @@ use std::net::ToSocketAddrs; #[cfg(feature = "storage-filesystem")] use std::path::PathBuf; use std::sync::{Arc, Mutex, Once, RwLock}; -use std::time::SystemTime; use bdk_wallet::template::Bip84; use bdk_wallet::{KeychainKind, Wallet as BdkWallet}; @@ -102,6 +101,7 @@ use crate::probing::{ RandomWalkStrategy, }; use crate::runtime::{Runtime, RuntimeSpawner}; +use crate::time; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper, @@ -1542,6 +1542,12 @@ fn build_with_store_internal( } } + #[cfg(not(feature = "net-tokio"))] + if config.listening_addresses.is_some() { + log_error!(logger, "Listening addresses were set but no network transport is enabled."); + return Err(BuildError::InvalidListeningAddresses); + } + let tx_broadcaster = Arc::new(TransactionBroadcaster::new(Arc::clone(&logger))); let fee_estimator = Arc::new(OnchainFeeEstimator::new()); @@ -1938,8 +1944,8 @@ fn build_with_store_internal( tx_broadcaster.set_wallet(Arc::downgrade(&wallet)); // Initialize the KeysManager - let cur_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map_err(|e| { - log_error!(logger, "Failed to get current time: {}", e); + let cur_time = time::duration_since_epoch().ok_or_else(|| { + log_error!(logger, "Failed to get current time: system time is before the Unix epoch"); BuildError::InvalidSystemTime })?; @@ -2361,8 +2367,8 @@ fn build_with_store_internal( }, }; - let cur_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map_err(|e| { - log_error!(logger, "Failed to get current time: {}", e); + let cur_time = time::duration_since_epoch().ok_or_else(|| { + log_error!(logger, "Failed to get current time: system time is before the Unix epoch"); BuildError::InvalidSystemTime })?; diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 9ed38c2128..1b919282e9 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -10,7 +10,7 @@ use std::fmt; use std::future::Future; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use base64::prelude::BASE64_STANDARD; use base64::Engine; @@ -42,6 +42,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::time::{self, Instant}; use crate::tx_broadcaster::SortedTransactions; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; @@ -236,7 +237,7 @@ impl BitcoindChainSource { )); } - let now = SystemTime::now(); + let now = Instant::now(); match synchronize_listeners( self.api_client.as_ref(), self.config.network, @@ -255,15 +256,14 @@ impl BitcoindChainSource { ); *self.spv_client.lock().await = Some(spv_client); { - let elapsed_ms = now.elapsed().map(|d| d.as_millis()).unwrap_or(0); + let elapsed_ms = now.elapsed().as_millis(); log_info!( self.logger, "Finished synchronizing listeners in {}ms", elapsed_ms, ); *self.latest_chain_tip.write().expect("lock") = Some(chain_tip); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics( &self.node_metrics, &*self.kv_store, @@ -479,10 +479,10 @@ impl BitcoindChainSource { } let spv_client = spv_client_lock.as_mut().expect("initialized above"); - let now = SystemTime::now(); + let now = Instant::now(); match spv_client.poll_best_tip().await { Ok((ChainTip::Better(tip), true)) => { - let elapsed_ms = now.elapsed().map(|d| d.as_millis()).unwrap_or(0); + let elapsed_ms = now.elapsed().as_millis(); log_trace!(self.logger, "Finished polling best tip in {}ms", elapsed_ms); *self.latest_chain_tip.write().expect("lock") = Some(tip); }, @@ -496,7 +496,7 @@ impl BitcoindChainSource { let cur_height = channel_manager.current_best_block().height; - let now = SystemTime::now(); + let now = Instant::now(); let bdk_unconfirmed_txids = onchain_wallet.get_unconfirmed_txids(); match self .api_client @@ -504,7 +504,7 @@ impl BitcoindChainSource { .await { Ok((unconfirmed_txs, evicted_txids)) => { - let elapsed_ms = now.elapsed().map(|d| d.as_millis()).unwrap_or(0); + let elapsed_ms = now.elapsed().as_millis(); log_trace!( self.logger, "Finished polling mempool of size {} and {} evicted transactions in {}ms", @@ -525,8 +525,7 @@ impl BitcoindChainSource { }, } - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics(&self.node_metrics, &*self.kv_store, &*self.logger, |m| { m.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; m.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; @@ -654,8 +653,7 @@ impl BitcoindChainSource { ); } - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics(&self.node_metrics, &*self.kv_store, &*self.logger, |m| { m.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt }) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 86025998e8..38e34c5493 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -8,7 +8,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock, Weak}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use bdk_chain::bdk_core::spk_client::{ FullScanRequest as BdkFullScanRequest, FullScanResponse as BdkFullScanResponse, @@ -38,6 +38,7 @@ use crate::fee_estimator::{ use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_trace, log_warn, LdkLogger, Logger}; use crate::runtime::Runtime; +use crate::time::{self, Instant}; use crate::tx_broadcaster::SortedTransactions; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::PersistedNodeMetrics; @@ -202,8 +203,7 @@ impl ElectrumChainSource { if incremental_sync { "Incremental sync" } else { "Sync" }, now.elapsed().as_millis() ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics( &self.node_metrics, &*self.kv_store, @@ -275,8 +275,7 @@ impl ElectrumChainSource { let res = electrum_client.sync_confirmables(vec![confirmable]).await; if let Ok(_) = res { - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics( &self.node_metrics, &*self.kv_store, @@ -310,8 +309,7 @@ impl ElectrumChainSource { now.elapsed().as_millis() ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics(&self.node_metrics, &*self.kv_store, &*self.logger, |m| { m.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt }) diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 1c13f141fb..1b627cebe1 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use bdk_esplora::EsploraAsyncExt; use bitcoin::transaction::Version; @@ -29,6 +29,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_trace, log_warn, LdkLogger, Logger}; +use crate::time::{self, Instant}; use crate::tx_broadcaster::SortedTransactions; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; @@ -164,10 +165,7 @@ impl EsploraChainSource { if incremental_sync { "Incremental sync" } else { "Sync" }, now.elapsed().as_millis() ); - let unix_time_secs_opt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics( &self.node_metrics, &*self.kv_store, @@ -325,8 +323,7 @@ impl EsploraChainSource { now.elapsed().as_millis() ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics( &self.node_metrics, &*self.kv_store, @@ -411,8 +408,7 @@ impl EsploraChainSource { "Fee rate cache update finished in {}ms.", now.elapsed().as_millis() ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics(&self.node_metrics, &*self.kv_store, &*self.logger, |m| { m.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt }) diff --git a/src/connection.rs b/src/connection.rs index ccb6f98464..86d13bab27 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -8,13 +8,16 @@ use std::collections::hash_map::{self, HashMap}; use std::ops::Deref; use std::sync::{Arc, Mutex}; +#[cfg(feature = "net-tokio")] use std::time::Duration; use bitcoin::secp256k1::PublicKey; use lightning::ln::msgs::SocketAddress; use crate::config::TorConfig; -use crate::logger::{log_debug, log_error, log_info, LdkLogger}; +#[cfg(feature = "net-tokio")] +use crate::logger::{log_debug, log_info}; +use crate::logger::{log_error, LdkLogger}; use crate::types::{KeysManager, PeerManager}; use crate::Error; @@ -57,7 +60,9 @@ where { pending_connections: PendingConnections, peer_manager: Arc, + #[cfg_attr(not(feature = "net-tokio"), allow(dead_code))] tor_proxy_config: Option, + #[cfg_attr(not(feature = "net-tokio"), allow(dead_code))] keys_manager: Arc, logger: L, } @@ -112,6 +117,20 @@ where res } + #[cfg(not(feature = "net-tokio"))] + async fn do_connect_peer_internal( + &self, node_id: PublicKey, addr: SocketAddress, + ) -> Result<(), Error> { + log_error!( + self.logger, + "Failed to connect to peer {}@{}: no network transport is enabled.", + node_id, + addr + ); + Err(Error::ConnectionFailed) + } + + #[cfg(feature = "net-tokio")] async fn do_connect_peer_internal( &self, node_id: PublicKey, addr: SocketAddress, ) -> Result<(), Error> { @@ -242,6 +261,7 @@ where } } + #[cfg(feature = "net-tokio")] async fn await_connection( &self, connection_future: F, node_id: PublicKey, addr: SocketAddress, ) -> Result<(), Error> @@ -309,6 +329,24 @@ where } } +/// Placeholder socket type used when no network transport is enabled. +/// +/// It has no values, so no peer connection can ever be created with it. +#[cfg(not(feature = "net-tokio"))] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum SocketDescriptor {} + +#[cfg(not(feature = "net-tokio"))] +impl lightning::ln::peer_handler::SocketDescriptor for SocketDescriptor { + fn send_data(&mut self, _data: &[u8], _continue_read: bool) -> usize { + match *self {} + } + + fn disconnect_socket(&mut self) { + match *self {} + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ffi/types.rs b/src/ffi/types.rs index e6ef62c539..991d713658 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -60,6 +60,8 @@ use vss_client::headers::{ VssHeaderProviderError as VssClientHeaderProviderError, }; +use crate::time; + /// Errors around providing headers for each VSS request. #[derive(Debug, uniffi::Error)] pub enum VssHeaderProviderError { @@ -271,7 +273,7 @@ impl Offer { /// Whether the offer has expired. pub fn is_expired(&self) -> bool { - self.inner.is_expired() + time::duration_since_epoch().map_or(false, |now| self.inner.is_expired_no_std(now)) } /// A complete description of the purpose of the payment. @@ -498,7 +500,7 @@ impl Refund { /// Whether the refund has expired. pub fn is_expired(&self) -> bool { - self.inner.is_expired() + time::duration_since_epoch().map_or(false, |now| self.inner.is_expired_no_std(now)) } /// The issuer of the refund, possibly beginning with `user@domain` or `domain`. @@ -656,7 +658,7 @@ impl Bolt12Invoice { /// Whether the invoice has expired. pub fn is_expired(&self) -> bool { - self.inner.is_expired() + time::duration_since_epoch().map_or(false, |now| self.inner.is_expired_no_std(now)) } /// A complete description of the purpose of the originating offer or refund. @@ -1478,12 +1480,15 @@ impl Bolt11Invoice { /// Returns the seconds remaining until the invoice expires. pub fn seconds_until_expiry(&self) -> u64 { - self.inner.duration_until_expiry().as_secs() + time::duration_since_epoch() + .map(|now| self.inner.expiration_remaining_from_epoch(now)) + .unwrap_or_default() + .as_secs() } /// Returns whether the invoice has expired. pub fn is_expired(&self) -> bool { - self.inner.is_expired() + time::duration_since_epoch().map_or(false, |now| self.inner.would_expire(now)) } /// Returns whether the expiry time would pass at the given point in time. diff --git a/src/gossip.rs b/src/gossip.rs index 41206dfa86..6283687cac 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -17,6 +17,7 @@ use crate::logger::{log_error, log_trace, LdkLogger, Logger}; use crate::runtime::Runtime; #[cfg(feature = "chain-bitcoind")] use crate::runtime::RuntimeSpawner; +use crate::time; use crate::types::{GossipSync, Graph, P2PGossipSync, RapidGossipSync, UtxoLookup}; use crate::Error; @@ -89,8 +90,11 @@ impl GossipSource { match response.status_code { 200 => { - let new_latest_sync_timestamp = - gossip_sync.update_network_graph(response.as_bytes()).map_err(|e| { + let now = time::unix_time_secs() + .expect("current time should not be earlier than the Unix epoch"); + let new_latest_sync_timestamp = gossip_sync + .update_network_graph_no_std(response.as_bytes(), Some(now)) + .map_err(|e| { log_trace!( logger, "Failed to update network graph with RGS data: {:?}", diff --git a/src/lib.rs b/src/lib.rs index fbb65b0005..1c07a50099 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,6 +112,7 @@ mod peer_store; pub mod probing; mod runtime; mod scoring; +pub mod time; mod tx_broadcaster; mod types; mod util; @@ -119,7 +120,7 @@ mod wallet; use std::default::Default; use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::Duration; #[cfg(cycle_tests)] use std::{any::Any, sync::Weak}; @@ -203,6 +204,7 @@ use crate::config::{LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY use crate::ffi::{maybe_deref, maybe_wrap}; use crate::liquidity::Liquidity; use crate::scoring::setup_background_pathfinding_scores_sync; +use crate::time::Instant; use crate::wallet::FundingAmount; #[cfg(not(feature = "uniffi"))] @@ -436,6 +438,7 @@ impl Node { ); } + #[cfg(feature = "net-tokio")] if let Some(listening_addresses) = &self.config.listening_addresses { // Setup networking let peer_manager_connection_handler = Arc::clone(&self.peer_manager); @@ -595,8 +598,8 @@ impl Node { let skip_broadcast = match bcast_node_metrics.read().expect("lock").latest_node_announcement_broadcast_timestamp { Some(latest_bcast_time_secs) => { // Skip if the time hasn't elapsed yet. - let next_bcast_unix_time = SystemTime::UNIX_EPOCH + Duration::from_secs(latest_bcast_time_secs) + NODE_ANN_BCAST_INTERVAL; - next_bcast_unix_time.elapsed().is_err() + let next_bcast_time = Duration::from_secs(latest_bcast_time_secs) + NODE_ANN_BCAST_INTERVAL; + time::duration_since_epoch().map_or(true, |now| now < next_bcast_time) } None => { // Don't skip if we haven't broadcasted before. @@ -630,8 +633,7 @@ impl Node { if let Some(node_alias) = node_alias.as_ref() { bcast_pm.broadcast_node_announcement([0; 3], node_alias.0, addresses); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let unix_time_secs_opt = time::unix_time_secs(); update_and_persist_node_metrics( &bcast_node_metrics, &*bcast_store, @@ -760,8 +762,7 @@ impl Node { true, || { Some( - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) + time::duration_since_epoch() .expect("current time should not be earlier than the Unix epoch"), ) }, diff --git a/src/liquidity/client/lsps2.rs b/src/liquidity/client/lsps2.rs index 6435a71e5b..61c4b05294 100644 --- a/src/liquidity/client/lsps2.rs +++ b/src/liquidity/client/lsps2.rs @@ -32,6 +32,7 @@ use crate::liquidity::{ use crate::logger::{log_debug, log_error, log_info, LdkLogger}; use crate::payment::store::LSPS2Parameters; use crate::payment::PaymentMetadata; +use crate::time; use crate::types::{ChannelManager, KeysManager, LiquidityManager}; use crate::{Config, Error}; @@ -394,11 +395,18 @@ where }]); let currency = self.config.network.into(); + let timestamp = time::duration_since_epoch().ok_or_else(|| { + log_error!( + self.logger, + "Failed to create invoice: system time is before the Unix epoch" + ); + Error::InvoiceCreationFailed + })?; let mut invoice_builder = InvoiceBuilder::new(currency) .invoice_description(description.clone()) .payment_hash(payment_hash) .payment_secret(payment_secret) - .current_timestamp() + .duration_since_epoch(timestamp) .min_final_cltv_expiry_delta(min_final_cltv_expiry_delta.into()) .expiry_time(Duration::from_secs(expiry_secs.into())) .private_route(route_hint); diff --git a/src/liquidity/mod.rs b/src/liquidity/mod.rs index ffc1f878bc..410ecd3566 100644 --- a/src/liquidity/mod.rs +++ b/src/liquidity/mod.rs @@ -36,6 +36,7 @@ use crate::liquidity::client::lsps2::LSPS2Client; use crate::liquidity::service::lsps2::{LSPS2Service, LSPS2ServiceLiquiditySource}; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::runtime::Runtime; +use crate::time::LdkTimeProvider; use crate::types::{Broadcaster, ChannelManager, DynStore, KeysManager, LiquidityManager, Wallet}; use crate::{Config, Error}; @@ -309,14 +310,15 @@ where }); let liquidity_manager = Arc::new( - LiquidityManager::new( + LiquidityManager::new_with_custom_time_provider( Arc::clone(&self.keys_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.channel_manager), - Arc::clone(&self.kv_store), Arc::clone(&self.tx_broadcaster), + Arc::clone(&self.kv_store), liquidity_service_config, liquidity_client_config, + Arc::new(LdkTimeProvider), ) .await .map_err(|_| BuildError::ReadFailed)?, diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index 946511c5d9..0f68c06a16 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -11,7 +11,6 @@ use std::time::Duration; use bitcoin::secp256k1::PublicKey; use bitcoin::Transaction; -use chrono::Utc; use lightning::events::HTLCHandlingFailureType; use lightning::ln::channelmanager::InterceptId; use lightning::ln::types::ChannelId; @@ -23,6 +22,7 @@ use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceCo use lightning_types::payment::PaymentHash; use crate::logger::{log_error, LdkLogger}; +use crate::time; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{total_anchor_channels_reserve_sats, Config}; @@ -296,7 +296,7 @@ where } } - let valid_until = LSPSDateTime(Utc::now() + LSPS2_GETINFO_REQUEST_EXPIRY); + let valid_until = LSPSDateTime(time::now_utc() + LSPS2_GETINFO_REQUEST_EXPIRY); let opening_fee_params = LSPS2RawOpeningFeeParams { min_fee_msat: service_config.min_channel_opening_fee_msat, proportional: service_config.channel_opening_fee_ppm, diff --git a/src/logger.rs b/src/logger.rs index 857b33e3a9..060ee59577 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -16,7 +16,6 @@ use std::path::Path; use std::sync::Arc; use bitcoin::secp256k1::PublicKey; -use chrono::Utc; use lightning::ln::types::ChannelId; use lightning::types::payment::PaymentHash; pub use lightning::util::logger::Level as LogLevel; @@ -25,6 +24,7 @@ pub(crate) use lightning::{log_bytes, log_debug, log_error, log_info, log_trace, use log::{Level as LogFacadeLevel, Record as LogFacadeRecord}; use crate::io::utils::create_dir_all_private; +use crate::time; fn open_log_file(file_path: &str) -> std::io::Result { let mut options = fs::OpenOptions::new(); @@ -212,7 +212,7 @@ impl LogWriter for Writer { let log = format!( "{} {:<5} [{}:{}] {}{}\n", - Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"), + time::now_utc().format("%Y-%m-%d %H:%M:%S%.3f"), record.level.to_string(), record.module_path, record.line, diff --git a/src/payment/asynchronous/rate_limiter.rs b/src/payment/asynchronous/rate_limiter.rs index bf12508927..cac9016dc2 100644 --- a/src/payment/asynchronous/rate_limiter.rs +++ b/src/payment/asynchronous/rate_limiter.rs @@ -7,8 +7,9 @@ //! [`RateLimiter`] to control the rate of requests from users. +use crate::time::Instant; use std::collections::HashMap; -use std::time::{Duration, Instant}; +use std::time::Duration; /// Implements a leaky-bucket style rate limiter parameterized by the max capacity of the bucket, the refill interval, /// and the max idle duration. diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 0e994d67ac..fc84f92d89 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -11,7 +11,7 @@ use std::num::NonZeroU64; use std::sync::{Arc, RwLock}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use lightning::blinded_path::message::BlindedMessagePath; use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId}; @@ -34,6 +34,7 @@ use crate::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; use crate::runtime::Runtime; +use crate::time; use crate::types::{ChannelManager, KeysManager, PaymentStore}; #[cfg(not(feature = "uniffi"))] @@ -243,9 +244,9 @@ impl Bolt12Payment { })?; if let Some(expiry_secs) = expiry_secs { - let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) - .duration_since(UNIX_EPOCH) - .expect("system time must be after Unix epoch"); + let absolute_expiry = time::duration_since_epoch() + .expect("system time must be after Unix epoch") + + Duration::from_secs(expiry_secs as u64); offer_builder = offer_builder.absolute_expiry(absolute_expiry); } @@ -524,9 +525,9 @@ impl Bolt12Payment { })?; if let Some(expiry_secs) = expiry_secs { - let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) - .duration_since(UNIX_EPOCH) - .expect("system time must be after Unix epoch"); + let absolute_expiry = time::duration_since_epoch() + .expect("system time must be after Unix epoch") + + Duration::from_secs(expiry_secs as u64); offer_builder = offer_builder.absolute_expiry(absolute_expiry); } @@ -571,9 +572,9 @@ impl Bolt12Payment { ) -> Result { let payment_id = PaymentId(self.keys_manager.get_secure_random_bytes()); - let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) - .duration_since(UNIX_EPOCH) - .expect("system time must be after Unix epoch"); + let absolute_expiry = time::duration_since_epoch() + .expect("system time must be after Unix epoch") + + Duration::from_secs(expiry_secs as u64); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); let route_parameters = route_parameters.or(self.config.route_parameters).unwrap_or_default(); diff --git a/src/payment/forwarding_store.rs b/src/payment/forwarding_store.rs index 645406297e..03635fded3 100644 --- a/src/payment/forwarding_store.rs +++ b/src/payment/forwarding_store.rs @@ -7,7 +7,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::PublicKey; @@ -34,6 +34,7 @@ use crate::io::{ FORWARDED_PAYMENT_REPLAY_MARKER_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::logger::{log_debug, log_error, Logger}; +use crate::time; use crate::types::{ ChannelForwardingStatsStore, ChannelPairForwardingStatsStore, DynStore, ForwardedPaymentStore, }; @@ -237,10 +238,8 @@ impl ForwardingStore { } let details_id = matches!(self.tracking_mode, ForwardedPaymentTrackingMode::Detailed) .then_some(forward_id); - let forwarded_at_timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("current time should not be earlier than the Unix epoch") - .as_secs(); + let forwarded_at_timestamp = + time::unix_time_secs().expect("current time should not be earlier than the Unix epoch"); let inbound_amount_msat = forward.outbound_amount_forwarded_msat.saturating_add(fee_earned_msat); @@ -583,12 +582,13 @@ pub(crate) async fn run_forwarded_payment_aggregation( } let period = Duration::from_secs(FORWARDED_PAYMENT_AGGREGATION_BUCKET_SIZE_SECS); - let now = - SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_secs(); + let now = time::unix_time_secs().unwrap_or(0); let secs_until_next_bucket = seconds_until_next_forwarding_aggregation( now, FORWARDED_PAYMENT_AGGREGATION_BUCKET_SIZE_SECS, ); + // Tokio deadlines must use the runtime clock, whose origin may differ from our provider. + #[allow(clippy::disallowed_methods)] let first_tick = tokio::time::Instant::now() + Duration::from_secs(secs_until_next_bucket); let mut interval = tokio::time::interval_at(first_tick, period); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); @@ -617,8 +617,7 @@ async fn aggregate_expired_forwarded_payments( channel_pair_stats_store: &ChannelPairForwardingStatsStore, retention_secs: u64, logger: &Arc, ) -> Result<(u64, u64), Error> { - let now = - SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_secs(); + let now = time::unix_time_secs().unwrap_or(0); aggregate_expired_forwarded_payments_at( forwarded_payment_store, replay_marker_store, @@ -847,8 +846,7 @@ pub fn aggregate_channel_pair_stats( next_node_id = bucket.next_node_id; } } - let now = - SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_secs(); + let now = time::unix_time_secs().unwrap_or(0); Some(ChannelPairForwardingStats { id: channel_pair_stats_id( &first.prev_channel_id, diff --git a/src/payment/store.rs b/src/payment/store.rs index 46cc57b87b..dbfe5016da 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -5,8 +5,6 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - use bitcoin::secp256k1::PublicKey; use bitcoin::{BlockHash, Txid}; use lightning::chain::chaininterface::TransactionType as LdkTransactionType; @@ -24,6 +22,7 @@ use lightning_types::string::UntrustedString; use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate, UpdatableObject}; use crate::hex_utils; +use crate::time; /// An opaque token used to continue a paginated listing. /// @@ -90,10 +89,7 @@ impl PaymentDetails { id: PaymentId, kind: PaymentKind, amount_msat: Option, fee_paid_msat: Option, direction: PaymentDirection, status: PaymentStatus, ) -> Self { - let latest_update_timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs(); + let latest_update_timestamp = time::unix_time_secs().unwrap_or(0); Self { id, kind, amount_msat, fee_paid_msat, direction, status, latest_update_timestamp } } } @@ -121,10 +117,7 @@ impl Writeable for PaymentDetails { impl Readable for PaymentDetails { fn read(reader: &mut R) -> Result { - let unix_time_secs = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs(); + let unix_time_secs = time::unix_time_secs().unwrap_or(0); _init_and_read_len_prefixed_tlv_fields!(reader, { (0, id, required), // Used to be `hash` (2, preimage, required), @@ -353,10 +346,7 @@ impl UpdatableObject for PaymentDetails { } if updated { - self.latest_update_timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs(); + self.latest_update_timestamp = time::unix_time_secs().unwrap_or(0); } updated diff --git a/src/probing.rs b/src/probing.rs index 7a17cd2fad..04c7b19251 100644 --- a/src/probing.rs +++ b/src/probing.rs @@ -66,7 +66,7 @@ use std::fmt; #[cfg(feature = "uniffi")] use std::sync::RwLock; use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; +use std::time::Duration; use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::{PaymentId, RecentPaymentDetails}; @@ -83,6 +83,7 @@ use crate::config::{ DEFAULT_PROBING_INTERVAL_SECS, MIN_PROBING_INTERVAL, }; use crate::logger::{log_debug, LdkLogger, Logger}; +use crate::time::Instant; use crate::types::{ChannelManager, Graph, Router}; use crate::util::random_range; diff --git a/src/scoring.rs b/src/scoring.rs index b5c2c9a638..3fe56a9847 100644 --- a/src/scoring.rs +++ b/src/scoring.rs @@ -1,5 +1,4 @@ use std::sync::{Arc, Mutex}; -use std::time::SystemTime; use lightning::routing::scoring::ChannelLiquidities; use lightning::util::ser::Readable; @@ -12,6 +11,7 @@ use crate::config::{ use crate::io::utils::write_external_pathfinding_scores_to_cache; use crate::logger::LdkLogger; use crate::runtime::Runtime; +use crate::time; use crate::types::DynStore; use crate::{update_and_persist_node_metrics, Logger, PersistedNodeMetrics, Scorer}; @@ -82,9 +82,8 @@ async fn sync_external_scores( log_error!(logger, "Failed to persist external scores to cache: {}", e); } - let duration_since_epoch = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("system time must be after Unix epoch"); + let duration_since_epoch = + time::duration_since_epoch().expect("system time must be after Unix epoch"); scorer.lock().expect("lock").merge(liquidities, duration_since_epoch); update_and_persist_node_metrics(node_metrics, &*kv_store, logger, |m| { m.latest_pathfinding_scores_sync_timestamp = Some(duration_since_epoch.as_secs()); diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000000..564d167ac2 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,181 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +//! Wall-clock and monotonic time sources. +//! +//! All of LDK Node's clock reads go through this module. Native targets default to the system +//! clocks. Hosts without them, such as `wasm32-unknown-unknown`, must install a [`TimeProvider`] +//! via [`set_time_provider`] before building a node. +//! +//! The provider does not control clocks read inside dependencies, such as Tokio timers. + +use std::sync::{Arc, OnceLock}; +use std::time::Duration; + +use chrono::{DateTime, Utc}; +use lightning_liquidity::utils::time::TimeProvider as LiquidityTimeProvider; + +/// Supplies wall and monotonic time for LDK-Node and its explicit-time dependency calls. +/// +/// Implementations must be safe to call concurrently and must not call back into LDK-Node's +/// clock-dependent operations, including logging. Wall time should track the actual Unix time +/// used by peers and services for timestamp and expiry validation. Returning a time before the +/// epoch causes some operations to fail or panic, just as with the native clock. +pub trait TimeProvider: Send + Sync { + /// Returns time since the Unix epoch, or `None` if the clock is set before it. + fn duration_since_epoch(&self) -> Option; + + /// Returns elapsed time from a fixed, arbitrary origin. + /// + /// Values must never decrease across calls, including calls from different threads. This + /// clock must advance independently of wall-clock adjustments and must not wrap around. + fn monotonic_time(&self) -> Duration; +} + +/// The process-wide clock has already been installed or used. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TimeProviderAlreadyInitialized; + +impl std::fmt::Display for TimeProviderAlreadyInitialized { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("the time provider has already been installed or used") + } +} + +impl std::error::Error for TimeProviderAlreadyInitialized {} + +static TIME_PROVIDER: OnceLock> = OnceLock::new(); + +/// Installs the clock shared by all nodes in this process. +/// +/// Call this before building a node or using any node clock, including through logging. The first +/// clock read selects the native provider if none has been installed on a supported native target. +/// Bare WASM hosts must install a provider before the first read. Once selected, the provider +/// cannot be replaced, even after all nodes have stopped, so monotonic measurements always use the +/// same origin. Returns [`TimeProviderAlreadyInitialized`] if a provider was already selected. +pub fn set_time_provider( + provider: Arc, +) -> Result<(), TimeProviderAlreadyInitialized> { + TIME_PROVIDER.set(provider).map_err(|_| TimeProviderAlreadyInitialized) +} + +#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] +struct NativeTimeProvider(std::time::Instant); + +#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] +#[allow(clippy::disallowed_methods)] // Native platform clock boundary. +impl TimeProvider for NativeTimeProvider { + fn duration_since_epoch(&self) -> Option { + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok() + } + + fn monotonic_time(&self) -> Duration { + self.0.elapsed() + } +} + +#[allow(clippy::disallowed_methods)] // Select the native platform clock once. +fn time_provider() -> &'static dyn TimeProvider { + TIME_PROVIDER + .get_or_init(|| { + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] + { + Arc::new(NativeTimeProvider(std::time::Instant::now())) + } + #[cfg(all(target_family = "wasm", target_os = "unknown"))] + panic!("install a time provider before reading the clock on this target") + }) + .as_ref() +} + +/// Returns the time elapsed since the Unix epoch, or `None` if the clock is set before it. +pub(crate) fn duration_since_epoch() -> Option { + time_provider().duration_since_epoch() +} + +/// Returns the seconds elapsed since the Unix epoch, or `None` if the clock is set before it. +pub(crate) fn unix_time_secs() -> Option { + duration_since_epoch().map(|d| d.as_secs()) +} + +/// Returns the current wall-clock time as a UTC [`DateTime`]. +/// +/// Falls back to the Unix epoch if the clock is set before it or outside the supported range. +pub(crate) fn now_utc() -> DateTime { + to_utc(duration_since_epoch()) +} + +fn to_utc(since_epoch: Option) -> DateTime { + since_epoch + .and_then(|d| DateTime::from_timestamp(i64::try_from(d.as_secs()).ok()?, d.subsec_nanos())) + .unwrap_or_default() +} + +/// A measurement of a monotonically nondecreasing clock. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct Instant(Duration); + +impl Instant { + pub(crate) fn now() -> Self { + Self(time_provider().monotonic_time()) + } + + /// Returns the time elapsed since `earlier`, or zero if `earlier` is later than `self`. + pub(crate) fn duration_since(&self, earlier: Self) -> Duration { + self.0.saturating_sub(earlier.0) + } + + pub(crate) fn elapsed(&self) -> Duration { + Self::now().duration_since(*self) + } +} + +/// Supplies our clock to LDK components that take a [`LiquidityTimeProvider`]. +#[derive(Clone, Copy, Debug)] +pub(crate) struct LdkTimeProvider; + +impl LiquidityTimeProvider for LdkTimeProvider { + fn duration_since_epoch(&self) -> Duration { + duration_since_epoch().expect("current time should not be earlier than the Unix epoch") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] + fn native_clock() { + let start = Instant::now(); + assert!(Instant::now() >= start); + assert!(unix_time_secs().unwrap() > 0); + // The first read selects the native provider, which can't be replaced afterwards. + assert_eq!( + set_time_provider(Arc::new(NativeTimeProvider(std::time::Instant::now()))), + Err(TimeProviderAlreadyInitialized) + ); + } + + #[test] + fn instant_saturates() { + let earlier = Instant(Duration::from_secs(10)); + let later = Instant(Duration::from_secs(20)); + assert_eq!(later.duration_since(earlier), Duration::from_secs(10)); + assert_eq!(earlier.duration_since(later), Duration::ZERO); + } + + #[test] + fn utc_conversion() { + let timestamp = Duration::new(1_700_000_000, 123_456_789); + let utc = to_utc(Some(timestamp)); + assert_eq!(utc.timestamp(), timestamp.as_secs() as i64); + assert_eq!(utc.timestamp_subsec_nanos(), timestamp.subsec_nanos()); + assert_eq!(to_utc(None), DateTime::::default()); + assert_eq!(to_utc(Some(Duration::MAX)), DateTime::::default()); + } +} diff --git a/src/types.rs b/src/types.rs index fd86d1bcd8..19f5c7d19d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -30,13 +30,15 @@ use lightning::util::persist::{ }; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; -use lightning_liquidity::utils::time::DefaultTimeProvider; +#[cfg(feature = "net-tokio")] use lightning_net_tokio::SocketDescriptor; #[cfg(not(feature = "uniffi"))] use lightning_types::features::ChannelTypeFeatures; use crate::chain::ChainSource; use crate::config::{AnchorChannelsConfig, ChannelConfig}; +#[cfg(not(feature = "net-tokio"))] +use crate::connection::SocketDescriptor; use crate::data_store::{DataStore, KeepAllEntries, KeepLeastRecentlyUsed, KeepNoEntries}; use crate::fee_estimator::OnchainFeeEstimator; use crate::ffi::maybe_wrap; @@ -46,6 +48,7 @@ use crate::payment::{ ChannelPairForwardingStats, ForwardedPaymentDetails, PaymentDetails, PendingPaymentDetails, }; use crate::runtime::RuntimeSpawner; +use crate::time::LdkTimeProvider; #[cfg(feature = "uniffi")] type ChannelTypeFeatures = Arc; @@ -250,7 +253,7 @@ pub(crate) type LiquidityManager = lightning_liquidity::LiquidityManager< Arc, Arc, Arc, - DefaultTimeProvider, + Arc, Arc, >; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 13a8ef4e00..b6f400ed88 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -66,6 +66,7 @@ use crate::payment::{ PendingPaymentDetails, TransactionType, }; use crate::runtime::Runtime; +use crate::time; use crate::types::{Broadcaster, PaymentStore, PendingPaymentStore}; use crate::{ChainSource, Error}; @@ -202,11 +203,15 @@ impl Wallet { } pub(crate) fn get_full_scan_request(&self) -> FullScanRequest { - self.inner.lock().expect("lock").start_full_scan().build() + let start_time = + time::unix_time_secs().expect("current time should not be earlier than the Unix epoch"); + self.inner.lock().expect("lock").start_full_scan_at(start_time).build() } pub(crate) fn get_incremental_sync_request(&self) -> SyncRequest<(KeychainKind, u32)> { - self.inner.lock().expect("lock").start_sync_with_revealed_spks().build() + let start_time = + time::unix_time_secs().expect("current time should not be earlier than the Unix epoch"); + self.inner.lock().expect("lock").start_sync_with_revealed_spks_at(start_time).build() } pub(crate) fn get_cached_txs(&self) -> Vec> {