From 8e1ff9a6e9c178ccf0285541af8688bc8ea84b71 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Mon, 25 May 2026 16:49:30 +0000 Subject: [PATCH 1/2] Add `vss_client_v050_compatibility` test We recently added the requirement that VSS servers return key-version lists in creation order. We also added a version header to all VSS responses such that clients requiring creation-order lists can ensure the VSS server they talk to provides this guarantee. These VSS server changes remain fully backwards compatible with previously-released VSS clients. Here we add a test that ensures that this is true against the last VSS client release made prior to these changes. This new test also makes sure that any future changes to VSS server remain backwards compatible with VSS client v0.5.0. AI wrote the test. --- ...mpl_tests.yml => implementation-tests.yml} | 3 +- ...ion.yml => ldk-node-integration-tests.yml} | 2 +- ...ild-and-deploy-rust.yml => ping-tests.yml} | 5 +- .github/workflows/server-tests.yml | 46 +++ rust/Cargo.lock | 182 +++++++++- rust/server/Cargo.toml | 3 + .../tests/vss_client_v050_compatibility.rs | 326 ++++++++++++++++++ 7 files changed, 559 insertions(+), 8 deletions(-) rename .github/workflows/{impl_tests.yml => implementation-tests.yml} (94%) rename .github/workflows/{ldk-node-integration.yml => ldk-node-integration-tests.yml} (98%) rename .github/workflows/{build-and-deploy-rust.yml => ping-tests.yml} (96%) create mode 100644 .github/workflows/server-tests.yml create mode 100644 rust/server/tests/vss_client_v050_compatibility.rs diff --git a/.github/workflows/impl_tests.yml b/.github/workflows/implementation-tests.yml similarity index 94% rename from .github/workflows/impl_tests.yml rename to .github/workflows/implementation-tests.yml index d740a2f6..20a116eb 100644 --- a/.github/workflows/impl_tests.yml +++ b/.github/workflows/implementation-tests.yml @@ -7,13 +7,14 @@ concurrency: cancel-in-progress: true jobs: - test-postgres-backend: + postgres-backend-tests: strategy: fail-fast: false matrix: platform: [ ubuntu-latest ] toolchain: [ stable, 1.85.0 ] # 1.85.0 is the MSRV runs-on: ${{ matrix.platform }} + timeout-minutes: 15 services: postgres: diff --git a/.github/workflows/ldk-node-integration.yml b/.github/workflows/ldk-node-integration-tests.yml similarity index 98% rename from .github/workflows/ldk-node-integration.yml rename to .github/workflows/ldk-node-integration-tests.yml index 73ce8202..6dcec98b 100644 --- a/.github/workflows/ldk-node-integration.yml +++ b/.github/workflows/ldk-node-integration-tests.yml @@ -7,7 +7,7 @@ concurrency: cancel-in-progress: true jobs: - build-and-test: + ldk-node-integration-tests: strategy: fail-fast: false matrix: diff --git a/.github/workflows/build-and-deploy-rust.yml b/.github/workflows/ping-tests.yml similarity index 96% rename from .github/workflows/build-and-deploy-rust.yml rename to .github/workflows/ping-tests.yml index 1cc7aefa..07c2d545 100644 --- a/.github/workflows/build-and-deploy-rust.yml +++ b/.github/workflows/ping-tests.yml @@ -1,4 +1,4 @@ -name: Ping Check +name: Ping Tests on: [push, pull_request] @@ -7,13 +7,14 @@ concurrency: cancel-in-progress: true jobs: - ping-check: + ping-tests: strategy: fail-fast: false matrix: platform: [ ubuntu-latest ] toolchain: [ stable, 1.85.0 ] # 1.85.0 is the MSRV runs-on: ${{ matrix.platform }} + timeout-minutes: 15 services: postgres: diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml new file mode 100644 index 00000000..0ed87df0 --- /dev/null +++ b/.github/workflows/server-tests.yml @@ -0,0 +1,46 @@ +name: Server Tests + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + server-tests: + strategy: + fail-fast: false + matrix: + platform: [ ubuntu-latest ] + toolchain: [ stable, 1.85.0 ] # 1.85.0 is the MSRV + runs-on: ${{ matrix.platform }} + timeout-minutes: 15 + + services: + postgres: + image: postgres:latest + ports: + - 5432:5432 + env: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Build and Deploy VSS Server + run: | + cd rust + RUSTFLAGS="--cfg noop_authorizer" cargo build --no-default-features + ./target/debug/vss-server server/vss-server-config.toml& + - name: Run the server tests + run: | + sleep 5 + cd rust/server + cargo test diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9c67c152..0aab3dfa 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -71,7 +71,7 @@ dependencies = [ "hex-conservative 1.0.1", "jsonwebtoken", "openssl", - "secp256k1", + "secp256k1 0.31.1", "serde", "serde_json", "tokio", @@ -83,21 +83,60 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals 0.3.0", + "bitcoin_hashes 0.14.1", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bitcoin" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf93e61f2dbc3e3c41234ca26a65e2c0b0975c52e0f069ab9893ebbede584d3" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-internals 0.3.0", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes 0.14.1", + "hex-conservative 0.2.2", + "hex_lit", + "secp256k1 0.29.1", +] + [[package]] name = "bitcoin-consensus-encoding" version = "1.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd69023e5db2f3f7241672de6be29408373ba0ff407e7fda71d70d728bec05a" dependencies = [ - "bitcoin-internals", + "bitcoin-internals 0.5.0", ] +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" + [[package]] name = "bitcoin-internals" version = "0.5.0" @@ -110,6 +149,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" +[[package]] +name = "bitcoin-units" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346568ebaab2918487cea76dd55dae13c27bb618cdb737c952e69eb2017c4118" +dependencies = [ + "bitcoin-internals 0.3.0", +] + [[package]] name = "bitcoin_hashes" version = "0.14.1" @@ -127,7 +175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aaf7add9aa250546d4d7a0ad0755a25327f5205dc2d7eba6b6ec08cd864c79e" dependencies = [ "bitcoin-consensus-encoding", - "bitcoin-internals", + "bitcoin-internals 0.5.0", ] [[package]] @@ -136,6 +184,19 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitreq" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df90cd78f0510165fd370574676aeb57dbec0ee3bfff68645bb7b0e9a65dbd5" +dependencies = [ + "rustls", + "rustls-webpki", + "tokio", + "tokio-rustls", + "webpki-roots", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -179,6 +240,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20-poly1305" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b0fc281743d80256607bd65e8beedc42cb0787ea119c85b81b4c0eab85e5f" + [[package]] name = "chrono" version = "0.4.43" @@ -411,6 +478,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366fa3443ac84474447710ec17bb00b05dfbd096137817981e86f992f21a2793" +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "hmac" version = "0.12.1" @@ -1149,6 +1222,40 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1170,6 +1277,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes 0.14.1", + "rand 0.8.5", + "secp256k1-sys 0.10.1", +] + [[package]] name = "secp256k1" version = "0.31.1" @@ -1178,7 +1296,16 @@ checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" dependencies = [ "bitcoin_hashes 0.14.1", "rand 0.9.2", - "secp256k1-sys", + "secp256k1-sys 0.11.0", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", ] [[package]] @@ -1507,6 +1634,16 @@ dependencies = [ "whoami", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -1605,6 +1742,27 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vss-client-ng" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6334cb4940aba86a2e2aa9dde7c722a2510f55815422088a2a2ac24f46579e6a" +dependencies = [ + "async-trait", + "base64", + "bitcoin", + "bitcoin_hashes 0.14.1", + "bitreq", + "chacha20-poly1305", + "log", + "prost", + "prost-build", + "rand 0.8.5", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "vss-server" version = "0.1.0" @@ -1623,6 +1781,7 @@ dependencies = [ "serde", "tokio", "toml", + "vss-client-ng", ] [[package]] @@ -1713,6 +1872,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -1995,6 +2163,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zmij" version = "1.0.19" diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index c3a44698..ecb6c817 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -29,5 +29,8 @@ rand = { version = "0.9.2", default-features = false } [target.'cfg(noop_authorizer)'.dependencies] api = { path = "../api", features = ["_test_utils"] } +[dev-dependencies] +vss-client-v050 = { package = "vss-client-ng", version = "=0.5.0" } + [lints] workspace = true diff --git a/rust/server/tests/vss_client_v050_compatibility.rs b/rust/server/tests/vss_client_v050_compatibility.rs new file mode 100644 index 00000000..64fd17da --- /dev/null +++ b/rust/server/tests/vss_client_v050_compatibility.rs @@ -0,0 +1,326 @@ +//! Compatibility shakedown for the pinned vss-client-ng v0.5.0 dependency against current +//! vss-server master. This test assumes a no-auth VSS server is already running at +//! `localhost:8080` and exercises a full client lifecycle through the public v0.5.0 client API: +//! empty listing, missing-key reads, conditional and non-conditional writes, gets, conflict +//! handling, transactional put/delete, direct deletes, paginated listing, and cleanup. + +use std::collections::{BTreeMap, BTreeSet}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use vss_client_v050::client::VssClient; +use vss_client_v050::error::VssError; +use vss_client_v050::types::{ + DeleteObjectRequest, GetObjectRequest, GetObjectResponse, KeyValue, ListKeyVersionsRequest, + PutObjectRequest, +}; +use vss_client_v050::util::retry::{ExponentialBackoffRetryPolicy, RetryPolicy}; + +const VSS_SERVER_BASE_URL: &str = "http://localhost:8080/vss"; +const KEY_ALPHA: &str = "compat/alpha"; +const KEY_BETA: &str = "compat/beta"; +const KEY_DELTA: &str = "compat/delta"; +const KEY_EPSILON: &str = "compat/epsilon"; +const KEY_GAMMA: &str = "compat/gamma"; +const KEY_OUTSIDE_PREFIX: &str = "outside-prefix"; +const KEY_STALE_GLOBAL: &str = "compat/stale-global"; +const KEY_THETA: &str = "compat/theta"; +const KEY_PREFIX: &str = "compat/"; +const GLOBAL_VERSION_KEY: &str = "global_version"; +const LIST_PAGE_SIZE: i32 = 2; + +#[tokio::test] +async fn test_vss_client_v050_compatibility() -> Result<(), VssError> { + let client = VssClient::new(VSS_SERVER_BASE_URL.to_string(), retry_policy()); + let store_id = unique_store_id(); + let mut global_version = 0; + + let empty_list = + client.list_key_versions(&list_request(&store_id, None, Some(10), None)).await?; + // A new store should report the initial global version. + assert_eq!(empty_list.global_version, Some(global_version)); + // A new store should not contain any key-version entries. + assert!(empty_list.key_versions.is_empty()); + // An empty result set should also be the final page. + assert_eq!(empty_list.next_page_token.as_deref(), Some("")); + + // Reading a key that has never been written should surface the protocol's missing-key error. + assert_no_such_key(client.get_object(&get_request(&store_id, "missing")).await, "missing"); + + client + .put_object(&put_request( + &store_id, + Some(global_version), + vec![kv(KEY_ALPHA, 0, b"alpha-v1"), kv(KEY_BETA, 0, b"beta-v1")], + vec![], + )) + .await?; + global_version += 1; + + // The first conditional write should make alpha readable at server-side version 1. + assert_key_value(&client, &store_id, KEY_ALPHA, 1, b"alpha-v1").await?; + // The first conditional write should make beta readable at server-side version 1. + assert_key_value(&client, &store_id, KEY_BETA, 1, b"beta-v1").await?; + + client + .put_object(&put_request( + &store_id, + Some(global_version), + vec![ + kv(KEY_ALPHA, 1, b"alpha-v2"), + kv(KEY_GAMMA, 0, b"gamma-v1"), + kv(KEY_OUTSIDE_PREFIX, 0, b"outside-prefix-v1"), + ], + vec![], + )) + .await?; + global_version += 1; + + // Updating alpha with the matching key version should advance alpha to version 2. + assert_key_value(&client, &store_id, KEY_ALPHA, 2, b"alpha-v2").await?; + // Creating gamma in the same request should make it readable at version 1. + assert_key_value(&client, &store_id, KEY_GAMMA, 1, b"gamma-v1").await?; + + let stale_put = client + .put_object(&put_request( + &store_id, + Some(global_version), + vec![kv(KEY_ALPHA, 1, b"stale-alpha")], + vec![], + )) + .await; + // Reusing alpha's old key version should be rejected as a conflict. + assert_conflict(stale_put); + // The rejected stale write must not change alpha's committed value. + assert_key_value(&client, &store_id, KEY_ALPHA, 2, b"alpha-v2").await?; + + let stale_global_version_put = client + .put_object(&put_request( + &store_id, + Some(global_version - 1), + vec![kv(KEY_STALE_GLOBAL, 0, b"stale-global-version")], + vec![], + )) + .await; + // Reusing an old global version should be rejected independently of key-level versions. + assert_conflict(stale_global_version_put); + // A failed global-version write must not create the requested key. + assert_no_such_key( + client.get_object(&get_request(&store_id, KEY_STALE_GLOBAL)).await, + KEY_STALE_GLOBAL, + ); + + client + .put_object(&put_request( + &store_id, + Some(global_version), + vec![kv(KEY_DELTA, -1, b"delta-v1")], + vec![], + )) + .await?; + global_version += 1; + + client + .put_object(&put_request( + &store_id, + Some(global_version), + vec![kv(KEY_DELTA, -1, b"delta-v2")], + vec![], + )) + .await?; + global_version += 1; + + // Non-conditional writes should reset the server-side key version to 1 and keep the last value. + assert_key_value(&client, &store_id, KEY_DELTA, 1, b"delta-v2").await?; + + client + .put_object(&put_request( + &store_id, + Some(global_version), + vec![kv(KEY_THETA, 0, b"theta-v1")], + vec![kv(KEY_BETA, 1, b"")], + )) + .await?; + global_version += 1; + + // A transaction mixing a put and delete should commit the put side. + assert_key_value(&client, &store_id, KEY_THETA, 1, b"theta-v1").await?; + // The same transaction should remove beta atomically. + assert_no_such_key(client.get_object(&get_request(&store_id, KEY_BETA)).await, KEY_BETA); + + client.delete_object(&delete_request(&store_id, KEY_GAMMA, 1)).await?; + client.delete_object(&delete_request(&store_id, KEY_GAMMA, 1)).await?; + // Repeating a direct delete should leave gamma deleted and exercise delete idempotency. + assert_no_such_key(client.get_object(&get_request(&store_id, KEY_GAMMA)).await, KEY_GAMMA); + + client + .put_object(&put_request(&store_id, None, vec![kv(KEY_EPSILON, 0, b"epsilon-v1")], vec![])) + .await?; + // A write without global-version checking should still create the key at version 1. + assert_key_value(&client, &store_id, KEY_EPSILON, 1, b"epsilon-v1").await?; + + let listed_versions = + list_all_key_versions(&client, &store_id, Some(KEY_PREFIX), global_version).await?; + let listed_keys: BTreeSet<&str> = listed_versions.keys().map(String::as_str).collect(); + // Prefix listing should include only the live keys under compat/ after deletes and conflicts. + assert_eq!(listed_keys, BTreeSet::from([KEY_ALPHA, KEY_DELTA, KEY_EPSILON, KEY_THETA])); + // Listing should report alpha's latest key version. + assert_eq!(listed_versions[KEY_ALPHA], 2); + // Listing should report delta's non-conditional write version. + assert_eq!(listed_versions[KEY_DELTA], 1); + // Listing should report epsilon's no-global-version write version. + assert_eq!(listed_versions[KEY_EPSILON], 1); + // Listing should report theta's transactional write version. + assert_eq!(listed_versions[KEY_THETA], 1); + + let cleanup_keys = + [KEY_ALPHA, KEY_DELTA, KEY_EPSILON, KEY_THETA, KEY_OUTSIDE_PREFIX, GLOBAL_VERSION_KEY]; + for key in cleanup_keys { + client.delete_object(&delete_request(&store_id, key, -1)).await?; + } + + let final_list = + client.list_key_versions(&list_request(&store_id, None, Some(10), None)).await?; + // Deleting the protocol global-version key should make the store report the default version. + assert_eq!(final_list.global_version, Some(0)); + // Cleanup should leave no key-version entries behind for this store. + assert!(final_list.key_versions.is_empty()); + // Cleanup should leave the final list response on its last page. + assert_eq!(final_list.next_page_token.as_deref(), Some("")); + + Ok(()) +} + +fn retry_policy() -> impl RetryPolicy { + ExponentialBackoffRetryPolicy::new(Duration::from_millis(10)).with_max_attempts(1) +} + +fn unique_store_id() -> String { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock must be after UNIX epoch") + .as_nanos(); + format!("v050-compat-{nanos}") +} + +fn get_request(store_id: &str, key: &str) -> GetObjectRequest { + GetObjectRequest { store_id: store_id.to_string(), key: key.to_string() } +} + +fn put_request( + store_id: &str, global_version: Option, transaction_items: Vec, + delete_items: Vec, +) -> PutObjectRequest { + PutObjectRequest { + store_id: store_id.to_string(), + global_version, + transaction_items, + delete_items, + } +} + +fn delete_request(store_id: &str, key: &str, version: i64) -> DeleteObjectRequest { + DeleteObjectRequest { store_id: store_id.to_string(), key_value: Some(kv(key, version, b"")) } +} + +fn list_request( + store_id: &str, page_token: Option, page_size: Option, key_prefix: Option<&str>, +) -> ListKeyVersionsRequest { + ListKeyVersionsRequest { + store_id: store_id.to_string(), + key_prefix: key_prefix.map(str::to_string), + page_size, + page_token, + } +} + +fn kv(key: &str, version: i64, value: &[u8]) -> KeyValue { + KeyValue { key: key.to_string(), version, value: value.to_vec() } +} + +async fn assert_key_value( + client: &VssClient>, store_id: &str, key: &str, + expected_version: i64, expected_value: &[u8], +) -> Result<(), VssError> { + let response = client.get_object(&get_request(store_id, key)).await?; + let value = response_value(response, key); + // The server must echo the requested key in a successful get response. + assert_eq!(value.key, key); + // The key-level version must match the lifecycle step's expected version. + assert_eq!(value.version, expected_version); + // The stored bytes must round-trip unchanged through the v0.5.0 client. + assert_eq!(value.value, expected_value); + Ok(()) +} + +fn response_value(response: GetObjectResponse, key: &str) -> KeyValue { + // A successful get response must include a KeyValue payload. + response.value.unwrap_or_else(|| panic!("expected GetObjectResponse to include {key}")) +} + +fn assert_no_such_key(result: Result, key: &str) { + match result { + // The expected protocol error is the only accepted missing-key outcome. + Err(VssError::NoSuchKeyError(_)) => {}, + // Any other error would indicate the request failed for the wrong reason. + Err(e) => panic!("expected {key} to be missing, got {e}"), + // A successful get would mean the key unexpectedly exists. + Ok(_) => panic!("expected {key} to be missing"), + } +} + +fn assert_conflict(result: Result) { + match result { + // The expected protocol error is the only accepted conflict outcome. + Err(VssError::ConflictError(_)) => {}, + // Any other error would indicate the rejected write failed for the wrong reason. + Err(e) => panic!("expected conflict error, got {e}"), + // A successful write would mean conflict detection is not working. + Ok(_) => panic!("expected conflict error"), + } +} + +async fn list_all_key_versions( + client: &VssClient>, store_id: &str, key_prefix: Option<&str>, + expected_global_version: i64, +) -> Result, VssError> { + let mut page_token = None; + let mut key_versions = BTreeMap::new(); + let mut page_count = 0; + + loop { + let page = client + .list_key_versions(&list_request( + store_id, + page_token.take(), + Some(LIST_PAGE_SIZE), + key_prefix, + )) + .await?; + // Each paginated response must honor the requested maximum page size. + assert!(page.key_versions.len() <= LIST_PAGE_SIZE as usize); + + if page_count == 0 { + // Only the first page should include the store's global version. + assert_eq!(page.global_version, Some(expected_global_version)); + } else { + // Follow-up pages should omit the global version per the VSS protocol. + assert!(page.global_version.is_none()); + } + page_count += 1; + + for key_value in page.key_versions { + // List responses should include only key/version metadata, not stored values. + assert!(key_value.value.is_empty()); + key_versions.insert(key_value.key, key_value.version); + } + + match page.next_page_token { + Some(token) if !token.is_empty() => page_token = Some(token), + _ => break, + } + } + + // With four matching keys and a page size of two, this path must exercise pagination. + assert!(page_count > 1); + Ok(key_versions) +} From 39e1cde770286107302ea6bed1c03ee48a56c7ef Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Mon, 25 May 2026 19:47:00 +0000 Subject: [PATCH 2/2] Fix CI to actually run rust stable and MSRV --- .github/workflows/implementation-tests.yml | 4 ++++ .github/workflows/ldk-node-integration-tests.yml | 9 +++++++++ .github/workflows/ping-tests.yml | 6 +++++- .github/workflows/server-tests.yml | 4 ++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/implementation-tests.yml b/.github/workflows/implementation-tests.yml index 20a116eb..4aa629c7 100644 --- a/.github/workflows/implementation-tests.yml +++ b/.github/workflows/implementation-tests.yml @@ -36,6 +36,10 @@ jobs: uses: actions/checkout@v3 with: path: vss-server + - name: Install Rust toolchain + run: | + rustup toolchain install ${{ matrix.toolchain }} --profile minimal + rustup default ${{ matrix.toolchain }} - name: Run postgres backend test suite run: | diff --git a/.github/workflows/ldk-node-integration-tests.yml b/.github/workflows/ldk-node-integration-tests.yml index 6dcec98b..707905c5 100644 --- a/.github/workflows/ldk-node-integration-tests.yml +++ b/.github/workflows/ldk-node-integration-tests.yml @@ -41,12 +41,21 @@ jobs: with: repository: lightningdevkit/ldk-node path: ldk-node + - name: Install Rust toolchain + run: | + rustup toolchain install ${{ matrix.toolchain }} --profile minimal + rustup default ${{ matrix.toolchain }} - name: Build and Deploy VSS Server run: | cd vss-server/rust RUSTFLAGS="--cfg noop_authorizer" cargo build --no-default-features ./target/debug/vss-server server/vss-server-config.toml& + - name: Pin packages to allow for MSRV + if: "matrix.toolchain == '1.85.0'" + run: | + cd ldk-node + cargo update -p idna_adapter --precise "1.2.0" --verbose # idna_adapter 1.2.1 uses ICU4X 2.2.0, requiring 1.86 and newer - name: Run LDK Node Integration tests run: | cd ldk-node diff --git a/.github/workflows/ping-tests.yml b/.github/workflows/ping-tests.yml index 07c2d545..10b7a22f 100644 --- a/.github/workflows/ping-tests.yml +++ b/.github/workflows/ping-tests.yml @@ -34,8 +34,12 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + - name: Install Rust toolchain + run: | + rustup toolchain install ${{ matrix.toolchain }} --profile minimal --component rustfmt + rustup default ${{ matrix.toolchain }} - name: Check formatting - run: rustup component add rustfmt && cd rust && cargo fmt --all -- --check + run: cd rust && cargo fmt --all -- --check - name: Build and Deploy VSS Server run: | cd rust diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 0ed87df0..c90b2dd3 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -34,6 +34,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + - name: Install Rust toolchain + run: | + rustup toolchain install ${{ matrix.toolchain }} --profile minimal + rustup default ${{ matrix.toolchain }} - name: Build and Deploy VSS Server run: | cd rust