From 283af94d47e3dc50d83adf6ad92a81df55f017d7 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Tue, 19 May 2026 17:10:16 +0000 Subject: [PATCH 1/2] Read `vss.proto` from the local repository We now read `vss.proto` from the local repository instead of downloading it from a specific commit on github. We will use protocol versioning, tags, and releases to coordinate clients and servers. --- Cargo.toml | 1 - build.rs | 18 +----------------- src/types.rs | 4 +++- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 40c6077..60bff38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ log = { version = "0.4.29", default-features = false, features = ["std"]} [target.'cfg(genproto)'.build-dependencies] prost-build = { version = "0.11.3" } -bitreq = { version = "0.3", default-features = false, features = ["std", "https"] } [dev-dependencies] mockito = "0.28.0" diff --git a/build.rs b/build.rs index e3254cd..9324369 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,7 @@ #[cfg(genproto)] extern crate prost_build; #[cfg(genproto)] -use std::io::Write; -#[cfg(genproto)] -use std::{env, fs, fs::File, path::Path}; +use std::{env, fs, path::Path}; /// To generate updated proto objects: /// 1. Place `vss.proto` file in `src/proto/` @@ -15,21 +13,7 @@ fn main() { #[cfg(genproto)] fn generate_protos() { - download_file( - "https://raw.githubusercontent.com/lightningdevkit/vss-server/022ee5e92debb60516438af0a369966495bfe595/proto/vss.proto", - "src/proto/vss.proto", - ).unwrap(); - prost_build::compile_protos(&["src/proto/vss.proto"], &["src/"]).unwrap(); let from_path = Path::new(&env::var("OUT_DIR").unwrap()).join("vss.rs"); fs::copy(from_path, "src/types.rs").unwrap(); } - -#[cfg(genproto)] -fn download_file(url: &str, save_to: &str) -> Result<(), Box> { - let response = bitreq::get(url).send()?; - fs::create_dir_all(Path::new(save_to).parent().unwrap())?; - let mut out_file = File::create(save_to)?; - out_file.write_all(&response.into_bytes())?; - Ok(()) -} diff --git a/src/types.rs b/src/types.rs index ace954c..cdcc417 100644 --- a/src/types.rs +++ b/src/types.rs @@ -212,7 +212,7 @@ pub struct ListKeyVersionsRequest { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListKeyVersionsResponse { - /// Fetched keys and versions. + /// Fetched keys and versions, ordered by creation time (newest first). /// Even though this API reuses the `KeyValue` struct, the `value` sub-field will not be set by the server. #[prost(message, repeated, tag = "1")] pub key_versions: ::prost::alloc::vec::Vec, @@ -220,6 +220,8 @@ pub struct ListKeyVersionsResponse { /// Use this value to query for next-page of paginated `ListKeyVersions` operation, by specifying /// this value as the `page_token` in the next request. /// + /// Following AIP-158 (): + /// /// If `next_page_token` is empty (""), then the "last page" of results has been processed and /// there is no more data to be retrieved. /// From c55fa2946d1aea698fe117244e55926cc086b7b8 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Tue, 19 May 2026 18:24:38 +0000 Subject: [PATCH 2/2] Assert the VSS protocol version matches We assert that the VSS protocol version matches on all responses returned from the server, including errors. --- Cargo.toml | 2 +- src/client.rs | 12 ++++++++++++ src/error.rs | 23 ++++++++++++++++++++++ tests/tests.rs | 52 +++++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60bff38..84ea8e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vss-client-ng" -version = "0.5.0" +version = "0.6.0" authors = ["Leo Nash ", "Elias Rohrer "] rust-version = "1.75.0" license = "MIT OR Apache-2.0" diff --git a/src/client.rs b/src/client.rs index 2b195a0..e245ced 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,6 +20,8 @@ const CONTENT_TYPE: &str = "content-type"; const DEFAULT_TIMEOUT_SECS: u64 = 10; const MAX_RESPONSE_BODY_SIZE: usize = 1024 * 1024 * 1024; // 1GB const DEFAULT_CLIENT_CAPACITY: usize = 10; +const PROTOCOL_VERSION_HEADER: &str = "vss-protocol-version"; +const PROTOCOL_VERSION: &str = "0"; /// Thin-client to access a hosted instance of Versioned Storage Service (VSS). /// The provided [`VssClient`] API is minimalistic and is congruent to the VSS server-side API. @@ -212,6 +214,16 @@ impl> VssClient { } let response = self.client.send_async(http_request).await?; + // Return early in case of version mismatch, this issue must be solved first. + if response.headers.get(PROTOCOL_VERSION_HEADER).map(String::as_str) + != Some(PROTOCOL_VERSION) + { + let mut response = response; + return Err(VssError::VSSVersionMismatchError { + version_served: response.headers.remove(PROTOCOL_VERSION_HEADER), + version_expected: String::from(PROTOCOL_VERSION), + }); + } let status_code = response.status_code; let payload = response.into_bytes(); diff --git a/src/error.rs b/src/error.rs index a301211..085dabf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,6 +26,14 @@ pub enum VssError { /// There is an unknown error, it could be a client-side bug, unrecognized error-code, network error /// or something else. InternalError(String), + + /// The VSS server and client speak different versions of the VSS protocol + VSSVersionMismatchError { + /// The VSS protocol version served + version_served: Option, + /// The VSS protocol version expected + version_expected: String, + }, } impl VssError { @@ -62,6 +70,21 @@ impl Display for VssError { VssError::InternalServerError(message) => { write!(f, "InternalServerError: {}", message) }, + VssError::VSSVersionMismatchError { + version_served: Some(served), + version_expected, + } => { + write!( + f, + "The VSS server and client speak different versions of the \ + VSS protocol, the server serves version {}, client expects \ + {}", + served, version_expected, + ) + }, + VssError::VSSVersionMismatchError { version_served: None, version_expected: _ } => { + write!(f, "The server did not set the `vss-protocol-version` header") + }, VssError::InternalError(message) => { write!(f, "InternalError: {}", message) }, diff --git a/tests/tests.rs b/tests/tests.rs index 986f886..0c7b4c8 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -21,6 +21,8 @@ mod tests { const APPLICATION_OCTET_STREAM: &str = "application/octet-stream"; const CONTENT_TYPE: &str = "content-type"; + const PROTOCOL_VERSION_HEADER: &str = "vss-protocol-version"; + const PROTOCOL_VERSION: &str = "0"; const GET_OBJECT_ENDPOINT: &'static str = "/getObject"; const PUT_OBJECT_ENDPOINT: &'static str = "/putObjects"; @@ -44,6 +46,7 @@ mod tests { .match_header(CONTENT_TYPE, APPLICATION_OCTET_STREAM) .match_body(get_request.encode_to_vec()) .with_status(200) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(mock_response.encode_to_vec()) .create(); @@ -77,6 +80,7 @@ mod tests { .match_header("headerkey", "headervalue") .match_body(get_request.encode_to_vec()) .with_status(200) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(mock_response.encode_to_vec()) .create(); @@ -119,6 +123,7 @@ mod tests { .match_header(CONTENT_TYPE, APPLICATION_OCTET_STREAM) .match_body(request.encode_to_vec()) .with_status(200) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(mock_response.encode_to_vec()) .create(); @@ -154,6 +159,7 @@ mod tests { .match_header(CONTENT_TYPE, APPLICATION_OCTET_STREAM) .match_body(request.encode_to_vec()) .with_status(200) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(mock_response.encode_to_vec()) .create(); @@ -195,6 +201,7 @@ mod tests { .match_header(CONTENT_TYPE, APPLICATION_OCTET_STREAM) .match_body(request.encode_to_vec()) .with_status(200) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(mock_response.encode_to_vec()) .create(); @@ -222,6 +229,7 @@ mod tests { }; let mock_server = mockito::mock("POST", GET_OBJECT_ENDPOINT) .with_status(404) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(&error_response.encode_to_vec()) .create(); @@ -246,6 +254,7 @@ mod tests { let mock_response = GetObjectResponse { value: None, ..Default::default() }; let mock_server = mockito::mock("POST", GET_OBJECT_ENDPOINT) .with_status(200) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(&mock_response.encode_to_vec()) .create(); @@ -270,6 +279,7 @@ mod tests { }; let mock_server = mockito::mock("POST", Matcher::Any) .with_status(400) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(&error_response.encode_to_vec()) .create(); @@ -330,6 +340,7 @@ mod tests { }; let mock_server = mockito::mock("POST", Matcher::Any) .with_status(401) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(&error_response.encode_to_vec()) .create(); @@ -412,6 +423,7 @@ mod tests { }; let mock_server = mockito::mock("POST", Matcher::Any) .with_status(409) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(&error_response.encode_to_vec()) .create(); @@ -445,6 +457,7 @@ mod tests { }; let mock_server = mockito::mock("POST", Matcher::Any) .with_status(500) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(&error_response.encode_to_vec()) .create(); @@ -502,6 +515,7 @@ mod tests { ErrorResponse { error_code: 999, message: "UnknownException".to_string() }; let mut _mock_server = mockito::mock("POST", Matcher::Any) .with_status(999) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(&error_response.encode_to_vec()) .create(); @@ -534,6 +548,7 @@ mod tests { let malformed_error_response = b"malformed"; _mock_server = mockito::mock("POST", Matcher::Any) .with_status(409) + .with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION) .with_body(&malformed_error_response) .create(); @@ -546,17 +561,36 @@ mod tests { let list_malformed_err_response = vss_client.list_key_versions(&list_request).await; assert!(matches!(list_malformed_err_response.unwrap_err(), VssError::InternalError { .. })); - // Requests to endpoints are no longer mocked and will result in network error. + // Requests to endpoints are no longer mocked and will result in version mismatch + // errors. drop(_mock_server); - let get_network_err = vss_client.get_object(&get_request).await; - assert!(matches!(get_network_err.unwrap_err(), VssError::InternalError { .. })); - - let put_network_err = vss_client.put_object(&put_request).await; - assert!(matches!(put_network_err.unwrap_err(), VssError::InternalError { .. })); - - let list_network_err = vss_client.list_key_versions(&list_request).await; - assert!(matches!(list_network_err.unwrap_err(), VssError::InternalError { .. })); + let get_version_err = vss_client.get_object(&get_request).await; + assert!(matches!( + get_version_err.unwrap_err(), + VssError::VSSVersionMismatchError { + version_served: None, + version_expected: version + } if version == PROTOCOL_VERSION + )); + + let put_version_err = vss_client.put_object(&put_request).await; + assert!(matches!( + put_version_err.unwrap_err(), + VssError::VSSVersionMismatchError { + version_served: None, + version_expected: version + } if version == PROTOCOL_VERSION + )); + + let list_version_err = vss_client.list_key_versions(&list_request).await; + assert!(matches!( + list_version_err.unwrap_err(), + VssError::VSSVersionMismatchError { + version_served: None, + version_expected: version + } if version == PROTOCOL_VERSION + )); } fn retry_policy() -> impl RetryPolicy {