From 5c79d52d9d0bda13f0306fa5b206fd933f3ee579 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Thu, 11 Jun 2026 14:16:50 -0500 Subject: [PATCH 1/4] Update vss-client to 0.6 Switches vss-client-ng to the crates.io 0.6 release. Generated with OpenAI Codex. --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- src/io/vss_store.rs | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93c7cf59b..c9f15e61f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Compatibility Notes - Pending JIT-channel payments created before upgrading may fail after upgrade because the prior LSPS2 fee-limit state stored in `PaymentKind::Bolt11Jit` is not migrated. +- Users of the VSS storage backend must upgrade their VSS server to at least version + `v0.1.0-alpha.0` before upgrading LDK Node. # 0.7.0 - Dec. 3, 2025 This seventh minor release introduces numerous new features, bug fixes, and API improvements. In particular, it adds support for channel Splicing, Async Payments, as well as sourcing chain data from a Bitcoin Core REST backend. diff --git a/Cargo.toml b/Cargo.toml index bed984f07..aa9df0b18 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ async-trait = { version = "0.1", default-features = false } tokio-postgres = { version = "0.7", default-features = false, features = ["runtime"], optional = true } native-tls = { version = "0.2", default-features = false, optional = true } postgres-native-tls = { version = "0.5", default-features = false, features = ["runtime"], optional = true } -vss-client = { package = "vss-client-ng", version = "0.5" } +vss-client = { package = "vss-client-ng", version = "0.6" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", rev = "ff09ce9401afa448549a8f101172700bcd14d7bb" } diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 6c3535627..2d5db4e80 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -647,6 +647,12 @@ async fn determine_and_write_schema_version( // The value is not set. None }, + Err(VssError::VSSVersionMismatchError { version_served, version_expected }) => { + let msg = format!( + "VSS versiion mismatch, expected: {version_expected}, got: {version_served:?}" + ); + return Err(Error::new(ErrorKind::Other, msg)); + }, Err(e) => { let msg = format!("Failed to read schema version: {}", e); return Err(Error::new(ErrorKind::Other, msg)); From 74bb60363d1001d59811421170a927b0d0607bc9 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 14 Apr 2026 13:40:01 -0500 Subject: [PATCH 2/4] Extract build_vss_store test helper Move repeated VssStore construction logic into a shared build_vss_store() helper and have existing tests use it. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/io/vss_store.rs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 2d5db4e80..5c0308a59 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -947,34 +947,27 @@ mod tests { use super::*; use crate::io::test_utils::do_read_write_remove_list_persist; - #[tokio::test] - async fn vss_read_write_remove_list_persist() { + fn build_vss_store() -> VssStore { let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); let mut rng = rng(); let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); let mut node_seed = [0u8; 64]; rng.fill_bytes(&mut node_seed); let entropy = NodeEntropy::from_seed_bytes(node_seed); - let vss_store = - VssStoreBuilder::new(entropy, vss_base_url, rand_store_id, Network::Testnet) - .build_with_sigs_auth(HashMap::new()) - .unwrap(); + VssStoreBuilder::new(entropy, vss_base_url, rand_store_id, Network::Testnet) + .build_with_sigs_auth(HashMap::new()) + .unwrap() + } + + #[tokio::test] + async fn vss_read_write_remove_list_persist() { + let vss_store = build_vss_store(); do_read_write_remove_list_persist(&vss_store).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn vss_read_write_remove_list_persist_in_runtime_context() { - let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); - let mut rng = rng(); - let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); - let mut node_seed = [0u8; 64]; - rng.fill_bytes(&mut node_seed); - let entropy = NodeEntropy::from_seed_bytes(node_seed); - let vss_store = - VssStoreBuilder::new(entropy, vss_base_url, rand_store_id, Network::Testnet) - .build_with_sigs_auth(HashMap::new()) - .unwrap(); - + let vss_store = build_vss_store(); do_read_write_remove_list_persist(&vss_store).await; drop(vss_store) } From 7ebde03e45a5dd306c0bad5f138950308826cdcc Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 14 Apr 2026 14:47:15 -0500 Subject: [PATCH 3/4] Refactor list_all_keys into reusable list_keys Extract the single-page VSS listing logic into a list_keys method that accepts page_token and page_size parameters. list_internal now drives the pagination loop itself, calling list_keys per page. This prepares for PaginatedKVStore support which will reuse list_keys for single-page queries. This also fixes a potential issue where if the VSS server returned None for the page token we could enter into an infinite loop. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/io/vss_store.rs | 70 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 5c0308a59..67509db53 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -391,35 +391,34 @@ impl VssStoreInner { } } - async fn list_all_keys( + async fn list_keys( &self, client: &VssClient, primary_namespace: &str, - secondary_namespace: &str, - ) -> io::Result> { - let mut page_token = None; - let mut keys = vec![]; + secondary_namespace: &str, page_token: Option, page_size: Option, + ) -> io::Result<(Vec, Option)> { let key_prefix = self.build_obfuscated_prefix(primary_namespace, secondary_namespace); - while page_token != Some("".to_string()) { - let request = ListKeyVersionsRequest { - store_id: self.store_id.clone(), - key_prefix: Some(key_prefix.clone()), - page_token, - page_size: None, - }; + let request = ListKeyVersionsRequest { + store_id: self.store_id.clone(), + key_prefix: Some(key_prefix), + page_token, + page_size, + }; - let response = client.list_key_versions(&request).await.map_err(|e| { - let msg = format!( - "Failed to list keys in {}/{}: {}", - primary_namespace, secondary_namespace, e - ); - Error::new(ErrorKind::Other, msg) - })?; + let response = client.list_key_versions(&request).await.map_err(|e| { + let msg = format!( + "Failed to list keys in {}/{}: {}", + primary_namespace, secondary_namespace, e + ); + Error::new(ErrorKind::Other, msg) + })?; - for kv in response.key_versions { - keys.push(self.extract_key(&kv.key)?); - } - page_token = response.next_page_token; + let mut keys = Vec::with_capacity(response.key_versions.len()); + for kv in response.key_versions { + keys.push(self.extract_key(&kv.key)?); } - Ok(keys) + + // VSS may return an empty string instead of None to signal the last page. + let next_page_token = response.next_page_token.filter(|t| !t.is_empty()); + Ok((keys, next_page_token)) } async fn read_internal( @@ -543,17 +542,18 @@ impl VssStoreInner { ) -> io::Result> { check_namespace_key_validity(&primary_namespace, &secondary_namespace, None, "list")?; - let keys = self - .list_all_keys(client, &primary_namespace, &secondary_namespace) - .await - .map_err(|e| { - let msg = format!( - "Failed to retrieve keys in namespace: {}/{} : {}", - primary_namespace, secondary_namespace, e - ); - Error::new(ErrorKind::Other, msg) - })?; - + let mut page_token: Option = None; + let mut keys = vec![]; + loop { + let (page_keys, next_page_token) = self + .list_keys(client, &primary_namespace, &secondary_namespace, page_token, None) + .await?; + keys.extend(page_keys); + match next_page_token { + Some(t) => page_token = Some(t), + None => break, + } + } Ok(keys) } From f51bf20eaafac7f9dbf1b45ac8b2934da974f1cb Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 14 Apr 2026 14:54:24 -0500 Subject: [PATCH 4/4] Add PaginatedKVStore support to VssStore --- src/io/vss_store.rs | 138 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 67509db53..cfaf3eaf6 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -24,7 +24,7 @@ use bitcoin::Network; use lightning::impl_writeable_tlv_based_enum; use lightning::io::{self, Error, ErrorKind}; use lightning::sign::{EntropySource as LdkEntropySource, RandomBytes}; -use lightning::util::persist::KVStore; +use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse}; use lightning::util::ser::{Readable, Writeable}; use prost::Message; use vss_client::client::VssClient; @@ -293,6 +293,32 @@ impl KVStore for VssStore { } } +impl PaginatedKVStore for VssStore { + fn list_paginated( + &self, primary_namespace: &str, secondary_namespace: &str, page_token: Option, + ) -> impl Future> + 'static + Send { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let inner = Arc::clone(&self.inner); + let runtime = self.internal_runtime(); + async move { + let task = runtime.spawn(async move { + inner + .list_paginated_internal( + &inner.async_client, + primary_namespace, + secondary_namespace, + page_token, + ) + .await + }); + task.await.map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("VSS runtime task failed: {}", e)) + })? + } + } +} + impl Drop for VssStore { fn drop(&mut self) { if let Some(runtime) = self.internal_runtime.take() { @@ -557,6 +583,35 @@ impl VssStoreInner { Ok(keys) } + async fn list_paginated_internal( + &self, client: &VssClient, primary_namespace: String, + secondary_namespace: String, page_token: Option, + ) -> io::Result { + check_namespace_key_validity( + &primary_namespace, + &secondary_namespace, + None, + "list_paginated", + )?; + + const PAGE_SIZE: i32 = 50; + + let vss_page_token = page_token.map(|t| t.to_string()); + let (keys, next_page_token) = self + .list_keys( + client, + &primary_namespace, + &secondary_namespace, + vss_page_token, + Some(PAGE_SIZE), + ) + .await?; + + let next_page_token = next_page_token.map(PageToken::new); + + Ok(PaginatedListResponse { keys, next_page_token }) + } + async fn execute_locked_write< F: Future>, FN: FnOnce() -> F, @@ -971,4 +1026,85 @@ mod tests { do_read_write_remove_list_persist(&vss_store).await; drop(vss_store) } + + #[tokio::test] + async fn vss_paginated_listing() { + let store = build_vss_store(); + let ns = "test_paginated"; + let sub = "listing"; + let num_entries = 5; + + for i in 0..num_entries { + let key = format!("key_{:04}", i); + let data = vec![i as u8; 32]; + KVStore::write(&store, ns, sub, &key, data).await.unwrap(); + } + + let mut all_keys = Vec::new(); + let mut page_token = None; + + loop { + let response = + PaginatedKVStore::list_paginated(&store, ns, sub, page_token).await.unwrap(); + all_keys.extend(response.keys); + match response.next_page_token { + Some(token) => page_token = Some(token), + _ => break, + } + } + + assert_eq!(all_keys.len(), num_entries); + + // Verify no duplicates + let mut unique = all_keys.clone(); + unique.sort(); + unique.dedup(); + assert_eq!(unique.len(), num_entries); + } + + #[tokio::test] + async fn vss_paginated_empty_namespace() { + let store = build_vss_store(); + let response = + PaginatedKVStore::list_paginated(&store, "nonexistent", "ns", None).await.unwrap(); + assert!(response.keys.is_empty()); + assert!(response.next_page_token.is_none()); + } + + #[tokio::test] + async fn vss_paginated_removal() { + let store = build_vss_store(); + let ns = "test_paginated"; + let sub = "removal"; + + KVStore::write(&store, ns, sub, "a", vec![1u8; 8]).await.unwrap(); + KVStore::write(&store, ns, sub, "b", vec![2u8; 8]).await.unwrap(); + KVStore::write(&store, ns, sub, "c", vec![3u8; 8]).await.unwrap(); + + KVStore::remove(&store, ns, sub, "b", false).await.unwrap(); + + let response = PaginatedKVStore::list_paginated(&store, ns, sub, None).await.unwrap(); + assert_eq!(response.keys.len(), 2); + assert!(response.keys.contains(&"a".to_string())); + assert!(!response.keys.contains(&"b".to_string())); + assert!(response.keys.contains(&"c".to_string())); + } + + #[tokio::test] + async fn vss_paginated_namespace_isolation() { + let store = build_vss_store(); + + KVStore::write(&store, "ns_a", "sub", "key_1", vec![1u8; 8]).await.unwrap(); + KVStore::write(&store, "ns_a", "sub", "key_2", vec![2u8; 8]).await.unwrap(); + KVStore::write(&store, "ns_b", "sub", "key_3", vec![3u8; 8]).await.unwrap(); + + let response = PaginatedKVStore::list_paginated(&store, "ns_a", "sub", None).await.unwrap(); + assert_eq!(response.keys.len(), 2); + assert!(response.keys.contains(&"key_1".to_string())); + assert!(response.keys.contains(&"key_2".to_string())); + + let response = PaginatedKVStore::list_paginated(&store, "ns_b", "sub", None).await.unwrap(); + assert_eq!(response.keys.len(), 1); + assert!(response.keys.contains(&"key_3".to_string())); + } }