Skip to content

Commit d588704

Browse files
committed
Expose tiered storage configuration across FFI
Foreign-language applications cannot use the generic Rust store builder API. They therefore need an FFI-safe store interface before they can provide a custom primary store or configure tiered storage. Backup synchronization also requires the primary store to support paginated namespace listing and exhaustive key enumeration for backfilling and resilvering. In this commit, we: - Add the asynchronous DynStoreTrait FFI interface for reading, writing, removing, listing, paginated listing, and exhaustive key enumeration. - Add FFI-safe key and paginated-response types and translate between them and the corresponding rust-lightning types. - Adapt foreign store implementations to ldk-node's internal dynamic store interface. - Expose Builder::build_with_store for custom primary stores when tiered storage is enabled. - Expose SQLite ephemeral and backup storage directory configuration through the FFI builder. - Share tier-store setup between native and FFI-backed primary stores. - Update shared Rust test setup to erase concrete stores behind the FFI trait when testing the tiered UniFFI configuration. Assisted-by: Amp (AI coding agent)
1 parent e56a9f6 commit d588704

7 files changed

Lines changed: 509 additions & 26 deletions

File tree

bindings/ldk_node.udl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@ interface LogWriter {
3131
void log(LogRecord record);
3232
};
3333

34+
[Trait, WithForeign]
35+
interface DynStoreTrait {
36+
[Throws=IOError, Async]
37+
bytes read(string primary_namespace, string secondary_namespace, string key);
38+
[Throws=IOError, Async]
39+
void write(string primary_namespace, string secondary_namespace, string key, bytes buf);
40+
[Throws=IOError, Async]
41+
void remove(string primary_namespace, string secondary_namespace, string key, boolean lazy);
42+
[Throws=IOError, Async]
43+
sequence<string> list(string primary_namespace, string secondary_namespace);
44+
[Throws=IOError, Async]
45+
PaginatedListResponse list_paginated(string primary_namespace, string secondary_namespace, PageToken? page_token);
46+
[Throws=IOError, Async]
47+
sequence<KVStoreKey> list_all_keys();
48+
};
49+
50+
typedef dictionary KVStoreKey;
51+
52+
typedef dictionary PaginatedListResponse;
53+
54+
typedef enum IOError;
55+
3456
interface ProbingConfigBuilder {
3557
[Name=high_degree]
3658
constructor(u64 top_node_count);

src/builder.rs

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ use crate::data_store::{KeepAllEntries, KeepLeastRecentlyUsed};
6969
use crate::entropy::NodeEntropy;
7070
use crate::event::EventQueue;
7171
use crate::fee_estimator::OnchainFeeEstimator;
72+
#[cfg(all(feature = "uniffi", feature = "storage-tier"))]
73+
use crate::ffi::DynStoreTrait;
7274
use crate::gossip::GossipSource;
7375
#[cfg(feature = "storage-filesystem")]
7476
use crate::io::fs_store::open_or_migrate_fs_store;
@@ -716,7 +718,7 @@ impl NodeBuilder {
716718
/// If not set, durable data will be stored only in the primary store.
717719
///
718720
/// [`SQLITE_BACKUP_DB_FILE_NAME`]: crate::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME
719-
#[cfg(all(not(feature = "uniffi"), feature = "storage-tier"))]
721+
#[cfg(feature = "storage-tier")]
720722
pub fn set_backup_storage_dir_path(&mut self, backup_storage_dir_path: String) -> &mut Self {
721723
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
722724
tier_store_config.backup_storage_dir_path = Some(backup_storage_dir_path.into());
@@ -729,7 +731,7 @@ impl NodeBuilder {
729731
/// the network graph and scorer. Data stored here can be rebuilt if lost.
730732
///
731733
/// If not set, non-critical data will be stored in the primary store.
732-
#[cfg(all(not(feature = "uniffi"), feature = "storage-tier"))]
734+
#[cfg(feature = "storage-tier")]
733735
pub fn set_ephemeral_storage_dir_path(
734736
&mut self, ephemeral_storage_dir_path: String,
735737
) -> &mut Self {
@@ -962,6 +964,7 @@ impl NodeBuilder {
962964
/// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
963965
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
964966
#[cfg(not(feature = "storage-tier"))]
967+
#[cfg_attr(feature = "uniffi", allow(dead_code))]
965968
pub fn build_with_store<S: PaginatedKVStore + Send + Sync + 'static>(
966969
&self, node_entropy: NodeEntropy, kv_store: S,
967970
) -> Result<Node, BuildError> {
@@ -982,6 +985,7 @@ impl NodeBuilder {
982985
/// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
983986
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
984987
#[cfg(feature = "storage-tier")]
988+
#[cfg_attr(feature = "uniffi", allow(dead_code))]
985989
pub fn build_with_store<S: PaginatedKVStore + MigratableKVStore + Send + Sync + 'static>(
986990
&self, node_entropy: NodeEntropy, kv_store: S,
987991
) -> Result<Node, BuildError> {
@@ -1032,9 +1036,28 @@ impl NodeBuilder {
10321036
>(
10331037
&self, node_entropy: NodeEntropy, kv_store: S, runtime: Arc<Runtime>, logger: Arc<Logger>,
10341038
) -> Result<Node, BuildError> {
1039+
let primary_store: Arc<DynStore> = Arc::new(DynStoreWrapper(kv_store));
1040+
let store = self.setup_tier_store(primary_store, &runtime, &logger)?;
1041+
self.build_with_dyn_store(node_entropy, store, runtime, logger)
1042+
}
1043+
1044+
#[cfg(all(feature = "uniffi", feature = "storage-tier"))]
1045+
fn build_with_ffi_store(
1046+
&self, node_entropy: NodeEntropy, kv_store: Arc<dyn DynStoreTrait>,
1047+
) -> Result<Node, BuildError> {
1048+
let logger = setup_logger(&self.log_writer_config, &self.config)?;
1049+
let runtime = self.setup_runtime(&logger)?;
1050+
let primary_store: Arc<DynStore> = Arc::new(crate::ffi::DynStore::new(kv_store));
1051+
let store = self.setup_tier_store(primary_store, &runtime, &logger)?;
1052+
self.build_with_dyn_store(node_entropy, store, runtime, logger)
1053+
}
1054+
1055+
#[cfg(feature = "storage-tier")]
1056+
fn setup_tier_store(
1057+
&self, primary_store: Arc<DynStore>, runtime: &Arc<Runtime>, logger: &Arc<Logger>,
1058+
) -> Result<Arc<DynStore>, BuildError> {
10351059
let store: Arc<DynStore> = {
10361060
let ts_config = self.tier_store_config.as_ref();
1037-
let primary_store = Arc::new(DynStoreWrapper(kv_store));
10381061
let mut tier_store = TierStore::new(primary_store, Arc::clone(&logger));
10391062
let tier_index_exists = PathBuf::from(&self.config.storage_dir_path)
10401063
.join(io::sqlite_store::SQLITE_TIER_INDEX_DB_FILE_NAME)
@@ -1098,7 +1121,7 @@ impl NodeBuilder {
10981121
})?;
10991122
Arc::new(DynStoreWrapper(tier_store))
11001123
};
1101-
self.build_with_dyn_store(node_entropy, store, runtime, logger)
1124+
Ok(store)
11021125
}
11031126

11041127
fn build_with_dyn_store(
@@ -1645,23 +1668,45 @@ impl Builder {
16451668
}
16461669
}
16471670

1648-
#[cfg(feature = "uniffi")]
1671+
#[cfg(all(feature = "uniffi", not(feature = "storage-tier")))]
16491672
impl ArcedNodeBuilder {
16501673
/// Builds a [`Node`] instance according to the options previously configured.
1651-
// Note that the generics here don't actually work for Uniffi, but we don't currently expose
1652-
// this so its not needed.
1653-
#[cfg(not(feature = "storage-tier"))]
1674+
// Note that the generics here don't actually work for UniFFI, but we don't currently expose
1675+
// this so it is only used by Rust tests compiled with the `uniffi` feature.
16541676
pub fn build_with_store<S: PaginatedKVStore + Send + Sync + 'static>(
16551677
&self, node_entropy: Arc<NodeEntropy>, kv_store: S,
16561678
) -> Result<Arc<Node>, BuildError> {
16571679
self.inner.read().expect("lock").build_with_store(*node_entropy, kv_store).map(Arc::new)
16581680
}
1681+
}
16591682

1660-
#[cfg(feature = "storage-tier")]
1661-
pub fn build_with_store<S: PaginatedKVStore + MigratableKVStore + Send + Sync + 'static>(
1662-
&self, node_entropy: Arc<NodeEntropy>, kv_store: S,
1683+
#[cfg(all(feature = "uniffi", feature = "storage-tier"))]
1684+
#[uniffi::export]
1685+
impl Builder {
1686+
/// Configures a local SQLite backup store for durable data.
1687+
///
1688+
/// The backup is brought up to date before node construction completes. While configured,
1689+
/// durable writes and removals only succeed when both primary and backup storage succeed.
1690+
pub fn set_backup_storage_dir_path(&self, backup_storage_dir_path: String) {
1691+
self.inner.write().expect("lock").set_backup_storage_dir_path(backup_storage_dir_path);
1692+
}
1693+
1694+
/// Configures a local SQLite store for rebuildable cache data.
1695+
pub fn set_ephemeral_storage_dir_path(&self, ephemeral_storage_dir_path: String) {
1696+
self.inner
1697+
.write()
1698+
.expect("lock")
1699+
.set_ephemeral_storage_dir_path(ephemeral_storage_dir_path);
1700+
}
1701+
1702+
/// Builds a [`Node`] instance according to the options previously configured.
1703+
///
1704+
/// The provided store is used as authoritative primary storage. It must support paginated
1705+
/// namespace listing and exhaustive key enumeration for backup synchronization.
1706+
pub fn build_with_store(
1707+
&self, node_entropy: Arc<NodeEntropy>, kv_store: Arc<dyn DynStoreTrait>,
16631708
) -> Result<Arc<Node>, BuildError> {
1664-
self.inner.read().expect("lock").build_with_store(*node_entropy, kv_store).map(Arc::new)
1709+
self.inner.read().expect("lock").build_with_ffi_store(*node_entropy, kv_store).map(Arc::new)
16651710
}
16661711
}
16671712

0 commit comments

Comments
 (0)