From ef2464883db4ed8f1fc6ef22eb3555392af29374 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 30 Sep 2026 23:34:21 -0500 Subject: [PATCH 1/4] Centralize clock reads in a time module Keep node wall and monotonic clock reads behind one provider in the time module. Use native clocks by default and let embedding hosts supply a clock before first use. Require a provider on bare WASM and prevent replacement so monotonic measurements share one origin. Drop chrono's clock feature, as UTC timestamps now come from the provider. Ban direct platform clock reads with clippy and run the check in a dedicated CI job across the native and UniFFI feature sets, so new code stays wasm32-safe while the rest of the port lands. AI assistance: Claude Opus 5.5 and OpenAI Codex. Co-Authored-By: Claude Opus 5.5 --- .clippy.toml | 15 +- .github/workflows/rust.yml | 18 +++ Cargo.toml | 2 +- src/builder.rs | 10 +- src/chain/bitcoind.rs | 24 ++-- src/chain/electrum.rs | 12 +- src/chain/esplora.rs | 14 +- src/lib.rs | 14 +- src/liquidity/service/lsps2.rs | 4 +- src/logger.rs | 4 +- src/payment/asynchronous/rate_limiter.rs | 3 +- src/payment/bolt12.rs | 21 +-- src/payment/forwarding_store.rs | 20 ++- src/payment/store.rs | 18 +-- src/probing.rs | 3 +- src/scoring.rs | 7 +- src/time.rs | 170 +++++++++++++++++++++++ 17 files changed, 271 insertions(+), 88 deletions(-) create mode 100644 src/time.rs diff --git a/.clippy.toml b/.clippy.toml index be64409c8d..1be12cc23a 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1 +1,14 @@ -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 }, +] diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 119ee51734..85ac532375 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -109,6 +109,24 @@ 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 + doc: name: Documentation timeout-minutes: 120 diff --git a/Cargo.toml b/Cargo.toml index 9f8a729655..1248ae2f2e 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,7 +113,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..95b929ca1d 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, @@ -1938,8 +1938,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 +2361,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/lib.rs b/src/lib.rs index fbb65b0005..1194e65470 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"))] @@ -595,8 +597,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 +632,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 +761,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/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..9a2357c877 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,170 @@ +// 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}; + +/// 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) + } +} + +#[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()); + } +} From 0cf2164567ae5502d4d5d51629e933cafac2e12e Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 30 Sep 2026 23:35:20 -0500 Subject: [PATCH 2/4] Use explicit time in dependency APIs Supply node time to BDK scans, liquidity, invoice timestamps, gossip snapshots, and UniFFI expiry checks through existing explicit-time APIs. Preserve freshness and expiry validation, and report a pre-epoch clock as an LSPS2 invoice creation error. Disable transaction-sync's optional timing logs, which duplicate the node's own sync duration logs, so it no longer reads the clock itself. Ban the dependency APIs that read the platform clock internally, so call sites keep using the explicit-time variants. AI assistance: Claude Opus 5.5 and OpenAI Codex. Co-Authored-By: Claude Opus 5.5 --- .clippy.toml | 21 +++++++++++++++++++++ Cargo.toml | 4 ++-- src/ffi/types.rs | 15 ++++++++++----- src/gossip.rs | 8 ++++++-- src/liquidity/client/lsps2.rs | 10 +++++++++- src/liquidity/mod.rs | 6 ++++-- src/time.rs | 11 +++++++++++ src/types.rs | 4 ++-- src/wallet/mod.rs | 9 +++++++-- 9 files changed, 72 insertions(+), 16 deletions(-) diff --git a/.clippy.toml b/.clippy.toml index 1be12cc23a..3e20ed160f 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -11,4 +11,25 @@ disallowed-methods = [ { 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/Cargo.toml b/Cargo.toml index 1248ae2f2e..1e7fe06b4e 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ chain-esplora = [ "dep:ldk-esplora-client", "dep:lightning-transaction-sync", "lightning-transaction-sync/esplora-async-https", - "lightning-transaction-sync/time", ] chain-electrum = [ "dep:bdk_electrum", @@ -94,7 +93,8 @@ lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning 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-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 } +# 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", features = ["std"] } 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 } 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/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/time.rs b/src/time.rs index 9a2357c877..564d167ac2 100644 --- a/src/time.rs +++ b/src/time.rs @@ -17,6 +17,7 @@ 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. /// @@ -133,6 +134,16 @@ impl Instant { } } +/// 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::*; diff --git a/src/types.rs b/src/types.rs index fd86d1bcd8..6b826fcf6b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -30,7 +30,6 @@ use lightning::util::persist::{ }; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; -use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; #[cfg(not(feature = "uniffi"))] use lightning_types::features::ChannelTypeFeatures; @@ -46,6 +45,7 @@ use crate::payment::{ ChannelPairForwardingStats, ForwardedPaymentDetails, PaymentDetails, PendingPaymentDetails, }; use crate::runtime::RuntimeSpawner; +use crate::time::LdkTimeProvider; #[cfg(feature = "uniffi")] type ChannelTypeFeatures = Arc; @@ -250,7 +250,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> { From e4c8142af3264c6417d1edb5892ec971d5f4f5dc Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 30 Sep 2026 23:21:36 -0500 Subject: [PATCH 3/4] Gate LDK std behind an ldk-std feature LDK's std feature reads the system clock internally (channel manager, gossip, offers, payment retries, liquidity), which wasm32 can't do. Move it and liquidity's time feature behind a default ldk-std feature, so code can switch to LDK's explicit-time APIs when it is disabled. The chain sources and filesystem storage pull in LDK crates that require std, so they enable ldk-std. Native builds resolve the same LDK features as before. Co-Authored-By: Claude Opus 5.5 --- Cargo.toml | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1e7fe06b4e..af4dae3ab8 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ panic = 'abort' # Abort on panic [features] default = [ + "ldk-std", "chain-esplora", "chain-electrum", "chain-bitcoind", @@ -37,7 +38,19 @@ 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", +] chain-esplora = [ + "ldk-std", "dep:bdk_esplora", "dep:esplora-client", "dep:ldk-esplora-client", @@ -45,14 +58,15 @@ chain-esplora = [ "lightning-transaction-sync/esplora-async-https", ] 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"] @@ -85,17 +99,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-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" } 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 } # 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", features = ["std"] } +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 } From e3dc5509ccfb6b3f8e2d4f61d422a3ef9676d9d7 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Thu, 1 Oct 2026 14:18:51 -0500 Subject: [PATCH 4/4] Gate the tokio peer transport behind a feature lightning-net-tokio enables LDK's default features, which turns std back on even when ldk-std is disabled. Move it behind a default net-tokio feature so builds without ldk-std are free of LDK's internal clock reads. Without net-tokio, a placeholder socket type stands in for the transport. It has no values, so no connection can be created with it: connecting to peers fails and listening addresses are rejected at build time. This leaves the seam for a wasm32 transport later. Restore the CI check that disabling default features leaves no LDK clock features enabled, and lint a build without net-tokio so the placeholder keeps compiling. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/rust.yml | 9 +++++++++ Cargo.toml | 7 ++++++- src/builder.rs | 6 ++++++ src/connection.rs | 40 +++++++++++++++++++++++++++++++++++++- src/lib.rs | 1 + src/types.rs | 3 +++ 6 files changed, 64 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 85ac532375..f475d99b83 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -126,6 +126,15 @@ jobs: 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 diff --git a/Cargo.toml b/Cargo.toml index af4dae3ab8..94b6e40fcd 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ panic = 'abort' # Abort on panic [features] default = [ "ldk-std", + "net-tokio", "chain-esplora", "chain-electrum", "chain-bitcoind", @@ -49,6 +50,9 @@ ldk-std = [ "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", @@ -78,6 +82,7 @@ unified-payments = [ uniffi = ["dep:uniffi"] uniffi-default = [ "uniffi", + "net-tokio", "chain-esplora", "chain-electrum", "storage-sqlite", @@ -102,7 +107,7 @@ uniffi-default = [ 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", default-features = false } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457" } +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", default-features = false } lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2985bd95fdbce06bf9f25f684772b0ba9903e457", default-features = false } diff --git a/src/builder.rs b/src/builder.rs index 95b929ca1d..195187b635 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -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()); 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/lib.rs b/src/lib.rs index 1194e65470..1c07a50099 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -438,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); diff --git a/src/types.rs b/src/types.rs index 6b826fcf6b..19f5c7d19d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -30,12 +30,15 @@ use lightning::util::persist::{ }; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; +#[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;