diff --git a/CHANGELOG.md b/CHANGELOG.md index 97a448f41e..999091d08c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 0.7.0-rc.13 (Synonym Fork) +# 0.7.0-rc.14 (Synonym Fork) ## Bug Fixes - Fixed duplicate payment events (`PaymentReceived`, `PaymentSuccessful`, `PaymentFailed`) being @@ -13,10 +13,11 @@ - `RuntimeSyncIntervals::battery_saving()` preset (5min onchain, 2min lightning, 30min fees) - Minimum 10-second interval enforced for all values - Returns `BackgroundSyncNotEnabled` error if manual sync mode was configured at build time -- Optimized startup performance by parallelizing VSS reads: +- Optimized startup performance by parallelizing VSS reads and caching network graph locally: - Parallelized early reads (node_metrics, payments, wallet) - Parallelized channel monitors and scorer reads - Parallelized tail reads (output_sweeper, event_queue, peer_store) + - Added local caching for network graph to avoid slow VSS reads on startup - Added `claimable_on_close_sats` field to `ChannelDetails` struct. This field contains the amount (in satoshis) that would be claimable if the channel were force-closed now, computed from the channel monitor's `ClaimableOnChannelClose` balance. Returns `None` if no monitor diff --git a/Cargo.toml b/Cargo.toml index 921530a9fa..10cf322425 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ldk-node" -version = "0.7.0-rc.13" +version = "0.7.0-rc.14" authors = ["Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" diff --git a/Package.swift b/Package.swift index 7dc9d66788..ed5368bdbe 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.7.0-rc.13" -let checksum = "31d9d85d9a5d48fa47cebaf24aad3710432ea3368b35ba19efda582768d48265" +let tag = "v0.7.0-rc.14" +let checksum = "117aa66af8e95212856be9aa0a99c1b59afc87e2b798a5e2ec3d33f8c6105cab" let url = "https://github.com/synonymdev/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" let package = Package( @@ -27,8 +27,9 @@ let package = Package( ), .binaryTarget( name: "LDKNodeFFI", - url: url, - checksum: checksum + // url: url, + // checksum: checksum + path: "./bindings/swift/LDKNodeFFI.xcframework" ) ] ) diff --git a/bindings/kotlin/ldk-node-android/gradle.properties b/bindings/kotlin/ldk-node-android/gradle.properties index 84c304e555..69ad920025 100644 --- a/bindings/kotlin/ldk-node-android/gradle.properties +++ b/bindings/kotlin/ldk-node-android/gradle.properties @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official -libraryVersion=0.7.0-rc.13 +libraryVersion=0.7.0-rc.14 diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/arm64-v8a/libldk_node.so b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/arm64-v8a/libldk_node.so index e53d9f3e2a..1eefa64b7a 100755 Binary files a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/arm64-v8a/libldk_node.so and b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/arm64-v8a/libldk_node.so differ diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/armeabi-v7a/libldk_node.so b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/armeabi-v7a/libldk_node.so index e4e6ffa742..b85e57f186 100755 Binary files a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/armeabi-v7a/libldk_node.so and b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/armeabi-v7a/libldk_node.so differ diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/x86_64/libldk_node.so b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/x86_64/libldk_node.so index b80868083c..2474fe61a4 100755 Binary files a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/x86_64/libldk_node.so and b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/x86_64/libldk_node.so differ diff --git a/bindings/kotlin/ldk-node-jvm/gradle.properties b/bindings/kotlin/ldk-node-jvm/gradle.properties index 1f13da06f8..04395abe8c 100644 --- a/bindings/kotlin/ldk-node-jvm/gradle.properties +++ b/bindings/kotlin/ldk-node-jvm/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx1536m kotlin.code.style=official -libraryVersion=0.7.0-rc.13 +libraryVersion=0.7.0-rc.14 diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 0047b36fdc..5c05423cd3 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ldk_node" -version = "0.7.0-rc.13" +version = "0.7.0-rc.14" authors = [ { name="Elias Rohrer", email="dev@tnull.de" }, ] diff --git a/src/builder.rs b/src/builder.rs index d516a0881d..a85040d8c2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; use std::convert::TryInto; use std::default::Default; use std::path::PathBuf; +use std::sync::atomic::AtomicU32; use std::sync::{Arc, Mutex, Once, RwLock}; use std::time::SystemTime; use std::{fmt, fs}; @@ -56,9 +57,10 @@ use crate::connection::ConnectionManager; use crate::event::EventQueue; use crate::fee_estimator::OnchainFeeEstimator; use crate::gossip::GossipSource; +use crate::io::local_graph_store::read_local_graph_cache; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{ - read_node_metrics, read_payments_async, write_node_metrics, + read_network_graph, read_node_metrics, read_payments_async, write_node_metrics, EXTERNAL_PATHFINDING_SCORES_CACHE_KEY, }; use crate::io::vss_store::VssStore; @@ -1528,18 +1530,60 @@ fn build_with_store_internal( ); } - // Initialize the network graph - let network_graph = - match io::utils::read_network_graph(Arc::clone(&kv_store), Arc::clone(&logger)) { - Ok(graph) => Arc::new(graph), - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Arc::new(Graph::new(config.network.into(), Arc::clone(&logger))) - } else { - log_error!(logger, "Failed to read network graph from store: {}", e); - return Err(BuildError::ReadFailed); + // Initialize the network graph from local cache, with VSS fallback for migration. + let (network_graph, local_rgs_timestamp) = + match read_local_graph_cache(&config.storage_dir_path, Arc::clone(&logger)) { + Ok(cache_data) => { + log_trace!( + logger, + "Loaded network graph from local cache with RGS timestamp {}", + cache_data.rgs_timestamp + ); + (Arc::new(cache_data.graph), Arc::new(AtomicU32::new(cache_data.rgs_timestamp))) + }, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + // Local cache doesn't exist, try VSS as fallback for migration. + log_trace!( + logger, + "Local graph cache not found, trying VSS fallback for migration" + ); + match read_network_graph(Arc::clone(&kv_store), Arc::clone(&logger)) { + Ok(graph) => { + let timestamp = + node_metrics.read().unwrap().latest_rgs_snapshot_timestamp.unwrap_or(0); + log_info!( + logger, + "Migrated network graph from VSS to local cache (RGS timestamp: {})", + timestamp + ); + (Arc::new(graph), Arc::new(AtomicU32::new(timestamp))) + }, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + // New user, create empty graph. RGS will download full snapshot. + log_trace!(logger, "No network graph found, creating empty graph"); + ( + Arc::new(Graph::new(config.network.into(), Arc::clone(&logger))), + Arc::new(AtomicU32::new(0)), + ) + }, + Err(e) => { + log_error!(logger, "Failed to read network graph from VSS: {}", e); + return Err(BuildError::ReadFailed); + }, } }, + Err(e) => { + // Local cache is invalid. Start fresh to avoid graph/timestamp mismatch. + log_error!( + logger, + "Local graph cache invalid: {}, starting fresh with empty graph", + e + ); + ( + Arc::new(Graph::new(config.network.into(), Arc::clone(&logger))), + Arc::new(AtomicU32::new(0)), + ) + }, }; // Read channel monitors and scorer data in parallel @@ -1804,17 +1848,12 @@ fn build_with_store_internal( } p2p_source }, - GossipSourceConfig::RapidGossipSync(rgs_server) => { - let latest_sync_timestamp = - node_metrics.read().unwrap().latest_rgs_snapshot_timestamp.unwrap_or(0); - - Arc::new(GossipSource::new_rgs( - rgs_server.clone(), - latest_sync_timestamp, - Arc::clone(&network_graph), - Arc::clone(&logger), - )) - }, + GossipSourceConfig::RapidGossipSync(rgs_server) => Arc::new(GossipSource::new_rgs( + rgs_server.clone(), + Arc::clone(&local_rgs_timestamp), + Arc::clone(&network_graph), + Arc::clone(&logger), + )), }; let (liquidity_source, custom_message_handler) = @@ -2075,6 +2114,7 @@ fn build_with_store_internal( om_mailbox, async_payments_role, runtime_sync_intervals: Arc::new(RwLock::new(RuntimeSyncIntervals::default())), + local_rgs_timestamp, }) } diff --git a/src/gossip.rs b/src/gossip.rs index 01aff47425..04710e5033 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -27,7 +27,7 @@ pub(crate) enum GossipSource { RapidGossipSync { gossip_sync: Arc, server_url: String, - latest_sync_timestamp: AtomicU32, + latest_sync_timestamp: Arc, logger: Arc, }, } @@ -43,11 +43,10 @@ impl GossipSource { } pub fn new_rgs( - server_url: String, latest_sync_timestamp: u32, network_graph: Arc, + server_url: String, latest_sync_timestamp: Arc, network_graph: Arc, logger: Arc, ) -> Self { let gossip_sync = Arc::new(RapidGossipSync::new(network_graph, Arc::clone(&logger))); - let latest_sync_timestamp = AtomicU32::new(latest_sync_timestamp); Self::RapidGossipSync { gossip_sync, server_url, latest_sync_timestamp, logger } } diff --git a/src/io/local_graph_store.rs b/src/io/local_graph_store.rs new file mode 100644 index 0000000000..c4f05e533c --- /dev/null +++ b/src/io/local_graph_store.rs @@ -0,0 +1,276 @@ +// 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. + +//! Local caching for the network graph to avoid slow VSS reads/writes. + +use std::fs; +use std::future::Future; +use std::ops::Deref; +use std::pin::Pin; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; + +use lightning::io; +use lightning::io::Cursor; +use lightning::routing::gossip::NetworkGraph; +use lightning::util::persist::{ + KVStore, KVStoreSync, NETWORK_GRAPH_PERSISTENCE_KEY, + NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use lightning::util::ser::ReadableArgs; + +use crate::logger::{log_error, LdkLogger}; +use crate::types::DynStore; + +const LOCAL_GRAPH_CACHE_MAGIC: &[u8; 4] = b"LGNC"; +const LOCAL_GRAPH_CACHE_VERSION: u8 = 1; +pub const NETWORK_GRAPH_LOCAL_CACHE_FILENAME: &str = "network_graph_cache"; + +/// Result of reading the local graph cache. +pub struct LocalGraphCacheData +where + L::Target: LdkLogger, +{ + pub graph: NetworkGraph, + pub rgs_timestamp: u32, +} + +/// Header: 4 bytes magic + 1 byte version + 4 bytes timestamp +const LOCAL_GRAPH_CACHE_HEADER_SIZE: usize = 9; + +/// Reads the network graph and RGS timestamp from the local cache. +pub fn read_local_graph_cache( + storage_dir_path: &str, logger: L, +) -> Result, std::io::Error> +where + L::Target: LdkLogger, +{ + let cache_path = format!("{}/{}", storage_dir_path, NETWORK_GRAPH_LOCAL_CACHE_FILENAME); + let data = fs::read(&cache_path)?; + + if data.len() < LOCAL_GRAPH_CACHE_HEADER_SIZE { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Local graph cache too small", + )); + } + + if &data[0..4] != LOCAL_GRAPH_CACHE_MAGIC { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid local graph cache magic bytes", + )); + } + + if data[4] != LOCAL_GRAPH_CACHE_VERSION { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Unsupported local graph cache version: {}", data[4]), + )); + } + + let rgs_timestamp = u32::from_be_bytes([data[5], data[6], data[7], data[8]]); + let graph_data = &data[LOCAL_GRAPH_CACHE_HEADER_SIZE..]; + + let mut graph_cursor = Cursor::new(graph_data); + let graph = NetworkGraph::read(&mut graph_cursor, logger.clone()).map_err(|e| { + log_error!(logger, "Failed to deserialize NetworkGraph from local cache: {}", e); + std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to deserialize NetworkGraph") + })?; + + Ok(LocalGraphCacheData { graph, rgs_timestamp }) +} + +fn write_local_graph_cache_bytes( + storage_dir_path: &str, graph_bytes: &[u8], rgs_timestamp: u32, +) -> Result<(), std::io::Error> { + fs::create_dir_all(storage_dir_path)?; + + let cache_path = format!("{}/{}", storage_dir_path, NETWORK_GRAPH_LOCAL_CACHE_FILENAME); + + let mut cache_data = Vec::with_capacity(LOCAL_GRAPH_CACHE_HEADER_SIZE + graph_bytes.len()); + cache_data.extend_from_slice(LOCAL_GRAPH_CACHE_MAGIC); + cache_data.push(LOCAL_GRAPH_CACHE_VERSION); + cache_data.extend_from_slice(&rgs_timestamp.to_be_bytes()); + cache_data.extend_from_slice(graph_bytes); + + fs::write(&cache_path, cache_data) +} + +fn read_local_graph_cache_bytes(storage_dir_path: &str) -> Result, std::io::Error> { + let cache_path = format!("{}/{}", storage_dir_path, NETWORK_GRAPH_LOCAL_CACHE_FILENAME); + let data = fs::read(&cache_path)?; + + if data.len() < LOCAL_GRAPH_CACHE_HEADER_SIZE { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Local graph cache too small", + )); + } + + if &data[0..4] != LOCAL_GRAPH_CACHE_MAGIC { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid local graph cache magic bytes", + )); + } + + if data[4] != LOCAL_GRAPH_CACHE_VERSION { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Unsupported local graph cache version: {}", data[4]), + )); + } + + Ok(data[LOCAL_GRAPH_CACHE_HEADER_SIZE..].to_vec()) +} + +/// A KVStore wrapper that redirects network graph operations to local storage. +pub struct LocalGraphStore { + inner: Arc, + storage_dir_path: String, + rgs_timestamp: Arc, +} + +impl LocalGraphStore { + pub fn new( + inner: Arc, storage_dir_path: String, rgs_timestamp: Arc, + ) -> Self { + Self { inner, storage_dir_path, rgs_timestamp } + } + + fn is_network_graph( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> bool { + primary_namespace == NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE + && secondary_namespace == NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE + && key == NETWORK_GRAPH_PERSISTENCE_KEY + } +} + +impl KVStoreSync for LocalGraphStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Result, io::Error> { + if self.is_network_graph(primary_namespace, secondary_namespace, key) { + read_local_graph_cache_bytes(&self.storage_dir_path).map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + io::Error::new(io::ErrorKind::NotFound, e) + } else { + io::Error::new(io::ErrorKind::Other, format!("Local cache read failed: {}", e)) + } + }) + } else { + KVStoreSync::read(&*self.inner, primary_namespace, secondary_namespace, key) + } + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Result<(), io::Error> { + if self.is_network_graph(primary_namespace, secondary_namespace, key) { + let timestamp = self.rgs_timestamp.load(Ordering::Acquire); + write_local_graph_cache_bytes(&self.storage_dir_path, &buf, timestamp).map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("Local cache write failed: {}", e)) + }) + } else { + KVStoreSync::write(&*self.inner, primary_namespace, secondary_namespace, key, buf) + } + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> Result<(), io::Error> { + if self.is_network_graph(primary_namespace, secondary_namespace, key) { + let cache_path = + format!("{}/{}", self.storage_dir_path, NETWORK_GRAPH_LOCAL_CACHE_FILENAME); + match std::fs::remove_file(&cache_path) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to remove local cache: {}", e), + )), + } + } else { + KVStoreSync::remove(&*self.inner, primary_namespace, secondary_namespace, key, lazy) + } + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Result, io::Error> { + KVStoreSync::list(&*self.inner, primary_namespace, secondary_namespace) + } +} + +impl KVStore for LocalGraphStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin, io::Error>> + 'static + Send>> { + if self.is_network_graph(primary_namespace, secondary_namespace, key) { + let storage_dir = self.storage_dir_path.clone(); + Box::pin(async move { + read_local_graph_cache_bytes(&storage_dir).map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + io::Error::new(io::ErrorKind::NotFound, e) + } else { + io::Error::new( + io::ErrorKind::Other, + format!("Local cache read failed: {}", e), + ) + } + }) + }) + } else { + KVStore::read(&*self.inner, primary_namespace, secondary_namespace, key) + } + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Pin> + 'static + Send>> { + if self.is_network_graph(primary_namespace, secondary_namespace, key) { + let storage_dir = self.storage_dir_path.clone(); + let timestamp = self.rgs_timestamp.load(Ordering::Acquire); + Box::pin(async move { + write_local_graph_cache_bytes(&storage_dir, &buf, timestamp).map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("Local cache write failed: {}", e)) + }) + }) + } else { + KVStore::write(&*self.inner, primary_namespace, secondary_namespace, key, buf) + } + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> Pin> + 'static + Send>> { + if self.is_network_graph(primary_namespace, secondary_namespace, key) { + let storage_dir = self.storage_dir_path.clone(); + Box::pin(async move { + let cache_path = format!("{}/{}", storage_dir, NETWORK_GRAPH_LOCAL_CACHE_FILENAME); + match std::fs::remove_file(&cache_path) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to remove local cache: {}", e), + )), + } + }) + } else { + KVStore::remove(&*self.inner, primary_namespace, secondary_namespace, key, lazy) + } + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Pin, io::Error>> + 'static + Send>> { + KVStore::list(&*self.inner, primary_namespace, secondary_namespace) + } +} diff --git a/src/io/mod.rs b/src/io/mod.rs index 38fba5114f..611acc8362 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -7,6 +7,7 @@ //! Objects and traits for data persistence. +pub(crate) mod local_graph_store; pub mod sqlite_store; #[cfg(test)] pub(crate) mod test_utils; diff --git a/src/lib.rs b/src/lib.rs index 343262efb5..4b94448fa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,6 +106,7 @@ use std::collections::HashMap; use std::default::Default; use std::net::ToSocketAddrs; use std::ops::Deref; +use std::sync::atomic::AtomicU32; use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -213,6 +214,8 @@ pub struct Node { om_mailbox: Option>, async_payments_role: Option, runtime_sync_intervals: Arc>, + /// Shared RGS timestamp used by LocalGraphStore to persist the timestamp alongside the graph. + local_rgs_timestamp: Arc, } impl Node { @@ -570,7 +573,13 @@ impl Node { )); // Setup background processing - let background_persister = Arc::clone(&self.kv_store); + // Wrap the kv_store with LocalGraphStore to redirect network graph persistence to local storage + let background_persister: Arc = + Arc::new(io::local_graph_store::LocalGraphStore::new( + Arc::clone(&self.kv_store), + self.config.storage_dir_path.clone(), + Arc::clone(&self.local_rgs_timestamp), + )); let background_event_handler = Arc::clone(&event_handler); let background_chain_mon = Arc::clone(&self.chain_monitor); let background_chan_man = Arc::clone(&self.channel_manager);