Skip to content

Commit 7b52f44

Browse files
committed
Add Rust integration tests for tiered storage
Extend the Rust test harness to support tiered store configurations and add integration coverage for routing data across primary, backup, and ephemeral stores. This introduces tier-store-aware test helpers, including support for configuring separate stores per node and inspecting persisted data across native and UniFFI-enabled test builds. Add an integration test covering the tiered-storage channel lifecycle and verifying that: - durable node data is persisted to both the primary and backup stores - ephemeral-routed data is stored in the ephemeral tier - ephemeral data is not mirrored back into the durable tiers Also update existing Rust test call sites and benchmark helpers to match the new tier-store-aware setup APIs.
1 parent dc1279b commit 7b52f44

File tree

3 files changed

+261
-17
lines changed

3 files changed

+261
-17
lines changed

benches/payments.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ fn payment_benchmark(c: &mut Criterion) {
127127
true,
128128
false,
129129
common::TestStoreType::Sqlite,
130+
common::TestStoreType::Sqlite,
130131
);
131132

132133
let runtime =

tests/common/mod.rs

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,23 @@ pub(crate) enum TestChainSource<'a> {
326326
BitcoindRestSync(&'a BitcoinD),
327327
}
328328

329-
#[derive(Clone, Copy)]
329+
#[cfg(feature = "uniffi")]
330+
use ldk_node::FfiDynStoreTrait;
331+
332+
#[cfg(feature = "uniffi")]
333+
type TestDynStore = Arc<dyn FfiDynStoreTrait>;
334+
#[cfg(not(feature = "uniffi"))]
335+
type TestDynStore = TestSyncStore;
336+
337+
#[derive(Clone)]
330338
pub(crate) enum TestStoreType {
331339
TestSyncStore,
332340
Sqlite,
341+
TierStore {
342+
primary: TestDynStore,
343+
backup: Option<TestDynStore>,
344+
ephemeral: Option<TestDynStore>,
345+
},
333346
}
334347

335348
impl Default for TestStoreType {
@@ -380,6 +393,27 @@ macro_rules! setup_builder {
380393

381394
pub(crate) use setup_builder;
382395

396+
pub(crate) fn create_tier_stores(base_path: PathBuf) -> (TestDynStore, TestDynStore, TestDynStore) {
397+
let primary = TestSyncStore::new(base_path.join("primary"));
398+
let backup = TestSyncStore::new(base_path.join("backup"));
399+
let ephemeral = TestSyncStore::new(base_path.join("ephemeral"));
400+
401+
#[cfg(feature = "uniffi")]
402+
{
403+
use ldk_node::DynStoreWrapper;
404+
405+
(
406+
Arc::new(DynStoreWrapper(primary)) as Arc<dyn FfiDynStoreTrait>,
407+
Arc::new(DynStoreWrapper(backup)) as Arc<dyn FfiDynStoreTrait>,
408+
Arc::new(DynStoreWrapper(ephemeral)) as Arc<dyn FfiDynStoreTrait>,
409+
)
410+
}
411+
#[cfg(not(feature = "uniffi"))]
412+
{
413+
(primary, backup, ephemeral)
414+
}
415+
}
416+
383417
pub(crate) fn setup_two_nodes(
384418
chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool,
385419
anchors_trusted_no_reserve: bool,
@@ -390,21 +424,22 @@ pub(crate) fn setup_two_nodes(
390424
anchor_channels,
391425
anchors_trusted_no_reserve,
392426
TestStoreType::TestSyncStore,
427+
TestStoreType::TestSyncStore,
393428
)
394429
}
395430

396431
pub(crate) fn setup_two_nodes_with_store(
397432
chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool,
398-
anchors_trusted_no_reserve: bool, store_type: TestStoreType,
433+
anchors_trusted_no_reserve: bool, store_type_a: TestStoreType, store_type_b: TestStoreType,
399434
) -> (TestNode, TestNode) {
400435
println!("== Node A ==");
401436
let mut config_a = random_config(anchor_channels);
402-
config_a.store_type = store_type;
437+
config_a.store_type = store_type_a;
403438
let node_a = setup_node(chain_source, config_a);
404439

405440
println!("\n== Node B ==");
406441
let mut config_b = random_config(anchor_channels);
407-
config_b.store_type = store_type;
442+
config_b.store_type = store_type_b;
408443
if allow_0conf {
409444
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
410445
}
@@ -484,9 +519,53 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) ->
484519
let node = match config.store_type {
485520
TestStoreType::TestSyncStore => {
486521
let kv_store = TestSyncStore::new(config.node_config.storage_dir_path.into());
487-
builder.build_with_store(config.node_entropy.into(), kv_store).unwrap()
522+
#[cfg(feature = "uniffi")]
523+
{
524+
use ldk_node::DynStoreWrapper;
525+
let kv_store: Arc<dyn FfiDynStoreTrait> = Arc::new(DynStoreWrapper(kv_store));
526+
527+
builder.build_with_store(config.node_entropy.into(), kv_store).unwrap()
528+
}
529+
#[cfg(not(feature = "uniffi"))]
530+
{
531+
builder.build_with_store(config.node_entropy, kv_store).unwrap()
532+
}
488533
},
489534
TestStoreType::Sqlite => builder.build(config.node_entropy.into()).unwrap(),
535+
TestStoreType::TierStore { primary, backup, ephemeral } => {
536+
if let Some(backup) = backup {
537+
#[cfg(feature = "uniffi")]
538+
{
539+
builder.set_backup_store(backup);
540+
}
541+
#[cfg(not(feature = "uniffi"))]
542+
{
543+
use ldk_node::{DynStore, DynStoreWrapper};
544+
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(backup));
545+
builder.set_backup_store(store);
546+
}
547+
}
548+
if let Some(ephemeral) = ephemeral {
549+
#[cfg(feature = "uniffi")]
550+
{
551+
builder.set_ephemeral_store(ephemeral);
552+
}
553+
#[cfg(not(feature = "uniffi"))]
554+
{
555+
use ldk_node::{DynStore, DynStoreWrapper};
556+
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(ephemeral));
557+
builder.set_ephemeral_store(store);
558+
}
559+
}
560+
#[cfg(feature = "uniffi")]
561+
{
562+
builder.build_with_store(config.node_entropy.into(), primary).unwrap()
563+
}
564+
#[cfg(not(feature = "uniffi"))]
565+
{
566+
builder.build_with_store(config.node_entropy, primary).unwrap()
567+
}
568+
},
490569
};
491570

492571
if config.recovery_mode {
@@ -1709,3 +1788,36 @@ impl TestSyncStoreInner {
17091788
self.do_list(primary_namespace, secondary_namespace)
17101789
}
17111790
}
1791+
1792+
pub fn test_kv_read(
1793+
store: &TestDynStore, primary_ns: &str, secondary_ns: &str, key: &str,
1794+
) -> Result<Vec<u8>, bitcoin::io::Error> {
1795+
#[cfg(feature = "uniffi")]
1796+
{
1797+
ldk_node::FfiDynStoreTrait::read(
1798+
&**store,
1799+
primary_ns.to_string(),
1800+
secondary_ns.to_string(),
1801+
key.to_string(),
1802+
)
1803+
.map_err(Into::into)
1804+
}
1805+
#[cfg(not(feature = "uniffi"))]
1806+
{
1807+
KVStoreSync::read(store, primary_ns, secondary_ns, key)
1808+
}
1809+
}
1810+
1811+
pub fn test_kv_list(
1812+
store: &TestDynStore, primary_ns: &str, secondary_ns: &str,
1813+
) -> Result<Vec<String>, bitcoin::io::Error> {
1814+
#[cfg(feature = "uniffi")]
1815+
{
1816+
ldk_node::FfiDynStoreTrait::list(&**store, primary_ns.to_string(), secondary_ns.to_string())
1817+
.map_err(Into::into)
1818+
}
1819+
#[cfg(not(feature = "uniffi"))]
1820+
{
1821+
KVStoreSync::list(store, primary_ns, secondary_ns)
1822+
}
1823+
}

tests/integration_tests_rust.rs

Lines changed: 143 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ use bitcoin::hashes::Hash;
1717
use bitcoin::{Address, Amount, ScriptBuf, Txid};
1818
use common::logging::{init_log_logger, validate_log_entry, MultiNodeLogger, TestLogWriter};
1919
use common::{
20-
bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle,
21-
expect_channel_pending_event, expect_channel_ready_event, expect_channel_ready_events,
22-
expect_event, expect_payment_claimable_event, expect_payment_received_event,
23-
expect_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait,
24-
generate_listening_addresses, open_channel, open_channel_push_amt, open_channel_with_all,
25-
premine_and_distribute_funds, premine_blocks, prepare_rbf, random_chain_source, random_config,
26-
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, splice_in_with_all,
27-
wait_for_tx, TestChainSource, TestStoreType, TestSyncStore,
20+
bump_fee_and_broadcast, create_tier_stores, distribute_funds_unconfirmed,
21+
do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event,
22+
expect_channel_ready_events, expect_event, expect_payment_claimable_event,
23+
expect_payment_received_event, expect_payment_successful_event, expect_splice_pending_event,
24+
generate_blocks_and_wait, generate_listening_addresses, open_channel, open_channel_push_amt,
25+
open_channel_with_all, premine_and_distribute_funds, premine_blocks, prepare_rbf,
26+
random_chain_source, random_config, random_storage_path, setup_bitcoind_and_electrsd,
27+
setup_builder, setup_node, setup_two_nodes, setup_two_nodes_with_store, splice_in_with_all,
28+
test_kv_list, test_kv_read, wait_for_tx, TestChainSource, TestStoreType, TestSyncStore,
2829
};
2930
use electrsd::corepc_node::Node as BitcoinD;
3031
use electrsd::ElectrsD;
@@ -39,6 +40,11 @@ use ldk_node::{Builder, Event, NodeError};
3940
use lightning::ln::channelmanager::PaymentId;
4041
use lightning::routing::gossip::{NodeAlias, NodeId};
4142
use lightning::routing::router::RouteParametersConfig;
43+
use lightning::util::persist::{
44+
CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
45+
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_KEY,
46+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
47+
};
4248
use lightning_invoice::{Bolt11InvoiceDescription, Description};
4349
use lightning_types::payment::{PaymentHash, PaymentPreimage};
4450
use log::LevelFilter;
@@ -52,6 +58,106 @@ async fn channel_full_cycle() {
5258
.await;
5359
}
5460

61+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
62+
async fn channel_full_cycle_tier_store() {
63+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
64+
let chain_source = random_chain_source(&bitcoind, &electrsd);
65+
let (primary_a, backup_a, ephemeral_a) = create_tier_stores(random_storage_path());
66+
let (primary_b, backup_b, ephemeral_b) = create_tier_stores(random_storage_path());
67+
68+
let (node_a, node_b) = setup_two_nodes_with_store(
69+
&chain_source,
70+
false,
71+
true,
72+
false,
73+
TestStoreType::TierStore {
74+
primary: primary_a.clone(),
75+
backup: Some(backup_a.clone()),
76+
ephemeral: Some(ephemeral_a.clone()),
77+
},
78+
TestStoreType::TierStore {
79+
primary: primary_b,
80+
backup: Some(backup_b),
81+
ephemeral: Some(ephemeral_b),
82+
},
83+
);
84+
do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false)
85+
.await;
86+
87+
// Verify primary and backup both contain the same channel manager data.
88+
let primary_channel_manager = test_kv_read(
89+
&(primary_a.clone()),
90+
CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
91+
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE,
92+
CHANNEL_MANAGER_PERSISTENCE_KEY,
93+
)
94+
.expect("Primary should have channel manager data");
95+
let backup_channel_manager = test_kv_read(
96+
&(backup_a.clone()),
97+
CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
98+
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE,
99+
CHANNEL_MANAGER_PERSISTENCE_KEY,
100+
)
101+
.expect("Backup should have channel manager data");
102+
assert_eq!(
103+
primary_channel_manager, backup_channel_manager,
104+
"Primary and backup should store identical channel manager data"
105+
);
106+
107+
// Verify payment data is persisted in both primary and backup stores.
108+
let primary_payments =
109+
test_kv_list(&(primary_a.clone()), "payments", "").expect("Primary should list payments");
110+
assert!(
111+
!primary_payments.is_empty(),
112+
"Primary should have payment entries after the full cycle"
113+
);
114+
115+
let backup_payments =
116+
test_kv_list(&(backup_a.clone()), "payments", "").expect("Backup should list payments");
117+
assert!(!backup_payments.is_empty(), "Backup should have payment entries after the full cycle");
118+
119+
// Verify ephemeral store does not contain primary-backed critical data.
120+
let ephemeral_channel_manager = test_kv_read(
121+
&(ephemeral_a.clone()),
122+
CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
123+
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE,
124+
CHANNEL_MANAGER_PERSISTENCE_KEY,
125+
);
126+
assert!(ephemeral_channel_manager.is_err(), "Ephemeral should not have channel manager data");
127+
128+
let ephemeral_payments = test_kv_list(&(ephemeral_a.clone()), "payments", "");
129+
assert!(
130+
ephemeral_payments.is_err() || ephemeral_payments.unwrap().is_empty(),
131+
"Ephemeral should not have payment data"
132+
);
133+
134+
// Verify ephemeral-routed data is stored in the ephemeral store.
135+
let ephemeral_network_graph = test_kv_read(
136+
&(ephemeral_a.clone()),
137+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
138+
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
139+
NETWORK_GRAPH_PERSISTENCE_KEY,
140+
);
141+
assert!(ephemeral_network_graph.is_ok(), "Ephemeral should have network graph data");
142+
143+
// Verify ephemeral-routed data is not mirrored to primary or backup stores.
144+
let primary_network_graph = test_kv_read(
145+
&(primary_a.clone()),
146+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
147+
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
148+
NETWORK_GRAPH_PERSISTENCE_KEY,
149+
);
150+
assert!(primary_network_graph.is_err(), "Primary should not have ephemeral network graph data");
151+
152+
let backup_network_graph = test_kv_read(
153+
&(backup_a.clone()),
154+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
155+
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
156+
NETWORK_GRAPH_PERSISTENCE_KEY,
157+
);
158+
assert!(backup_network_graph.is_err(), "Backup should not have ephemeral network graph data");
159+
}
160+
55161
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
56162
async fn channel_full_cycle_force_close() {
57163
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
@@ -237,8 +343,20 @@ async fn start_stop_reinit() {
237343
setup_builder!(builder, config.node_config);
238344
builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config));
239345

240-
let node =
241-
builder.build_with_store(config.node_entropy.into(), test_sync_store.clone()).unwrap();
346+
let node;
347+
#[cfg(feature = "uniffi")]
348+
{
349+
use ldk_node::{DynStoreWrapper, FfiDynStoreTrait};
350+
351+
let test_sync_store: Arc<dyn FfiDynStoreTrait> =
352+
Arc::new(DynStoreWrapper(test_sync_store.clone()));
353+
354+
node = builder.build_with_store(config.node_entropy.into(), test_sync_store).unwrap();
355+
}
356+
#[cfg(not(feature = "uniffi"))]
357+
{
358+
node = builder.build_with_store(config.node_entropy, test_sync_store.clone()).unwrap();
359+
}
242360
node.start().unwrap();
243361

244362
let expected_node_id = node.node_id();
@@ -276,8 +394,21 @@ async fn start_stop_reinit() {
276394
setup_builder!(builder, config.node_config);
277395
builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config));
278396

279-
let reinitialized_node =
280-
builder.build_with_store(config.node_entropy.into(), test_sync_store).unwrap();
397+
let reinitialized_node;
398+
#[cfg(feature = "uniffi")]
399+
{
400+
use ldk_node::{DynStoreWrapper, FfiDynStoreTrait};
401+
402+
let test_sync_store: Arc<dyn FfiDynStoreTrait> = Arc::new(DynStoreWrapper(test_sync_store));
403+
404+
reinitialized_node =
405+
builder.build_with_store(config.node_entropy.into(), test_sync_store).unwrap();
406+
}
407+
#[cfg(not(feature = "uniffi"))]
408+
{
409+
reinitialized_node =
410+
builder.build_with_store(config.node_entropy, test_sync_store).unwrap();
411+
}
281412
reinitialized_node.start().unwrap();
282413
assert_eq!(reinitialized_node.node_id(), expected_node_id);
283414

0 commit comments

Comments
 (0)