From 0a9be6444feb12bafa8e8b660fc65ac54caeb1b5 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. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" } From d8c98a3f2a48e77ed3b59dd1bd55bcd2cf204a1e 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 6c3535627..9650d44aa 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -941,34 +941,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 81e9f9f79acb2aab92d9f86f09aa63a33e2c8a6f 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 | 68 ++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 9650d44aa..4ab03e12b 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -391,35 +391,32 @@ 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) + + Ok((keys, response.next_page_token)) } async fn read_internal( @@ -543,17 +540,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) if !t.is_empty() => page_token = Some(t), + _ => break, + } + } Ok(keys) } From fd809c2ad127d285866f73141355b0a14e22ee69 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 | 146 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 4 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 4ab03e12b..d96e11f1d 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() { @@ -416,7 +442,9 @@ impl VssStoreInner { keys.push(self.extract_key(&kv.key)?); } - Ok((keys, response.next_page_token)) + // 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( @@ -548,13 +576,42 @@ impl VssStoreInner { .await?; keys.extend(page_keys); match next_page_token { - Some(t) if !t.is_empty() => page_token = Some(t), - _ => break, + Some(t) => page_token = Some(t), + None => break, } } 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, @@ -963,4 +1020,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())); + } }