From 22243252fab0325cbcdd700fec39afd9b981ea0d Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 7 Aug 2026 16:24:51 -0500 Subject: [PATCH 1/8] feat(sdk): add transport feature to dapi-grpc for types-only consumers dapi-grpc unconditionally built tonic with its native transport stack (channel + TLS roots) on non-wasm targets, so any consumer of the message types or proof-verification layers (drive-proof-verifier) dragged hyper/tokio/rustls into its dependency tree even when it never opens a connection. The wasm target already proves the crate works with codegen-only tonic. Add a default-on 'transport' cargo feature carrying tonic's channel/transport/tls features, mirroring the client/server feature split tenderdash-proto already has. build.rs drives tonic-build's build_transport from CARGO_FEATURE_TRANSPORT (never on wasm32, where the transport stack does not build). Consumers that need the native transport are wired explicitly: rs-dapi-client (target-scoped to non-wasm), dash-sdk (default feature), and drive-abci via server (which now implies transport). wasm-sdk switches to default-features = false like rs-dapi-client already does, since the default-on feature would otherwise request tonic's transport on wasm. drive-proof-verifier needs no changes and its standalone tree drops from 407 to 339 crates: hyper, h2, rustls, ring, tower and the rest of the transport stack disappear; what remains of tonic's codegen core is a sync-only tokio slice via tokio-stream. Types-only consumers build with: dapi-grpc = { default-features = false, features = ["platform", "client"] } --- packages/dapi-grpc/Cargo.toml | 28 +++++++++++++++++++--------- packages/dapi-grpc/build.rs | 13 +++++++++++-- packages/rs-dapi-client/Cargo.toml | 5 +++++ packages/rs-sdk/Cargo.toml | 1 + packages/wasm-sdk/Cargo.toml | 8 +++++++- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/packages/dapi-grpc/Cargo.toml b/packages/dapi-grpc/Cargo.toml index 498f8584c9d..17330d5ac3c 100644 --- a/packages/dapi-grpc/Cargo.toml +++ b/packages/dapi-grpc/Cargo.toml @@ -14,7 +14,7 @@ rust-version.workspace = true license = "MIT" [features] -default = ["core", "platform", "client"] +default = ["core", "platform", "client", "transport"] # Internal Drive endpoints. Used by DAPI drive = ["platform"] core = [] @@ -26,12 +26,29 @@ tenderdash-proto = [] # Client support. client = ["platform"] +# Networked tonic client: `connect()` on generated clients, TLS roots. Without +# this feature the crate provides message types and transport-generic client +# stubs only — no hyper/tokio in the dependency tree. Types-only consumers +# (proof verification, embedders with their own transport) build with +# `default-features = false, features = ["platform", "client"]`. +# wasm32 consumers must use `default-features = false` (as rs-dapi-client and +# wasm-sdk do): tonic's transport stack does not build on wasm, and the wasm +# codegen path never emits `connect()`. +transport = [ + "tonic/channel", + "tonic/transport", + "tonic/tls-native-roots", + "tonic/tls-webpki-roots", + "tonic/tls-ring", +] + # Build tonic server code. Includes all client features and adds server-specific dependencies. server = [ "platform", "tenderdash-proto/server", "client", "drive", + "transport", "tonic/router", ] @@ -55,14 +72,7 @@ tonic = { version = "0.14.2", features = ["codegen"], default-features = false } getrandom = { version = "0.2", features = ["js"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tonic = { version = "0.14.2", features = [ - "codegen", - "channel", - "transport", - "tls-native-roots", - "tls-webpki-roots", - "tls-ring", -], default-features = false } +tonic = { version = "0.14.2", features = ["codegen"], default-features = false } [build-dependencies] tonic-prost-build = { version = "0.14.2" } diff --git a/packages/dapi-grpc/build.rs b/packages/dapi-grpc/build.rs index 50e0d57b6d7..f055da18fc2 100644 --- a/packages/dapi-grpc/build.rs +++ b/packages/dapi-grpc/build.rs @@ -69,6 +69,7 @@ fn generate_code(typ: ImplType, output_base: &Path) { println!("cargo:rerun-if-changed=./protos"); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_SERDE"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_TRANSPORT"); println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_ARCH"); println!("cargo:rerun-if-env-changed=DAPI_GRPC_OUT_DIR"); } @@ -416,15 +417,23 @@ enum ImplType { impl ImplType { // Configure the builder based on the implementation type. pub fn configure(&self, builder: Builder) -> Builder { + // The `transport` cargo feature controls whether generated clients get + // the `connect()` convenience impls over tonic's own channel. Without + // it, clients are still generated but stay generic over the caller's + // transport. Never enabled for wasm32, where tonic transport does not + // build. Note: cfg!(target_arch) in a build script reflects the HOST, + // so the target must be read from CARGO_CFG_TARGET_ARCH. + let transport = std::env::var("CARGO_FEATURE_TRANSPORT").is_ok() + && std::env::var("CARGO_CFG_TARGET_ARCH").map(|arch| arch != "wasm32") == Ok(true); match self { Self::Server => builder .build_client(true) .build_server(true) - .build_transport(true), + .build_transport(transport), Self::Client => builder .build_client(true) .build_server(false) - .build_transport(true), + .build_transport(transport), Self::Wasm => builder .build_client(true) .build_server(false) diff --git a/packages/rs-dapi-client/Cargo.toml b/packages/rs-dapi-client/Cargo.toml index 705f5ccec2f..a52c12400fb 100644 --- a/packages/rs-dapi-client/Cargo.toml +++ b/packages/rs-dapi-client/Cargo.toml @@ -26,6 +26,11 @@ backon = { version = "1.3", default-features = false, features = [ "tokio-sleep", ] } tokio = { version = "1.40", features = ["time"] } +# The native transport (tonic channel + TLS) comes from dapi-grpc's transport +# feature; wasm builds use tonic-web-wasm-client instead and must not pull it. +dapi-grpc = { path = "../dapi-grpc", features = [ + "transport", +], default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] gloo-timers = { version = "0.3.0", features = ["futures"] } diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 8f8ba61d9df..99eda2a639b 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -76,6 +76,7 @@ default = [ "mocks", "offline-testing", "dapi-grpc/client", + "dapi-grpc/transport", "token_reward_explanations", ] spv-client = [ diff --git a/packages/wasm-sdk/Cargo.toml b/packages/wasm-sdk/Cargo.toml index 179eea2bf4d..5adb8a6b42b 100644 --- a/packages/wasm-sdk/Cargo.toml +++ b/packages/wasm-sdk/Cargo.toml @@ -85,7 +85,13 @@ rand = { version = "0.8", features = ["std"] } rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider" } once_cell = "1.19" js-sys = "0.3.64" -dapi-grpc = { path = "../dapi-grpc" } +# default-features = false: the default `transport` feature enables tonic's +# native transport stack, which does not build on wasm32. +dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ + "core", + "platform", + "client", +] } rs-dapi-client = { path = "../rs-dapi-client" } hmac = { version = "0.12" } sha2 = { version = "0.10" } From aaabd040cf250b474fd8c2ef280c0ba572629246 Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 7 Aug 2026 17:08:25 -0500 Subject: [PATCH 2/8] refactor(sdk): extract transport-free query core into dash-platform-queries Split rs-sdk per the maintainer guidance to refactor rather than duplicate: the query-building, wire-encoding, and proof-decoding core that a transport-free embedder needs now lives in a new packages/dash-platform-queries crate, and rs-sdk depends on it and re-exports every moved item at its old path, so no rs-sdk consumer changes imports. Moved out of rs-sdk: DocumentQuery and its wire encoders (document_query.rs), the count/sum/average/ranked proof helpers and their FromProof aggregate views (DocumentCount, DocumentSum, DocumentAverage, DocumentSplitCounts, DocumentSplitSums, DocumentSplitAverages, DocumentRankedEntries), DocumentHistoryQuery, block_info_from_metadata, QuerySettings, FinalizedEpochQuery, ensure_valid_state_transition_structure, and the DPNS username helpers (convert_to_homograph_safe_chars, is_valid_username, is_contested_username). Sdk-bound pieces stay behind: the contract-fetching DocumentQuery constructor (now the DocumentQuerySdk extension trait), the Query encoder impl, the Fetch bindings for the aggregate views, and the Query impls for FinalizedEpochQuery. QuerySettings loses its request_settings field: it was documented dead weight (not consulted by any encoder) and was the only rs-dapi-client tie in the moved struct. Sdk::query_settings and the few test construction sites were updated accordingly. The new crate has its own small thiserror enum (Config/Drive/Protocol); rs-sdk converts it via From, so existing ? call sites keep compiling. wasm-sdk gains the matching From impl for WasmSdkError, routed through SdkError so the mapping is unchanged. Coherence fallout: with DocumentQuery now foreign to rs-sdk, the blanket 'impl Query for T where T: TransportRequest' would conflict with the explicit identity impl for DocumentQuery. The blanket is now additionally bounded by a local, explicitly-implemented WireQuery marker covering every wire request proto (list mirrors rs-dapi-client's TransportRequest impls); rustc can then prove the impl sets disjoint. The new crate's dependency tree is transport-free: no rs-dapi-client, hyper, rustls, or tonic transport. --- .../package-filters/rs-packages-direct.yml | 3 + .../rs-packages-no-workflows.yml | 4 + .github/package-filters/rs-packages.yml | 5 + Cargo.lock | 18 ++ Cargo.toml | 1 + packages/dash-platform-queries/Cargo.toml | 53 +++++ .../src}/block_info_from_metadata.rs | 0 .../src}/documents/average_proof_helpers.rs | 2 +- .../src}/documents/count_proof_helpers.rs | 2 +- .../src}/documents/document_average.rs | 12 +- .../src}/documents/document_count.rs | 10 +- .../src}/documents/document_history_query.rs | 0 .../src}/documents/document_query.rs | 41 +--- .../src}/documents/document_ranked_entries.rs | 18 +- .../src}/documents/document_split_averages.rs | 12 +- .../src}/documents/document_split_counts.rs | 10 +- .../src}/documents/document_split_sums.rs | 10 +- .../src}/documents/document_sum.rs | 10 +- .../src/documents/mod.rs | 26 +++ .../src}/documents/ranked_proof_helpers.rs | 2 +- .../src}/documents/sum_proof_helpers.rs | 2 +- .../src/dpns_usernames.rs | 196 ++++++++++++++++++ packages/dash-platform-queries/src/error.rs | 45 ++++ packages/dash-platform-queries/src/lib.rs | 23 ++ packages/dash-platform-queries/src/mock.rs | 7 + .../src/query_settings.rs | 38 ++++ .../src/transition/mod.rs | 2 + .../src/transition/validation.rs | 42 ++++ .../src/types/finalized_epoch.rs | 37 ++++ .../dash-platform-queries/src/types/mod.rs | 2 + packages/rs-sdk/Cargo.toml | 2 + packages/rs-sdk/src/error.rs | 10 + packages/rs-sdk/src/lib.rs | 1 + packages/rs-sdk/src/platform.rs | 4 +- packages/rs-sdk/src/platform/delegate.rs | 2 + .../platform/documents/document_query_sdk.rs | 69 ++++++ .../src/platform/documents/fetch_bindings.rs | 48 +++++ packages/rs-sdk/src/platform/documents/mod.rs | 43 ++-- .../rs-sdk/src/platform/dpns_usernames/mod.rs | 189 +---------------- .../identities_contract_keys_query.rs | 2 + packages/rs-sdk/src/platform/query.rs | 94 ++++++++- .../rs-sdk/src/platform/query_settings.rs | 48 ----- .../src/platform/transition/validation.rs | 49 +---- packages/rs-sdk/src/platform/types/epoch.rs | 7 +- packages/rs-sdk/src/platform/types/evonode.rs | 2 + .../src/platform/types/finalized_epoch.rs | 35 +--- packages/rs-sdk/src/sdk.rs | 5 +- packages/rs-sdk/tests/fetch/common.rs | 2 - packages/rs-sdk/tests/fetch/document.rs | 2 +- .../tests/fetch/document_query_v0_v1.rs | 28 ++- packages/rs-sdk/tests/fetch/mock_fetch.rs | 2 +- .../tests/fetch/tokens/token_contract_info.rs | 5 - packages/wasm-sdk/src/error.rs | 9 + 53 files changed, 810 insertions(+), 481 deletions(-) create mode 100644 packages/dash-platform-queries/Cargo.toml rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/block_info_from_metadata.rs (100%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/average_proof_helpers.rs (99%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/count_proof_helpers.rs (99%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/document_average.rs (96%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/document_count.rs (85%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/document_history_query.rs (100%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/document_query.rs (96%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/document_ranked_entries.rs (97%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/document_split_averages.rs (85%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/document_split_counts.rs (89%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/document_split_sums.rs (85%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/document_sum.rs (95%) create mode 100644 packages/dash-platform-queries/src/documents/mod.rs rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/ranked_proof_helpers.rs (99%) rename packages/{rs-sdk/src/platform => dash-platform-queries/src}/documents/sum_proof_helpers.rs (99%) create mode 100644 packages/dash-platform-queries/src/dpns_usernames.rs create mode 100644 packages/dash-platform-queries/src/error.rs create mode 100644 packages/dash-platform-queries/src/lib.rs create mode 100644 packages/dash-platform-queries/src/mock.rs create mode 100644 packages/dash-platform-queries/src/query_settings.rs create mode 100644 packages/dash-platform-queries/src/transition/mod.rs create mode 100644 packages/dash-platform-queries/src/transition/validation.rs create mode 100644 packages/dash-platform-queries/src/types/finalized_epoch.rs create mode 100644 packages/dash-platform-queries/src/types/mod.rs create mode 100644 packages/rs-sdk/src/platform/documents/document_query_sdk.rs create mode 100644 packages/rs-sdk/src/platform/documents/fetch_bindings.rs delete mode 100644 packages/rs-sdk/src/platform/query_settings.rs diff --git a/.github/package-filters/rs-packages-direct.yml b/.github/package-filters/rs-packages-direct.yml index d9ea1b64067..441c8137023 100644 --- a/.github/package-filters/rs-packages-direct.yml +++ b/.github/package-filters/rs-packages-direct.yml @@ -115,6 +115,9 @@ rs-dapi-client: platform-encryption: - packages/rs-platform-encryption/** +dash-platform-queries: + - packages/dash-platform-queries/** + dash-sdk: - packages/rs-sdk/** diff --git a/.github/package-filters/rs-packages-no-workflows.yml b/.github/package-filters/rs-packages-no-workflows.yml index 3825b065eef..90835d0429f 100644 --- a/.github/package-filters/rs-packages-no-workflows.yml +++ b/.github/package-filters/rs-packages-no-workflows.yml @@ -127,9 +127,13 @@ rs-dapi-client: &dapi_client platform-encryption: &platform_encryption - packages/rs-platform-encryption/** +dash-platform-queries: &platform_queries + - packages/dash-platform-queries/** + dash-sdk: &sdk - packages/rs-drive-proof-verifier/** - packages/rs-sdk/** + - *platform_queries - *dash_async - *context_provider - *sdk_trusted_context_provider diff --git a/.github/package-filters/rs-packages.yml b/.github/package-filters/rs-packages.yml index f38a77fa931..6fae2aa84ab 100644 --- a/.github/package-filters/rs-packages.yml +++ b/.github/package-filters/rs-packages.yml @@ -151,10 +151,15 @@ platform-encryption: &platform_encryption - .github/workflows/tests* - packages/rs-platform-encryption/** +dash-platform-queries: &platform_queries + - .github/workflows/tests* + - packages/dash-platform-queries/** + dash-sdk: &sdk - .github/workflows/tests* - packages/rs-drive-proof-verifier/** - packages/rs-sdk/** + - *platform_queries - *dash_async - *context_provider - *sdk_trusted_context_provider diff --git a/Cargo.lock b/Cargo.lock index d448b3594d9..5b7495f8e67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,6 +1702,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dash-platform-queries" +version = "4.1.0" +dependencies = [ + "dapi-grpc", + "dash-context-provider", + "dash-platform-macros", + "dpp", + "drive", + "drive-proof-verifier", + "hex", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "dash-sdk" version = "4.1.0" @@ -1719,6 +1736,7 @@ dependencies = [ "dash-context-provider", "dash-network-seeds", "dash-platform-macros", + "dash-platform-queries", "derive_more 1.0.0", "dotenvy", "dpp", diff --git a/Cargo.toml b/Cargo.toml index b030762325f..3ac15c79194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "packages/wasm-dpp2", "packages/rs-dapi-client", "packages/rs-dash-async", + "packages/dash-platform-queries", "packages/rs-sdk", "packages/strategy-tests", "packages/simple-signer", diff --git a/packages/dash-platform-queries/Cargo.toml b/packages/dash-platform-queries/Cargo.toml new file mode 100644 index 00000000000..f3d76e7c747 --- /dev/null +++ b/packages/dash-platform-queries/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "dash-platform-queries" +description = "Transport-free query building and proof decoding core shared by Dash Platform SDK embedders" +version.workspace = true +edition = "2021" +rust-version.workspace = true +license = "MIT" + +[features] +default = [] +mocks = [ + "dep:serde", + "dep:serde_json", + "dapi-grpc/mocks", + "drive/serde", + "dpp/serde-conversion", +] + +[dependencies] +dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ + "platform", + "client", +] } +dash-context-provider = { path = "../rs-context-provider", default-features = false } +dash-platform-macros = { path = "../rs-dash-platform-macros" } +dpp = { path = "../rs-dpp", default-features = false, features = [ + "platform-value-cbor", + "state-transitions", + "state-transition-validation", +] } +drive = { path = "../rs-drive", default-features = false, features = [ + "verify", +] } +drive-proof-verifier = { path = "../rs-drive-proof-verifier", default-features = false } +hex = { version = "0.4.3" } +serde = { version = "1.0.219", default-features = false, features = [ + "rc", +], optional = true } +serde_json = { version = "1.0", optional = true } +thiserror = "2.0.17" +tracing = { version = "0.1.41" } + +[dev-dependencies] +dpp = { path = "../rs-dpp", default-features = false, features = [ + "fixtures-and-mocks", +] } + +[package.metadata.cargo-machete] +ignored = [ + # Used inside the `dash_platform_macros::Mockable` derive expansion under + # the `mocks` feature; machete cannot see through proc-macro output. + "serde_json", +] diff --git a/packages/rs-sdk/src/platform/block_info_from_metadata.rs b/packages/dash-platform-queries/src/block_info_from_metadata.rs similarity index 100% rename from packages/rs-sdk/src/platform/block_info_from_metadata.rs rename to packages/dash-platform-queries/src/block_info_from_metadata.rs diff --git a/packages/rs-sdk/src/platform/documents/average_proof_helpers.rs b/packages/dash-platform-queries/src/documents/average_proof_helpers.rs similarity index 99% rename from packages/rs-sdk/src/platform/documents/average_proof_helpers.rs rename to packages/dash-platform-queries/src/documents/average_proof_helpers.rs index ec73d18fba0..5ab4e9bf77a 100644 --- a/packages/rs-sdk/src/platform/documents/average_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/average_proof_helpers.rs @@ -19,7 +19,7 @@ //! [`DocumentAverage`]: drive_proof_verifier::DocumentAverage //! [`DocumentSplitAverages`]: drive_proof_verifier::DocumentSplitAverages -use crate::platform::documents::document_query::DocumentQuery; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; use dash_context_provider::ContextProvider; diff --git a/packages/rs-sdk/src/platform/documents/count_proof_helpers.rs b/packages/dash-platform-queries/src/documents/count_proof_helpers.rs similarity index 99% rename from packages/rs-sdk/src/platform/documents/count_proof_helpers.rs rename to packages/dash-platform-queries/src/documents/count_proof_helpers.rs index 9b99fcdee48..e19e9074f53 100644 --- a/packages/rs-sdk/src/platform/documents/count_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/count_proof_helpers.rs @@ -13,7 +13,7 @@ //! [`DocumentCount`]: drive_proof_verifier::DocumentCount //! [`DocumentSplitCounts`]: drive_proof_verifier::DocumentSplitCounts -use crate::platform::documents::document_query::DocumentQuery; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; use dash_context_provider::ContextProvider; diff --git a/packages/rs-sdk/src/platform/documents/document_average.rs b/packages/dash-platform-queries/src/documents/document_average.rs similarity index 96% rename from packages/rs-sdk/src/platform/documents/document_average.rs rename to packages/dash-platform-queries/src/documents/document_average.rs index 340b7ca1a4e..9a24a365b77 100644 --- a/packages/rs-sdk/src/platform/documents/document_average.rs +++ b/packages/dash-platform-queries/src/documents/document_average.rs @@ -13,11 +13,8 @@ //! absent branch — same forward-compat for absence proofs as count) //! contribute 0 to both axes via `filter_map(|e| e.)`. -use crate::platform::documents::average_proof_helpers::{ - assert_select_is_avg, verify_average_query, -}; -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::Fetch; +use crate::documents::average_proof_helpers::{assert_select_is_avg, verify_average_query}; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -101,11 +98,6 @@ impl FromProof for DocumentAverage { } } -impl Fetch for DocumentAverage { - type Query = super::document_query::DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} - #[cfg(test)] mod tests { //! Unit tests for the AVG fold. The fold logic is extracted diff --git a/packages/rs-sdk/src/platform/documents/document_count.rs b/packages/dash-platform-queries/src/documents/document_count.rs similarity index 85% rename from packages/rs-sdk/src/platform/documents/document_count.rs rename to packages/dash-platform-queries/src/documents/document_count.rs index 8f46f8c9c90..1ea5899ebd8 100644 --- a/packages/rs-sdk/src/platform/documents/document_count.rs +++ b/packages/dash-platform-queries/src/documents/document_count.rs @@ -13,9 +13,8 @@ //! queried-but-absent branch) contribute 0 to the sum via //! `filter_map(|e| e.count)`. -use crate::platform::documents::count_proof_helpers::{assert_select_is_count, verify_count_query}; -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::Fetch; +use crate::documents::count_proof_helpers::{assert_select_is_count, verify_count_query}; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -47,8 +46,3 @@ impl FromProof for DocumentCount { Ok((count, mtd, proof)) } } - -impl Fetch for DocumentCount { - type Query = DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} diff --git a/packages/rs-sdk/src/platform/documents/document_history_query.rs b/packages/dash-platform-queries/src/documents/document_history_query.rs similarity index 100% rename from packages/rs-sdk/src/platform/documents/document_history_query.rs rename to packages/dash-platform-queries/src/documents/document_history_query.rs diff --git a/packages/rs-sdk/src/platform/documents/document_query.rs b/packages/dash-platform-queries/src/documents/document_query.rs similarity index 96% rename from packages/rs-sdk/src/platform/documents/document_query.rs rename to packages/dash-platform-queries/src/documents/document_query.rs index 57648cd1988..8f5fd18a96a 100644 --- a/packages/rs-sdk/src/platform/documents/document_query.rs +++ b/packages/dash-platform-queries/src/documents/document_query.rs @@ -2,8 +2,7 @@ use std::sync::Arc; -use crate::platform::Fetch; -use crate::{error::Error, sdk::Sdk}; +use crate::error::Error; use dapi_grpc::platform::v0::get_documents_request::Version::{V0, V1}; use dapi_grpc::platform::v0::{ self as platform_proto, @@ -179,25 +178,6 @@ impl DocumentQuery { Self::from(d) } - /// Create new document query for provided document type name and data contract ID. - /// - /// Note that this method will fetch data contract first. - pub async fn new_with_data_contract_id( - api: &Sdk, - data_contract_id: Identifier, - document_type_name: &str, - ) -> Result { - let data_contract = - DataContract::fetch(api, data_contract_id) - .await? - .ok_or(Error::MissingDependency( - "DataContract".to_string(), - format!("data contract {} not found", data_contract_id), - ))?; - - Self::new(data_contract, document_type_name) - } - /// Point to a specific document ID. pub fn with_document_id(self, document_id: &Identifier) -> Self { let clause = WhereClause { @@ -317,7 +297,7 @@ impl DocumentQuery { /// /// # The 5th-best group /// - /// ```rust,no_run + /// ```rust,ignore /// # use dash_sdk::platform::{DataContract, DocumentQuery}; /// # use dash_sdk::platform::documents::document_query::RankingDirection; /// # use dash_sdk::drive::query::SelectProjection; @@ -1073,20 +1053,3 @@ fn value_to_proto_at_depth(value: Value, depth: u8) -> Result for DocumentQuery { - fn query( - &self, - settings: &crate::platform::QuerySettings<'_>, - ) -> Result { - GetDocumentsRequest::try_from_platform_versioned(self.clone(), settings.protocol_version) - } -} diff --git a/packages/rs-sdk/src/platform/documents/document_ranked_entries.rs b/packages/dash-platform-queries/src/documents/document_ranked_entries.rs similarity index 97% rename from packages/rs-sdk/src/platform/documents/document_ranked_entries.rs rename to packages/dash-platform-queries/src/documents/document_ranked_entries.rs index b0cd0d41f95..544a5c00541 100644 --- a/packages/rs-sdk/src/platform/documents/document_ranked_entries.rs +++ b/packages/dash-platform-queries/src/documents/document_ranked_entries.rs @@ -80,7 +80,7 @@ //! //! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 5` //! -//! ```rust,no_run +//! ```rust,ignore //! use dash_sdk::{Sdk, platform::{DataContract, DocumentQuery, Fetch, Identifier}}; //! use dash_sdk::drive::query::SelectProjection; //! use dash_sdk::platform::documents::document_query::RankingDirection; @@ -127,7 +127,7 @@ //! //! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4` //! -//! ```rust,no_run +//! ```rust,ignore //! # use dash_sdk::platform::{DataContract, DocumentQuery}; //! # use dash_sdk::platform::documents::document_query::RankingDirection; //! # use dash_sdk::drive::query::SelectProjection; @@ -142,9 +142,8 @@ //! # } //! ``` -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::documents::ranked_proof_helpers::verify_ranked_query; -use crate::platform::Fetch; +use crate::documents::document_query::DocumentQuery; +use crate::documents::ranked_proof_helpers::verify_ranked_query; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -178,11 +177,6 @@ impl FromProof for DocumentRankedEntries { } } -impl Fetch for DocumentRankedEntries { - type Query = DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} - #[cfg(test)] mod tests { //! Offline tests for the ranked client surface: the ordering @@ -201,8 +195,8 @@ mod tests { //! not exist offline. use super::*; - use crate::platform::documents::document_query::RankingDirection; - use crate::platform::documents::ranked_proof_helpers::assert_ranked_shape; + use crate::documents::document_query::RankingDirection; + use crate::documents::ranked_proof_helpers::assert_ranked_shape; use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::select as proto_select; use dapi_grpc::platform::v0::get_documents_request::{ order_clause, GetDocumentsRequestV1, OrderClause as ProtoOrderClause, diff --git a/packages/rs-sdk/src/platform/documents/document_split_averages.rs b/packages/dash-platform-queries/src/documents/document_split_averages.rs similarity index 85% rename from packages/rs-sdk/src/platform/documents/document_split_averages.rs rename to packages/dash-platform-queries/src/documents/document_split_averages.rs index f15f1695151..29a3ab9252d 100644 --- a/packages/rs-sdk/src/platform/documents/document_split_averages.rs +++ b/packages/dash-platform-queries/src/documents/document_split_averages.rs @@ -12,11 +12,8 @@ //! impl passes the verified entries through unchanged, mapping //! `AverageEntry` to `SplitAverageEntry`. -use crate::platform::documents::average_proof_helpers::{ - assert_select_is_avg, verify_average_query, -}; -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::Fetch; +use crate::documents::average_proof_helpers::{assert_select_is_avg, verify_average_query}; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -57,8 +54,3 @@ impl FromProof for DocumentSplitAverages { Ok((split, mtd, proof)) } } - -impl Fetch for DocumentSplitAverages { - type Query = super::document_query::DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} diff --git a/packages/rs-sdk/src/platform/documents/document_split_counts.rs b/packages/dash-platform-queries/src/documents/document_split_counts.rs similarity index 89% rename from packages/rs-sdk/src/platform/documents/document_split_counts.rs rename to packages/dash-platform-queries/src/documents/document_split_counts.rs index 79fb3455354..18eb2ab664f 100644 --- a/packages/rs-sdk/src/platform/documents/document_split_counts.rs +++ b/packages/dash-platform-queries/src/documents/document_split_counts.rs @@ -31,9 +31,8 @@ //! ranges are simply absent — the range itself is unbounded so //! there's no enumerable key set to ever-emit. -use crate::platform::documents::count_proof_helpers::{assert_select_is_count, verify_count_query}; -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::Fetch; +use crate::documents::count_proof_helpers::{assert_select_is_count, verify_count_query}; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -62,8 +61,3 @@ impl FromProof for DocumentSplitCounts { Ok((entries.map(DocumentSplitCounts::from_verified), mtd, proof)) } } - -impl Fetch for DocumentSplitCounts { - type Query = DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} diff --git a/packages/rs-sdk/src/platform/documents/document_split_sums.rs b/packages/dash-platform-queries/src/documents/document_split_sums.rs similarity index 85% rename from packages/rs-sdk/src/platform/documents/document_split_sums.rs rename to packages/dash-platform-queries/src/documents/document_split_sums.rs index fc5d1203304..50cdd6943ae 100644 --- a/packages/rs-sdk/src/platform/documents/document_split_sums.rs +++ b/packages/dash-platform-queries/src/documents/document_split_sums.rs @@ -12,9 +12,8 @@ //! passes the verified entries through unchanged, mapping //! `SumEntry` to `SplitSumEntry`. -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::documents::sum_proof_helpers::{assert_select_is_sum, verify_sum_query}; -use crate::platform::Fetch; +use crate::documents::document_query::DocumentQuery; +use crate::documents::sum_proof_helpers::{assert_select_is_sum, verify_sum_query}; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -54,8 +53,3 @@ impl FromProof for DocumentSplitSums { Ok((split, mtd, proof)) } } - -impl Fetch for DocumentSplitSums { - type Query = super::document_query::DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} diff --git a/packages/rs-sdk/src/platform/documents/document_sum.rs b/packages/dash-platform-queries/src/documents/document_sum.rs similarity index 95% rename from packages/rs-sdk/src/platform/documents/document_sum.rs rename to packages/dash-platform-queries/src/documents/document_sum.rs index c4265ec9ece..d88d57f530d 100644 --- a/packages/rs-sdk/src/platform/documents/document_sum.rs +++ b/packages/dash-platform-queries/src/documents/document_sum.rs @@ -18,9 +18,8 @@ //! can switch to `DocumentSplitSums` (which preserves per-branch //! `i64`s and lets the caller pick its own arithmetic). -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::documents::sum_proof_helpers::{assert_select_is_sum, verify_sum_query}; -use crate::platform::Fetch; +use crate::documents::document_query::DocumentQuery; +use crate::documents::sum_proof_helpers::{assert_select_is_sum, verify_sum_query}; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -86,11 +85,6 @@ impl FromProof for DocumentSum { } } -impl Fetch for DocumentSum { - type Query = super::document_query::DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} - #[cfg(test)] mod tests { //! Unit tests for the SUM fold. The fold logic is extracted diff --git a/packages/dash-platform-queries/src/documents/mod.rs b/packages/dash-platform-queries/src/documents/mod.rs new file mode 100644 index 00000000000..caabab7e85a --- /dev/null +++ b/packages/dash-platform-queries/src/documents/mod.rs @@ -0,0 +1,26 @@ +pub(crate) mod average_proof_helpers; +pub(crate) mod count_proof_helpers; +/// `FromProof` impl for the average-side aggregate result. Returns +/// `(count, sum)`; client divides. +pub mod document_average; +pub mod document_count; +pub mod document_history_query; +pub mod document_query; +/// `FromProof` impl for the ranked (`GROUP BY … ORDER BY LIMIT n +/// [OFFSET m]`) result — one entry per returned group, in ranking order, +/// plus the rank the page starts at. Requires an index declaring +/// `rankedCountable` / `rankedSummable` / `rankedAverageable` +/// (protocol version 14+). +pub mod document_ranked_entries; +/// `FromProof` impl for the average-side per-entry result. Mirrors +/// `document_split_sums`. +pub mod document_split_averages; +pub mod document_split_counts; +/// `FromProof` impl for the sum-side per-entry result. Mirrors +/// `document_split_counts`. +pub mod document_split_sums; +/// `FromProof` impl for the sum-side aggregate result. Mirrors +/// `document_count`. Lights up alongside grovedb PR 670. +pub mod document_sum; +pub(crate) mod ranked_proof_helpers; +pub(crate) mod sum_proof_helpers; diff --git a/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs b/packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs similarity index 99% rename from packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs rename to packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs index 9211efbce89..2b201ebc092 100644 --- a/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs @@ -18,7 +18,7 @@ //! //! [`DocumentRankedEntries`]: drive_proof_verifier::DocumentRankedEntries -use crate::platform::documents::document_query::DocumentQuery; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; use dash_context_provider::ContextProvider; diff --git a/packages/rs-sdk/src/platform/documents/sum_proof_helpers.rs b/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs similarity index 99% rename from packages/rs-sdk/src/platform/documents/sum_proof_helpers.rs rename to packages/dash-platform-queries/src/documents/sum_proof_helpers.rs index 17005e06301..fadfd00cfd8 100644 --- a/packages/rs-sdk/src/platform/documents/sum_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs @@ -19,7 +19,7 @@ //! [`DocumentSum`]: drive_proof_verifier::DocumentSum //! [`DocumentSplitSums`]: drive_proof_verifier::DocumentSplitSums -use crate::platform::documents::document_query::DocumentQuery; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; use dash_context_provider::ContextProvider; diff --git a/packages/dash-platform-queries/src/dpns_usernames.rs b/packages/dash-platform-queries/src/dpns_usernames.rs new file mode 100644 index 00000000000..3f452519b29 --- /dev/null +++ b/packages/dash-platform-queries/src/dpns_usernames.rs @@ -0,0 +1,196 @@ +//! Transport-free DPNS username helpers. +//! +//! The Sdk-bound DPNS surface (registration, availability checks, name +//! resolution) lives in `dash-sdk`; these free functions are pure string +//! validation/normalization shared with embedders. + +/// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' +/// with '0', '1', and '1' respectively to prevent homograph attacks +pub fn convert_to_homograph_safe_chars(input: &str) -> String { + input + .chars() + .map(|c| match c { + 'o' | 'O' => '0', + 'i' | 'I' => '1', + 'l' | 'L' => '1', + _ => c.to_ascii_lowercase(), + }) + .collect() +} + +/// Check if a username is valid according to DPNS rules +/// +/// A username is valid if: +/// - It's between 3 and 63 characters long +/// - It starts and ends with alphanumeric characters (a-zA-Z0-9) +/// - It contains only alphanumeric characters and hyphens +/// - It doesn't have consecutive hyphens (enforced by the pattern) +/// +/// Pattern: `^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$` +/// +/// # Arguments +/// +/// * `label` - The username label to check (e.g., "alice") +/// +/// # Returns +/// +/// Returns `true` if the username is valid, `false` otherwise +pub fn is_valid_username(label: &str) -> bool { + // Check length + if label.len() < 3 || label.len() > 63 { + return false; + } + + let chars: Vec = label.chars().collect(); + + // Check first character (must be alphanumeric) + if !chars[0].is_ascii_alphanumeric() { + return false; + } + + // Check last character (must be alphanumeric) + if !chars[chars.len() - 1].is_ascii_alphanumeric() { + return false; + } + + // Check middle characters (can be alphanumeric or hyphen) + for &ch in &chars[1..chars.len() - 1] { + if !ch.is_ascii_alphanumeric() && ch != '-' { + return false; + } + } + + // Additional check: no consecutive hyphens (good practice) + for i in 0..chars.len() - 1 { + if chars[i] == '-' && chars[i + 1] == '-' { + return false; + } + } + + true +} + +/// Check if a username is contested (requires masternode voting) +/// +/// A username is contested if its normalized label: +/// - Is between 3 and 19 characters long (inclusive) +/// - Contains only lowercase letters a-z, digits 0-1, and hyphens +/// +/// # Arguments +/// +/// * `label` - The username label to check (e.g., "alice") +/// +/// # Returns +/// +/// Returns `true` if the username would be contested, `false` otherwise +pub fn is_contested_username(label: &str) -> bool { + let normalized = convert_to_homograph_safe_chars(label); + + // Check length + if normalized.len() < 3 || normalized.len() > 19 { + return false; + } + + // Check if all characters match the pattern [a-z01-] + normalized + .chars() + .all(|c| matches!(c, 'a'..='z' | '0' | '1' | '-')) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_to_homograph_safe_chars() { + assert_eq!(convert_to_homograph_safe_chars("alice"), "a11ce"); + assert_eq!(convert_to_homograph_safe_chars("bob"), "b0b"); + assert_eq!(convert_to_homograph_safe_chars("COOL"), "c001"); + assert_eq!(convert_to_homograph_safe_chars("test123"), "test123"); + } + + #[test] + fn test_is_valid_username() { + // Valid usernames + assert!(is_valid_username("abc")); + assert!(is_valid_username("alice")); + assert!(is_valid_username("Alice123")); + assert!(is_valid_username("dash-p2p")); + assert!(is_valid_username("test-name-123")); + assert!(is_valid_username("a-b-c")); + assert!(is_valid_username("user2024")); + assert!(is_valid_username("CryptoKing")); + assert!(is_valid_username("web3-developer")); + assert!(is_valid_username("a".repeat(63).as_str())); // Max length + + // Invalid - too short + assert!(!is_valid_username("ab")); + assert!(!is_valid_username("a")); + assert!(!is_valid_username("")); + + // Invalid - too long + assert!(!is_valid_username("a".repeat(64).as_str())); + + // Invalid - starts with hyphen + assert!(!is_valid_username("-alice")); + assert!(!is_valid_username("-test")); + + // Invalid - ends with hyphen + assert!(!is_valid_username("alice-")); + assert!(!is_valid_username("test-")); + + // Invalid - starts and ends with hyphen + assert!(!is_valid_username("-alice-")); + + // Invalid - contains invalid characters + assert!(!is_valid_username("alice_bob")); // underscore + assert!(!is_valid_username("alice.bob")); // dot + assert!(!is_valid_username("alice@dash")); // at sign + assert!(!is_valid_username("alice!")); // exclamation + assert!(!is_valid_username("alice bob")); // space + assert!(!is_valid_username("alice#1")); // hash + assert!(!is_valid_username("alice$")); // dollar + assert!(!is_valid_username("alice%20")); // percent + + // Invalid - consecutive hyphens + assert!(!is_valid_username("alice--bob")); + assert!(!is_valid_username("test---name")); + } + + #[test] + fn test_is_contested_username() { + // Contested usernames (3-19 chars, only [a-z01-]) + assert!(is_contested_username("abc")); + assert!(is_contested_username("alice")); // becomes "a11ce" + assert!(is_contested_username("b0b")); + assert!(is_contested_username("cool")); // becomes "c001" + assert!(is_contested_username("a-b-c")); + assert!(is_contested_username("hello")); // becomes "he110" + assert!(is_contested_username("world")); // becomes "w0r1d" + assert!(is_contested_username("dash")); + assert!(is_contested_username("a11ce")); // already normalized + assert!(is_contested_username("dash-dao")); // becomes "dash-da0" + + // Not contested - too short + assert!(!is_contested_username("ab")); + assert!(!is_contested_username("io")); // becomes "10" which is 2 chars + assert!(!is_contested_username("a")); + + // Not contested - too long (20+ chars) + assert!(!is_contested_username("twenty-characters-ab")); // 20 chars + assert!(!is_contested_username( + "this-is-a-very-long-username-that-exceeds-limit" + )); + + // Not contested - contains invalid characters after normalization + assert!(!is_contested_username("alice2")); // contains '2' + assert!(!is_contested_username("alice_bob")); // contains '_' + assert!(!is_contested_username("alice.bob")); // contains '.' + assert!(!is_contested_username("alice@dash")); // contains '@' + assert!(!is_contested_username("alice!")); // contains '!' + assert!(!is_contested_username("test123")); // contains '2' and '3' + assert!(!is_contested_username("dash-p2p")); // contains 'p' and '2' + assert!(!is_contested_username("user5")); // contains '5' + assert!(!is_contested_username("name_with_underscore")); // contains '_' + } +} diff --git a/packages/dash-platform-queries/src/error.rs b/packages/dash-platform-queries/src/error.rs new file mode 100644 index 00000000000..0d8727763ce --- /dev/null +++ b/packages/dash-platform-queries/src/error.rs @@ -0,0 +1,45 @@ +//! Errors produced by the transport-free query core. + +use dpp::consensus::ConsensusError; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::ProtocolError; + +/// Error type for the transport-free query core. +/// +/// `dash-sdk` converts this into its own `Error` via `From`, so code that +/// moved here from the SDK keeps working behind `?` at its old call sites. +// Same allowance rs-sdk's Error carries: ProtocolError dominates the size. +#[allow(clippy::large_enum_variant)] +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Query is not configured properly for the target platform version + #[error("SDK misconfigured: {0}")] + Config(String), + /// Drive error + #[error("Drive error: {0}")] + Drive(#[from] drive::error::Error), + /// DPP error + #[error("Protocol error: {0}")] + Protocol(#[from] ProtocolError), +} + +impl From for Error { + fn from(value: ConsensusError) -> Self { + Self::Protocol(ProtocolError::ConsensusError(Box::new(value))) + } +} + +impl From for Error { + fn from(value: SimpleConsensusValidationResult) -> Self { + value + .errors + .into_iter() + .next() + .map(Error::from) + .unwrap_or_else(|| { + Error::Protocol(ProtocolError::CorruptedCodeExecution( + "state transition structure validation failed without an error".to_string(), + )) + }) + } +} diff --git a/packages/dash-platform-queries/src/lib.rs b/packages/dash-platform-queries/src/lib.rs new file mode 100644 index 00000000000..b644ca13c44 --- /dev/null +++ b/packages/dash-platform-queries/src/lib.rs @@ -0,0 +1,23 @@ +//! Transport-free query core of the Dash Platform SDK. +//! +//! This crate carries the pieces of `dash-sdk` that build queries, encode +//! them onto the wire format, and decode/verify proved responses — without +//! any transport dependency (no `rs-dapi-client`, no tokio, no tonic +//! transport stack). Embedders that bring their own transport can depend on +//! this crate alone; `dash-sdk` re-exports everything here at its +//! historical paths. + +// Same allowance the code carried in rs-sdk, whose crate root allows +// `result_large_err` for the dpp/drive error types threaded through here. +#![allow(clippy::result_large_err)] + +pub mod block_info_from_metadata; +pub mod documents; +pub mod dpns_usernames; +pub mod error; +pub mod mock; +pub mod query_settings; +pub mod transition; +pub mod types; + +pub use error::Error; diff --git a/packages/dash-platform-queries/src/mock.rs b/packages/dash-platform-queries/src/mock.rs new file mode 100644 index 00000000000..115e8445d38 --- /dev/null +++ b/packages/dash-platform-queries/src/mock.rs @@ -0,0 +1,7 @@ +//! Mocking support. +//! +//! The `dash_platform_macros::Mockable` derive expands to an impl of +//! `crate::mock::Mockable`, so every crate that derives it must expose the +//! trait at this path. The trait itself lives in `dapi-grpc` and is defined +//! even when mocks are disabled — serialization then just returns `None`. +pub use dapi_grpc::mock::Mockable; diff --git a/packages/dash-platform-queries/src/query_settings.rs b/packages/dash-platform-queries/src/query_settings.rs new file mode 100644 index 00000000000..cc9cd1a4509 --- /dev/null +++ b/packages/dash-platform-queries/src/query_settings.rs @@ -0,0 +1,38 @@ +//! Query encoding settings. +//! +//! [`QuerySettings`] is a small, borrow-style bundle handed to the SDK's +//! `Query::query` implementations so they can encode a user-facing query into +//! a wire `TransportRequest` without taking a full `&Sdk` dependency. This +//! keeps the encoder layer free of `Sdk`-shaped transitive deps (transport, +//! mock cache, nonce cache, context provider, …) and lets unit tests +//! construct settings directly without spinning up `Sdk::new_mock()`. +//! +//! The fields are the minimum surface a wire encoder needs today: protocol +//! version (to pick V0 vs V1 wire shapes) and the `prove` flag (proof-mode +//! requests vs unproved queries). + +use dpp::version::PlatformVersion; + +/// Settings passed to the SDK's `Query::query` for encoding a user-facing +/// query into a wire `TransportRequest`. +/// +/// Construct via `Sdk::query_settings` for normal use, or directly in unit +/// tests that want to exercise the encoder without an `Sdk`. +#[derive(Debug, Clone, Copy)] +pub struct QuerySettings<'a> { + /// Platform protocol version, used to pick wire encoding (V0 vs V1, etc). + pub protocol_version: &'a PlatformVersion, + + /// Whether to request and verify cryptographic proofs. + pub prove: bool, +} + +impl QuerySettings<'_> { + /// Cheap derivative with proofs forced off — used by `FetchUnproved`. + pub fn without_proofs(&self) -> Self { + Self { + prove: false, + ..*self + } + } +} diff --git a/packages/dash-platform-queries/src/transition/mod.rs b/packages/dash-platform-queries/src/transition/mod.rs new file mode 100644 index 00000000000..3a0f1376adb --- /dev/null +++ b/packages/dash-platform-queries/src/transition/mod.rs @@ -0,0 +1,2 @@ +//! Transport-free state transition helpers. +pub mod validation; diff --git a/packages/dash-platform-queries/src/transition/validation.rs b/packages/dash-platform-queries/src/transition/validation.rs new file mode 100644 index 00000000000..164a98befeb --- /dev/null +++ b/packages/dash-platform-queries/src/transition/validation.rs @@ -0,0 +1,42 @@ +use crate::Error; +use dpp::{ + consensus::{basic::BasicError, ConsensusError}, + state_transition::{StateTransition, StateTransitionStructureValidation}, + version::PlatformVersion, +}; + +/// Checks if an error is an UnsupportedFeatureError +fn is_unsupported_feature_error(error: &ConsensusError) -> bool { + matches!( + error, + ConsensusError::BasicError(BasicError::UnsupportedFeatureError(_)) + ) +} + +/// Ensures a state transition passes structure validation before broadcasting. +/// +/// Note: UnsupportedFeatureError is allowed to pass through, as it indicates +/// that structure validation is not implemented for that state transition type +/// (e.g., identity-based state transitions). The platform will still perform +/// validation during execution. +pub fn ensure_valid_state_transition_structure( + state_transition: &StateTransition, + platform_version: &PlatformVersion, +) -> Result<(), Error> { + let validation_result = state_transition.validate_structure(platform_version); + if validation_result.is_valid() { + Ok(()) + } else { + // Allow UnsupportedFeatureError to pass through - this means structure + // validation is not implemented for this state transition type + let all_unsupported_feature_errors = validation_result + .errors + .iter() + .all(is_unsupported_feature_error); + if all_unsupported_feature_errors { + Ok(()) + } else { + Err(validation_result.into()) + } + } +} diff --git a/packages/dash-platform-queries/src/types/finalized_epoch.rs b/packages/dash-platform-queries/src/types/finalized_epoch.rs new file mode 100644 index 00000000000..af1c273d320 --- /dev/null +++ b/packages/dash-platform-queries/src/types/finalized_epoch.rs @@ -0,0 +1,37 @@ +//! Finalized epoch related types and helpers +use dpp::block::epoch::EpochIndex; + +/// Query used to fetch multiple finalized epochs from Platform. +#[derive(Clone, Debug)] +pub struct FinalizedEpochQuery { + /// Starting epoch index. + pub start_epoch_index: EpochIndex, + /// Whether to include the start epoch. + pub start_epoch_index_included: bool, + /// Ending epoch index. + pub end_epoch_index: EpochIndex, + /// Whether to include the end epoch. + pub end_epoch_index_included: bool, +} + +impl Default for FinalizedEpochQuery { + fn default() -> Self { + Self { + start_epoch_index: 0, + start_epoch_index_included: true, + end_epoch_index: 0, + end_epoch_index_included: true, + } + } +} + +impl From<(EpochIndex, EpochIndex)> for FinalizedEpochQuery { + fn from((start, end): (EpochIndex, EpochIndex)) -> Self { + Self { + start_epoch_index: start, + start_epoch_index_included: true, + end_epoch_index: end, + end_epoch_index_included: true, + } + } +} diff --git a/packages/dash-platform-queries/src/types/mod.rs b/packages/dash-platform-queries/src/types/mod.rs new file mode 100644 index 00000000000..fa3157966fc --- /dev/null +++ b/packages/dash-platform-queries/src/types/mod.rs @@ -0,0 +1,2 @@ +//! Transport-free query types for various dpp objects. +pub mod finalized_epoch; diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 99eda2a639b..fefff062c29 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -25,6 +25,7 @@ grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a dash-async = { path = "../rs-dash-async" } dash-context-provider = { path = "../rs-context-provider", default-features = false } dash-platform-macros = { path = "../rs-dash-platform-macros" } +dash-platform-queries = { path = "../dash-platform-queries" } platform-encryption = { path = "../rs-platform-encryption" } http = { version = "1.1" } ciborium = { version = "0.2.2" } @@ -90,6 +91,7 @@ spv-client = [ mocks = [ "dep:serde", "dep:serde_json", + "dash-platform-queries/mocks", "rs-dapi-client/mocks", "rs-dapi-client/dump", "dpp/document-cbor-conversion", diff --git a/packages/rs-sdk/src/error.rs b/packages/rs-sdk/src/error.rs index 4e4ab3a9a18..cc8309ebcd4 100644 --- a/packages/rs-sdk/src/error.rs +++ b/packages/rs-sdk/src/error.rs @@ -133,6 +133,16 @@ pub enum Error { NoAvailableAddressesToRetry(Box), } +impl From for Error { + fn from(value: dash_platform_queries::Error) -> Self { + match value { + dash_platform_queries::Error::Config(msg) => Self::Config(msg), + dash_platform_queries::Error::Drive(e) => Self::Drive(e), + dash_platform_queries::Error::Protocol(e) => Self::Protocol(e), + } + } +} + /// State transition broadcast error #[derive(Debug, thiserror::Error)] #[error("state transition broadcast error: {message}")] diff --git a/packages/rs-sdk/src/lib.rs b/packages/rs-sdk/src/lib.rs index cb92f01d8d0..0299351a8dd 100644 --- a/packages/rs-sdk/src/lib.rs +++ b/packages/rs-sdk/src/lib.rs @@ -90,6 +90,7 @@ pub use error::Error; pub use sdk::{RequestSettings, Sdk, SdkBuilder}; pub use dapi_grpc; +pub use dash_platform_queries; pub use dpp; #[cfg(feature = "core_spv")] pub use dpp::dash_spv; diff --git a/packages/rs-sdk/src/platform.rs b/packages/rs-sdk/src/platform.rs index 9e2cbdf89af..e42a42b8995 100644 --- a/packages/rs-sdk/src/platform.rs +++ b/packages/rs-sdk/src/platform.rs @@ -6,7 +6,7 @@ // and while it will change the substance, the API structure will remain the same. pub mod address_sync; -pub mod block_info_from_metadata; +pub use dash_platform_queries::block_info_from_metadata; pub mod dashpay; mod delegate; pub mod documents; @@ -18,7 +18,7 @@ mod fetch_unproved; pub mod group_actions; pub mod identities_contract_keys_query; pub mod query; -pub mod query_settings; +pub use dash_platform_queries::query_settings; #[cfg(feature = "shielded")] pub mod shielded; pub mod tokens; diff --git a/packages/rs-sdk/src/platform/delegate.rs b/packages/rs-sdk/src/platform/delegate.rs index f58ecb03652..fddf6f5f62d 100644 --- a/packages/rs-sdk/src/platform/delegate.rs +++ b/packages/rs-sdk/src/platform/delegate.rs @@ -26,6 +26,8 @@ #[macro_export] macro_rules! delegate_transport_request_variant { ($request:ty, $response:ty, $($variant:ident),+) => { + impl $crate::platform::query::WireQuery for $request {} + impl $crate::platform::dapi::transport::TransportRequest for $request { type Client = $crate::platform::dapi::transport::PlatformGrpcClient; diff --git a/packages/rs-sdk/src/platform/documents/document_query_sdk.rs b/packages/rs-sdk/src/platform/documents/document_query_sdk.rs new file mode 100644 index 00000000000..b51b1dd7c2f --- /dev/null +++ b/packages/rs-sdk/src/platform/documents/document_query_sdk.rs @@ -0,0 +1,69 @@ +//! Sdk-bound surface of [`DocumentQuery`]. +//! +//! [`DocumentQuery`] itself is transport-free and lives in +//! `dash-platform-queries`; this module holds the pieces that need an +//! [`Sdk`]: the contract-fetching constructor and the rich→wire +//! [`Query`](crate::platform::Query) encoding step. + +use crate::platform::documents::document_query::DocumentQuery; +use crate::platform::Fetch; +use crate::{error::Error, sdk::Sdk}; +use dapi_grpc::platform::v0 as platform_proto; +use dapi_grpc::platform::v0::GetDocumentsRequest; +use dpp::prelude::{DataContract, Identifier}; +use dpp::version::TryFromPlatformVersioned; + +/// Sdk-bound extension methods for [`DocumentQuery`]. +/// +/// Kept as an extension trait because [`DocumentQuery`] is defined in the +/// transport-free `dash-platform-queries` crate, so its Sdk-dependent +/// constructor cannot be an inherent method there. Bring this trait into +/// scope to keep calling `DocumentQuery::new_with_data_contract_id(...)`. +#[allow(async_fn_in_trait)] +pub trait DocumentQuerySdk: Sized { + /// Create new document query for provided document type name and data contract ID. + /// + /// Note that this method will fetch data contract first. + async fn new_with_data_contract_id( + api: &Sdk, + data_contract_id: Identifier, + document_type_name: &str, + ) -> Result; +} + +impl DocumentQuerySdk for DocumentQuery { + async fn new_with_data_contract_id( + api: &Sdk, + data_contract_id: Identifier, + document_type_name: &str, + ) -> Result { + let data_contract = + DataContract::fetch(api, data_contract_id) + .await? + .ok_or(Error::MissingDependency( + "DataContract".to_string(), + format!("data contract {} not found", data_contract_id), + ))?; + + Self::new(data_contract, document_type_name).map_err(Error::from) + } +} + +/// Encode a [`DocumentQuery`] onto the wire using the SDK's +/// currently-known [`dpp::version::PlatformVersion`] for V0 vs V1 dispatch. +/// +/// The [`Fetch`] / [`FetchMany`](crate::platform::FetchMany) trampolines for +/// [`dpp::document::Document`] (and the document aggregate views) split +/// `Fetch::Query = DocumentQuery` (rich, what `FromProof` binds to) from +/// `Fetch::Request = GetDocumentsRequest` (wire); this impl is the +/// rich→wire step the trampoline invokes via +/// `Query::query(&rich, &sdk.query_settings())`. +impl crate::platform::Query for DocumentQuery { + fn query( + &self, + settings: &crate::platform::QuerySettings<'_>, + ) -> Result { + GetDocumentsRequest::try_from_platform_versioned(self.clone(), settings.protocol_version) + .map_err(Error::from) + } +} diff --git a/packages/rs-sdk/src/platform/documents/fetch_bindings.rs b/packages/rs-sdk/src/platform/documents/fetch_bindings.rs new file mode 100644 index 00000000000..b0ede2306dc --- /dev/null +++ b/packages/rs-sdk/src/platform/documents/fetch_bindings.rs @@ -0,0 +1,48 @@ +//! [`Fetch`] bindings for the document aggregate views. +//! +//! The `FromProof` decoding for these types moved to the transport-free +//! `dash-platform-queries` crate together with [`DocumentQuery`]; the +//! [`Fetch`] trait is Sdk-bound, so its impls stay here. + +use crate::platform::documents::document_query::DocumentQuery; +use crate::platform::Fetch; +use dapi_grpc::platform::v0::GetDocumentsRequest; +use drive_proof_verifier::{ + DocumentAverage, DocumentCount, DocumentRankedEntries, DocumentSplitAverages, + DocumentSplitCounts, DocumentSplitSums, DocumentSum, +}; + +impl Fetch for DocumentCount { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentSum { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentAverage { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentSplitCounts { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentSplitSums { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentSplitAverages { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentRankedEntries { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} diff --git a/packages/rs-sdk/src/platform/documents/mod.rs b/packages/rs-sdk/src/platform/documents/mod.rs index dbb6c5ae5ba..dd97c903b4d 100644 --- a/packages/rs-sdk/src/platform/documents/mod.rs +++ b/packages/rs-sdk/src/platform/documents/mod.rs @@ -1,27 +1,18 @@ -pub(super) mod average_proof_helpers; -pub(super) mod count_proof_helpers; -/// `Fetch` impl for the average-side aggregate result. Returns -/// `(count, sum)`; client divides. -pub mod document_average; -pub mod document_count; -pub mod document_history_query; -pub mod document_query; -/// `Fetch` impl for the ranked (`GROUP BY … ORDER BY LIMIT n -/// [OFFSET m]`) result — one entry per returned group, in ranking order, -/// plus the rank the page starts at. Requires an index declaring -/// `rankedCountable` / `rankedSummable` / `rankedAverageable` -/// (protocol version 14+). -pub mod document_ranked_entries; -/// `Fetch` impl for the average-side per-entry result. Mirrors -/// `document_split_sums`. -pub mod document_split_averages; -pub mod document_split_counts; -/// `Fetch` impl for the sum-side per-entry result. Mirrors -/// `document_split_counts`. -pub mod document_split_sums; -/// `Fetch` impl for the sum-side aggregate result. Mirrors -/// `document_count`. Lights up alongside grovedb PR 670. -pub mod document_sum; -pub(super) mod ranked_proof_helpers; -pub(super) mod sum_proof_helpers; +//! Document query surface. +//! +//! The transport-free core (query types, wire encoding, proof decoding) +//! lives in the `dash-platform-queries` crate and is re-exported here at +//! its historical paths; this module keeps the Sdk-bound pieces — `Fetch` +//! bindings, the contract-fetching constructor, and transition builders. + +pub use dash_platform_queries::documents::{ + document_average, document_count, document_history_query, document_query, + document_ranked_entries, document_split_averages, document_split_counts, document_split_sums, + document_sum, +}; + +pub mod document_query_sdk; +mod fetch_bindings; pub mod transitions; + +pub use document_query_sdk::DocumentQuerySdk; diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index 4d6ba1f660f..6de3e2950d1 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -2,6 +2,9 @@ mod contested_queries; mod queries; pub use contested_queries::ContestedDpnsUsername; +pub use dash_platform_queries::dpns_usernames::{ + convert_to_homograph_safe_chars, is_contested_username, is_valid_username, +}; pub use queries::DpnsUsername; use crate::platform::transition::put_document::PutDocument; @@ -21,20 +24,6 @@ use dpp::prelude::Identifier; use std::collections::BTreeMap; use std::sync::Arc; -/// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' -/// with '0', '1', and '1' respectively to prevent homograph attacks -pub fn convert_to_homograph_safe_chars(input: &str) -> String { - input - .chars() - .map(|c| match c { - 'o' | 'O' => '0', - 'i' | 'I' => '1', - 'l' | 'L' => '1', - _ => c.to_ascii_lowercase(), - }) - .collect() -} - fn extract_dpns_label(name: &str) -> &str { if let Some(dot_pos) = name.rfind('.') { let (label_part, suffix) = name.split_at(dot_pos); @@ -56,85 +45,6 @@ fn normalize_dpns_label(input: &str) -> String { convert_to_homograph_safe_chars(extract_dpns_label(input)) } -/// Check if a username is valid according to DPNS rules -/// -/// A username is valid if: -/// - It's between 3 and 63 characters long -/// - It starts and ends with alphanumeric characters (a-zA-Z0-9) -/// - It contains only alphanumeric characters and hyphens -/// - It doesn't have consecutive hyphens (enforced by the pattern) -/// -/// Pattern: `^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$` -/// -/// # Arguments -/// -/// * `label` - The username label to check (e.g., "alice") -/// -/// # Returns -/// -/// Returns `true` if the username is valid, `false` otherwise -pub fn is_valid_username(label: &str) -> bool { - // Check length - if label.len() < 3 || label.len() > 63 { - return false; - } - - let chars: Vec = label.chars().collect(); - - // Check first character (must be alphanumeric) - if !chars[0].is_ascii_alphanumeric() { - return false; - } - - // Check last character (must be alphanumeric) - if !chars[chars.len() - 1].is_ascii_alphanumeric() { - return false; - } - - // Check middle characters (can be alphanumeric or hyphen) - for &ch in &chars[1..chars.len() - 1] { - if !ch.is_ascii_alphanumeric() && ch != '-' { - return false; - } - } - - // Additional check: no consecutive hyphens (good practice) - for i in 0..chars.len() - 1 { - if chars[i] == '-' && chars[i + 1] == '-' { - return false; - } - } - - true -} - -/// Check if a username is contested (requires masternode voting) -/// -/// A username is contested if its normalized label: -/// - Is between 3 and 19 characters long (inclusive) -/// - Contains only lowercase letters a-z, digits 0-1, and hyphens -/// -/// # Arguments -/// -/// * `label` - The username label to check (e.g., "alice") -/// -/// # Returns -/// -/// Returns `true` if the username would be contested, `false` otherwise -pub fn is_contested_username(label: &str) -> bool { - let normalized = convert_to_homograph_safe_chars(label); - - // Check length - if normalized.len() < 3 || normalized.len() > 19 { - return false; - } - - // Check if all characters match the pattern [a-z01-] - normalized - .chars() - .all(|c| matches!(c, 'a'..='z' | '0' | '1' | '-')) -} - /// Hash a buffer twice using SHA256 (double SHA256) fn hash_double(data: Vec) -> [u8; 32] { use dpp::dashcore::hashes::{sha256d, Hash}; @@ -521,14 +431,6 @@ impl Sdk { mod tests { use super::*; - #[test] - fn test_convert_to_homograph_safe_chars() { - assert_eq!(convert_to_homograph_safe_chars("alice"), "a11ce"); - assert_eq!(convert_to_homograph_safe_chars("bob"), "b0b"); - assert_eq!(convert_to_homograph_safe_chars("COOL"), "c001"); - assert_eq!(convert_to_homograph_safe_chars("test123"), "test123"); - } - #[test] fn test_normalize_dpns_label_strips_dash_suffix_case_insensitively() { // Bare label and full name normalize to the same value, regardless @@ -562,89 +464,4 @@ mod tests { assert_eq!(extract_dpns_label("alice.eth"), "alice.eth"); assert_eq!(extract_dpns_label(".dash"), ""); } - - #[test] - fn test_is_valid_username() { - // Valid usernames - assert!(is_valid_username("abc")); - assert!(is_valid_username("alice")); - assert!(is_valid_username("Alice123")); - assert!(is_valid_username("dash-p2p")); - assert!(is_valid_username("test-name-123")); - assert!(is_valid_username("a-b-c")); - assert!(is_valid_username("user2024")); - assert!(is_valid_username("CryptoKing")); - assert!(is_valid_username("web3-developer")); - assert!(is_valid_username("a".repeat(63).as_str())); // Max length - - // Invalid - too short - assert!(!is_valid_username("ab")); - assert!(!is_valid_username("a")); - assert!(!is_valid_username("")); - - // Invalid - too long - assert!(!is_valid_username("a".repeat(64).as_str())); - - // Invalid - starts with hyphen - assert!(!is_valid_username("-alice")); - assert!(!is_valid_username("-test")); - - // Invalid - ends with hyphen - assert!(!is_valid_username("alice-")); - assert!(!is_valid_username("test-")); - - // Invalid - starts and ends with hyphen - assert!(!is_valid_username("-alice-")); - - // Invalid - contains invalid characters - assert!(!is_valid_username("alice_bob")); // underscore - assert!(!is_valid_username("alice.bob")); // dot - assert!(!is_valid_username("alice@dash")); // at sign - assert!(!is_valid_username("alice!")); // exclamation - assert!(!is_valid_username("alice bob")); // space - assert!(!is_valid_username("alice#1")); // hash - assert!(!is_valid_username("alice$")); // dollar - assert!(!is_valid_username("alice%20")); // percent - - // Invalid - consecutive hyphens - assert!(!is_valid_username("alice--bob")); - assert!(!is_valid_username("test---name")); - } - - #[test] - fn test_is_contested_username() { - // Contested usernames (3-19 chars, only [a-z01-]) - assert!(is_contested_username("abc")); - assert!(is_contested_username("alice")); // becomes "a11ce" - assert!(is_contested_username("b0b")); - assert!(is_contested_username("cool")); // becomes "c001" - assert!(is_contested_username("a-b-c")); - assert!(is_contested_username("hello")); // becomes "he110" - assert!(is_contested_username("world")); // becomes "w0r1d" - assert!(is_contested_username("dash")); - assert!(is_contested_username("a11ce")); // already normalized - assert!(is_contested_username("dash-dao")); // becomes "dash-da0" - - // Not contested - too short - assert!(!is_contested_username("ab")); - assert!(!is_contested_username("io")); // becomes "10" which is 2 chars - assert!(!is_contested_username("a")); - - // Not contested - too long (20+ chars) - assert!(!is_contested_username("twenty-characters-ab")); // 20 chars - assert!(!is_contested_username( - "this-is-a-very-long-username-that-exceeds-limit" - )); - - // Not contested - contains invalid characters after normalization - assert!(!is_contested_username("alice2")); // contains '2' - assert!(!is_contested_username("alice_bob")); // contains '_' - assert!(!is_contested_username("alice.bob")); // contains '.' - assert!(!is_contested_username("alice@dash")); // contains '@' - assert!(!is_contested_username("alice!")); // contains '!' - assert!(!is_contested_username("test123")); // contains '2' and '3' - assert!(!is_contested_username("dash-p2p")); // contains 'p' and '2' - assert!(!is_contested_username("user5")); // contains '5' - assert!(!is_contested_username("name_with_underscore")); // contains '_' - } } diff --git a/packages/rs-sdk/src/platform/identities_contract_keys_query.rs b/packages/rs-sdk/src/platform/identities_contract_keys_query.rs index 02ede03136f..e939e5b2d82 100644 --- a/packages/rs-sdk/src/platform/identities_contract_keys_query.rs +++ b/packages/rs-sdk/src/platform/identities_contract_keys_query.rs @@ -88,6 +88,8 @@ impl Query for IdentitiesContractKeysQuery { } } +impl crate::platform::query::WireQuery for IdentitiesContractKeysQuery {} + impl TransportRequest for IdentitiesContractKeysQuery { type Client = ::Client; type Response = ::Response; diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index fb52158c03d..b19e3b59837 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -99,8 +99,8 @@ pub trait Query: Send + Debug + Clone { /// /// * `settings` - A [`QuerySettings`](crate::platform::QuerySettings) borrowing the encoder /// inputs from the SDK: protocol version (used by encoders that pick wire shapes - /// per version — today only [`DocumentQuery`]'s V0/V1 split), `prove` flag, - /// and request settings. Construct from an SDK via + /// per version — today only [`DocumentQuery`]'s V0/V1 split) and the `prove` flag. + /// Construct from an SDK via /// [`Sdk::query_settings`](crate::Sdk::query_settings), or directly in unit tests /// that want to exercise the encoder without spinning up an `Sdk`. /// @@ -110,9 +110,97 @@ pub trait Query: Send + Debug + Clone { fn query(&self, settings: &crate::platform::QuerySettings<'_>) -> Result; } +/// Marker for wire proto request types that serve as their own [`Query`] +/// through the blanket identity impl below. +/// +/// This local marker exists for trait coherence: [`DocumentQuery`] moved to +/// the transport-free `dash-platform-queries` crate, so it is now foreign to +/// this crate. A blanket bounded only by the (equally foreign) +/// [`TransportRequest`] trait would conflict with the explicit +/// `impl Query for DocumentQuery` — rustc must assume some +/// future upstream crate could implement `TransportRequest` for +/// `DocumentQuery`. Because `WireQuery` is local and only ever implemented +/// explicitly (never via a blanket), the compiler can prove the two impl +/// sets disjoint. +/// +/// When adding a new endpoint whose request proto is used directly as its +/// own query (`Fetch::Query = Fetch::Request`), add the proto to the +/// `impl_wire_query!` list below; a missing entry fails to compile at the +/// fetch call site with a `WireQuery is not satisfied` error. +pub trait WireQuery {} + +macro_rules! impl_wire_query { + ($($request:ty),+ $(,)?) => { + $(impl WireQuery for $request {})+ + }; +} + +impl_wire_query!( + proto::BroadcastStateTransitionRequest, + proto::GetAddressInfoRequest, + proto::GetAddressesBranchStateRequest, + proto::GetAddressesInfosRequest, + proto::GetAddressesTrunkStateRequest, + proto::GetConsensusParamsRequest, + proto::GetContestedResourceIdentityVotesRequest, + proto::GetContestedResourceVoteStateRequest, + proto::GetContestedResourceVotersForIdentityRequest, + proto::GetContestedResourcesRequest, + proto::GetCurrentQuorumsInfoRequest, + proto::GetDataContractHistoryRequest, + proto::GetDataContractRequest, + proto::GetDataContractsRequest, + proto::GetDocumentHistoryRequest, + proto::GetDocumentsRequest, + proto::GetEpochsInfoRequest, + proto::GetEvonodesProposedEpochBlocksByIdsRequest, + proto::GetEvonodesProposedEpochBlocksByRangeRequest, + proto::GetFinalizedEpochInfosRequest, + proto::GetGroupActionSignersRequest, + proto::GetGroupActionsRequest, + proto::GetGroupInfoRequest, + proto::GetGroupInfosRequest, + proto::GetIdentitiesBalancesRequest, + proto::GetIdentitiesContractKeysRequest, + proto::GetIdentitiesTokenBalancesRequest, + proto::GetIdentitiesTokenInfosRequest, + proto::GetIdentityBalanceAndRevisionRequest, + proto::GetIdentityBalanceRequest, + proto::GetIdentityByNonUniquePublicKeyHashRequest, + proto::GetIdentityByPublicKeyHashRequest, + proto::GetIdentityContractNonceRequest, + proto::GetIdentityKeysRequest, + proto::GetIdentityNonceRequest, + proto::GetIdentityRequest, + proto::GetIdentityTokenBalancesRequest, + proto::GetIdentityTokenInfosRequest, + proto::GetMostRecentShieldedAnchorRequest, + proto::GetPathElementsRequest, + proto::GetPrefundedSpecializedBalanceRequest, + proto::GetProtocolVersionUpgradeStateRequest, + proto::GetProtocolVersionUpgradeVoteStatusRequest, + proto::GetRecentAddressBalanceChangesRequest, + proto::GetRecentCompactedAddressBalanceChangesRequest, + proto::GetShieldedAnchorsRequest, + proto::GetShieldedEncryptedNotesRequest, + proto::GetShieldedNotesCountRequest, + proto::GetShieldedNullifiersRequest, + proto::GetShieldedPoolStateRequest, + proto::GetStatusRequest, + proto::GetTokenContractInfoRequest, + proto::GetTokenDirectPurchasePricesRequest, + proto::GetTokenPerpetualDistributionLastClaimRequest, + proto::GetTokenPreProgrammedDistributionsRequest, + proto::GetTokenStatusesRequest, + proto::GetTokenTotalSupplyRequest, + proto::GetTotalCreditsInPlatformRequest, + proto::GetVotePollsByEndDateRequest, + proto::WaitForStateTransitionResultRequest, +); + impl Query for T where - T: TransportRequest + Sized + Send + Sync + Clone + Debug, + T: TransportRequest + WireQuery + Sized + Send + Sync + Clone + Debug, T::Response: Send + Sync + Debug, { fn query(&self, settings: &crate::platform::QuerySettings<'_>) -> Result { diff --git a/packages/rs-sdk/src/platform/query_settings.rs b/packages/rs-sdk/src/platform/query_settings.rs deleted file mode 100644 index f5741dbe04a..00000000000 --- a/packages/rs-sdk/src/platform/query_settings.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Query encoding settings. -//! -//! [`QuerySettings`] is a small, borrow-style bundle handed to -//! [`crate::platform::query::Query::query`] implementations so they can encode -//! a user-facing query into a wire `TransportRequest` without taking a full -//! `&Sdk` dependency. This keeps the encoder layer free of `Sdk`-shaped -//! transitive deps (transport, mock cache, nonce cache, context provider, …) -//! and lets unit tests construct settings directly without spinning up -//! `Sdk::new_mock()`. -//! -//! The fields are the minimum surface a wire encoder needs today: -//! protocol version (to pick V0 vs V1 wire shapes), the `prove` flag -//! (proof-mode requests vs unproved queries), and a borrowed -//! [`RequestSettings`] for any future encoder that needs to consult -//! transport-layer hints (timeouts, ban policy, …) — none do today, but -//! it costs nothing to thread through and avoids another trait churn -//! when the first encoder needs it. - -use dpp::version::PlatformVersion; -use rs_dapi_client::RequestSettings; - -/// Settings passed to [`crate::platform::query::Query::query`] for encoding a -/// user-facing query into a wire `TransportRequest`. -/// -/// Construct via [`crate::Sdk::query_settings`] for normal use, or directly in -/// unit tests that want to exercise the encoder without an `Sdk`. -#[derive(Debug, Clone, Copy)] -pub struct QuerySettings<'a> { - /// Transport-layer settings (timeouts, retries, TLS, ban behaviour). - /// Not consulted by any current encoder; threaded for forward compatibility. - pub request_settings: &'a RequestSettings, - - /// Platform protocol version, used to pick wire encoding (V0 vs V1, etc). - pub protocol_version: &'a PlatformVersion, - - /// Whether to request and verify cryptographic proofs. - pub prove: bool, -} - -impl<'a> QuerySettings<'a> { - /// Cheap derivative with proofs forced off — used by `FetchUnproved`. - pub fn without_proofs(&self) -> Self { - Self { - prove: false, - ..*self - } - } -} diff --git a/packages/rs-sdk/src/platform/transition/validation.rs b/packages/rs-sdk/src/platform/transition/validation.rs index 846d9ddae2d..d095afb0bb5 100644 --- a/packages/rs-sdk/src/platform/transition/validation.rs +++ b/packages/rs-sdk/src/platform/transition/validation.rs @@ -1,42 +1,7 @@ -use crate::Error; -use dpp::{ - consensus::{basic::BasicError, ConsensusError}, - state_transition::{StateTransition, StateTransitionStructureValidation}, - version::PlatformVersion, -}; - -/// Checks if an error is an UnsupportedFeatureError -fn is_unsupported_feature_error(error: &ConsensusError) -> bool { - matches!( - error, - ConsensusError::BasicError(BasicError::UnsupportedFeatureError(_)) - ) -} - -/// Ensures a state transition passes structure validation before broadcasting. -/// -/// Note: UnsupportedFeatureError is allowed to pass through, as it indicates -/// that structure validation is not implemented for that state transition type -/// (e.g., identity-based state transitions). The platform will still perform -/// validation during execution. -pub(crate) fn ensure_valid_state_transition_structure( - state_transition: &StateTransition, - platform_version: &PlatformVersion, -) -> Result<(), Error> { - let validation_result = state_transition.validate_structure(platform_version); - if validation_result.is_valid() { - Ok(()) - } else { - // Allow UnsupportedFeatureError to pass through - this means structure - // validation is not implemented for this state transition type - let all_unsupported_feature_errors = validation_result - .errors - .iter() - .all(is_unsupported_feature_error); - if all_unsupported_feature_errors { - Ok(()) - } else { - Err(validation_result.into()) - } - } -} +//! Re-export of the transport-free structure validation helper. +//! +//! The implementation moved to `dash-platform-queries`; broadcast paths in +//! this crate keep importing it from here. It returns the query core's +//! error type, which converts into [`crate::Error`] via `From` at the `?` +//! call sites. +pub(crate) use dash_platform_queries::transition::validation::ensure_valid_state_transition_structure; diff --git a/packages/rs-sdk/src/platform/types/epoch.rs b/packages/rs-sdk/src/platform/types/epoch.rs index 4cbbcf8fe68..b73c271d9c4 100644 --- a/packages/rs-sdk/src/platform/types/epoch.rs +++ b/packages/rs-sdk/src/platform/types/epoch.rs @@ -249,11 +249,9 @@ mod tests { use super::*; use dapi_grpc::platform::v0::get_epochs_info_request; use dpp::block::epoch::EPOCH_KEY_OFFSET; - use rs_dapi_client::RequestSettings; - fn query_settings(request_settings: &RequestSettings) -> crate::platform::QuerySettings<'_> { + fn query_settings() -> crate::platform::QuerySettings<'static> { crate::platform::QuerySettings { - request_settings, protocol_version: dpp::version::PlatformVersion::latest(), prove: true, } @@ -270,8 +268,7 @@ mod tests { /// returning elements. #[test] fn should_build_current_epoch_queries_verifiable_without_metadata() { - let request_settings = RequestSettings::default(); - let settings = query_settings(&request_settings); + let settings = query_settings(); // Step 1: the probe is ascending with an explicit genesis start. let probe = current_epoch_probe_query() diff --git a/packages/rs-sdk/src/platform/types/evonode.rs b/packages/rs-sdk/src/platform/types/evonode.rs index 1ccc5553f4f..4bba5ca8e10 100644 --- a/packages/rs-sdk/src/platform/types/evonode.rs +++ b/packages/rs-sdk/src/platform/types/evonode.rs @@ -51,6 +51,8 @@ impl Mockable for EvoNode { serde_json::ser::to_vec(self).ok() } } +impl crate::platform::query::WireQuery for EvoNode {} + impl TransportRequest for EvoNode { type Client = PlatformGrpcClient; type Response = proto::GetStatusResponse; diff --git a/packages/rs-sdk/src/platform/types/finalized_epoch.rs b/packages/rs-sdk/src/platform/types/finalized_epoch.rs index e9e25f8071d..134943ec707 100644 --- a/packages/rs-sdk/src/platform/types/finalized_epoch.rs +++ b/packages/rs-sdk/src/platform/types/finalized_epoch.rs @@ -4,40 +4,7 @@ use crate::Error; use dapi_grpc::platform::v0::{get_finalized_epoch_infos_request, GetFinalizedEpochInfosRequest}; use dpp::block::epoch::EpochIndex; -/// Query used to fetch multiple finalized epochs from Platform. -#[derive(Clone, Debug)] -pub struct FinalizedEpochQuery { - /// Starting epoch index. - pub start_epoch_index: EpochIndex, - /// Whether to include the start epoch. - pub start_epoch_index_included: bool, - /// Ending epoch index. - pub end_epoch_index: EpochIndex, - /// Whether to include the end epoch. - pub end_epoch_index_included: bool, -} - -impl Default for FinalizedEpochQuery { - fn default() -> Self { - Self { - start_epoch_index: 0, - start_epoch_index_included: true, - end_epoch_index: 0, - end_epoch_index_included: true, - } - } -} - -impl From<(EpochIndex, EpochIndex)> for FinalizedEpochQuery { - fn from((start, end): (EpochIndex, EpochIndex)) -> Self { - Self { - start_epoch_index: start, - start_epoch_index_included: true, - end_epoch_index: end, - end_epoch_index_included: true, - } - } -} +pub use dash_platform_queries::types::finalized_epoch::FinalizedEpochQuery; impl Query for FinalizedEpochQuery { fn query( diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index a9f76afbf5d..87be5dd9c12 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -581,15 +581,14 @@ impl Sdk { self.proofs } - /// Build a [`QuerySettings`] borrowing this SDK's protocol version, - /// request settings, and `prove` flag. + /// Build a [`QuerySettings`] borrowing this SDK's protocol version + /// and `prove` flag. /// /// Hand the resulting context to [`crate::platform::Query::query`] when /// you need to encode a user-facing query into a wire `TransportRequest` /// without taking a full `&Sdk` dependency through the encoder layer. pub fn query_settings(&self) -> crate::platform::QuerySettings<'_> { crate::platform::QuerySettings { - request_settings: &self.dapi_client_settings, protocol_version: self.version(), prove: self.prove(), } diff --git a/packages/rs-sdk/tests/fetch/common.rs b/packages/rs-sdk/tests/fetch/common.rs index b9ff7a69174..babb610e141 100644 --- a/packages/rs-sdk/tests/fetch/common.rs +++ b/packages/rs-sdk/tests/fetch/common.rs @@ -197,9 +197,7 @@ pub(crate) async fn setup_sdk_for_test_case (String, Sdk) { - let request_settings = rs_dapi_client::RequestSettings::default(); let settings = QuerySettings { - request_settings: &request_settings, protocol_version: dpp::version::PlatformVersion::latest(), prove: true, }; diff --git a/packages/rs-sdk/tests/fetch/document.rs b/packages/rs-sdk/tests/fetch/document.rs index df3b3da576f..c786bcd2577 100644 --- a/packages/rs-sdk/tests/fetch/document.rs +++ b/packages/rs-sdk/tests/fetch/document.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use super::{common::setup_logs, config::Config}; -use dash_sdk::platform::{DocumentQuery, Fetch, FetchMany}; +use dash_sdk::platform::{documents::DocumentQuerySdk, DocumentQuery, Fetch, FetchMany}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::document::{Document, DocumentV0Getters}; use dpp::platform_value::string_encoding::Encoding; diff --git a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs index 65fdb6994c0..e576b5463c9 100644 --- a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs +++ b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs @@ -140,9 +140,10 @@ fn v0_wire_shape_with_forced_v0_platform_version() { #[test] fn v0_rejects_count_star_projection() { let q = build_basic_document_query().with_select(SelectProjection::count_star()); - let err = q - .try_into_request_for_version(v0_dispatch_version()) - .expect_err("count_star on v0 must reject"); + let err = SdkError::from( + q.try_into_request_for_version(v0_dispatch_version()) + .expect_err("count_star on v0 must reject"), + ); match err { SdkError::Config(msg) => assert!( msg.contains("v3.1+"), @@ -155,9 +156,10 @@ fn v0_rejects_count_star_projection() { #[test] fn v0_rejects_group_by() { let q = build_basic_document_query().with_group_by("a"); - let err = q - .try_into_request_for_version(v0_dispatch_version()) - .expect_err("group_by on v0 must reject"); + let err = SdkError::from( + q.try_into_request_for_version(v0_dispatch_version()) + .expect_err("group_by on v0 must reject"), + ); assert!(matches!(err, SdkError::Config(_))); } @@ -174,25 +176,23 @@ fn v0_rejects_having() { operator: HavingOperator::GreaterThan, right: HavingRightOperand::Value(Value::U64(0)), }]); - let err = q - .try_into_request_for_version(v0_dispatch_version()) - .expect_err("having on v0 must reject"); + let err = SdkError::from( + q.try_into_request_for_version(v0_dispatch_version()) + .expect_err("having on v0 must reject"), + ); assert!(matches!(err, SdkError::Config(_))); } #[test] fn encoder_dispatches_v0_via_query_settings_without_sdk() { use dash_sdk::platform::{Query, QuerySettings}; - use rs_dapi_client::RequestSettings; // The whole point of QuerySettings: encoder is testable without // `Sdk::new_mock()`. Construct the context directly from a // PlatformVersion whose document_query is pinned to V0 dispatch // and assert the wire shape comes out V0. let v0_pv = v0_dispatch_version(); - let request_settings = RequestSettings::default(); let settings = QuerySettings { - request_settings: &request_settings, protocol_version: v0_pv, prove: true, }; @@ -206,7 +206,6 @@ fn encoder_dispatches_v0_via_query_settings_without_sdk() { // Same query, latest PlatformVersion (V1 dispatch) — should now // emit V1 wire bytes through the same code path. let latest_settings = QuerySettings { - request_settings: &request_settings, protocol_version: PlatformVersion::latest(), prove: true, }; @@ -261,12 +260,9 @@ fn protocol_version_for_v3_1_dev_keeps_document_query_v1() { #[test] fn document_query_dispatches_v0_when_sdk_initial_version_is_v3_0_pv() { use dash_sdk::platform::{Query, QuerySettings}; - use rs_dapi_client::RequestSettings; let pv_v3_0 = PlatformVersion::get(11).expect("PROTOCOL_VERSION_11 exists"); - let request_settings = RequestSettings::default(); let settings = QuerySettings { - request_settings: &request_settings, protocol_version: pv_v3_0, prove: true, }; diff --git a/packages/rs-sdk/tests/fetch/mock_fetch.rs b/packages/rs-sdk/tests/fetch/mock_fetch.rs index a8c98b4d575..f7f15427064 100644 --- a/packages/rs-sdk/tests/fetch/mock_fetch.rs +++ b/packages/rs-sdk/tests/fetch/mock_fetch.rs @@ -2,7 +2,7 @@ use super::common::{bootstrap_mock_sdk_to_latest, mock_data_contract, mock_document_type}; use dash_sdk::{ - platform::{DocumentQuery, Fetch}, + platform::{documents::DocumentQuerySdk, DocumentQuery, Fetch}, Sdk, SdkBuilder, }; use dpp::{ diff --git a/packages/rs-sdk/tests/fetch/tokens/token_contract_info.rs b/packages/rs-sdk/tests/fetch/tokens/token_contract_info.rs index 21afd47ee49..b1d96ab041f 100644 --- a/packages/rs-sdk/tests/fetch/tokens/token_contract_info.rs +++ b/packages/rs-sdk/tests/fetch/tokens/token_contract_info.rs @@ -4,7 +4,6 @@ use dash_sdk::platform::{Fetch, Identifier, Query, QuerySettings}; use dash_sdk::Sdk; use dpp::tokens::contract_info::TokenContractInfo; use dpp::version::PlatformVersion; -use rs_dapi_client::RequestSettings; #[tokio::test] async fn test_token_contract_info_fetch_by_identifier() { @@ -51,9 +50,7 @@ async fn test_token_contract_info_query_prove_true() { let token_id = Identifier::from_bytes(&[3u8; 32]).unwrap(); let query = TokenContractInfoQuery { token_id }; - let request_settings = RequestSettings::default(); let settings = QuerySettings { - request_settings: &request_settings, protocol_version: PlatformVersion::latest(), prove: true, }; @@ -72,9 +69,7 @@ async fn test_token_contract_info_query_prove_false() { let token_id = Identifier::from_bytes(&[4u8; 32]).unwrap(); let query = TokenContractInfoQuery { token_id }; - let request_settings = RequestSettings::default(); let settings = QuerySettings { - request_settings: &request_settings, protocol_version: PlatformVersion::latest(), prove: false, }; diff --git a/packages/wasm-sdk/src/error.rs b/packages/wasm-sdk/src/error.rs index e37159fa6c8..bebc02a33d2 100644 --- a/packages/wasm-sdk/src/error.rs +++ b/packages/wasm-sdk/src/error.rs @@ -121,6 +121,15 @@ impl WasmSdkError { } } +impl From for WasmSdkError { + fn from(err: dash_sdk::dash_platform_queries::Error) -> Self { + // Route through the SDK's own conversion so the transport-free query + // core's errors keep the exact mapping they had when they were + // `SdkError` variants. + SdkError::from(err).into() + } +} + impl From for WasmSdkError { fn from(err: SdkError) -> Self { use SdkError::*; From f9a70c1d43cab6d1f58f5e58c911bc8203e588a3 Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 7 Aug 2026 18:08:35 -0500 Subject: [PATCH 3/8] refactor(sdk): decode document queries from the wire request in shared client code The proto-to-domain decoding for document queries existed only server-side (rs-drive-abci's v1 conversions), so a client verifying a documents proof had to reconstruct the query shape by hand and could silently drift from what the server actually proves. Move the decode logic into dash-platform-queries::documents::proto_conversions with a neutral error type; drive-abci's conversions module becomes a thin mapping onto its QueryError surface with identical error message strings. On top of the shared decoder, DocumentQuery::try_from_request(request, contract) reconstructs the rich query from the wire request (both request versions), and verify_documents_response(...) / verify_documents_response_with_provider_contract(...) give embedders a request-driven verification entry point that delegates to the existing FromProof machinery, resolving the contract explicitly or via ContextProvider::get_data_contract. Round-trip tests cover encode-decode equality for representative queries in both wire versions plus malformed-clause rejection; drive-abci's document_query unit tests pass unchanged (76 cases). --- Cargo.lock | 3 + packages/dash-platform-queries/Cargo.toml | 1 + .../src/documents/document_query.rs | 336 +++++++++++++++ .../src/documents/mod.rs | 5 + .../src/documents/proto_conversions.rs | 372 +++++++++++++++++ packages/dash-platform-queries/src/error.rs | 16 + .../tests/document_query_wire_roundtrip.rs | 293 ++++++++++++++ packages/rs-drive-abci/Cargo.toml | 1 + .../query/document_query/v1/conversions.rs | 382 +++--------------- 9 files changed, 1073 insertions(+), 336 deletions(-) create mode 100644 packages/dash-platform-queries/src/documents/proto_conversions.rs create mode 100644 packages/dash-platform-queries/tests/document_query_wire_roundtrip.rs diff --git a/Cargo.lock b/Cargo.lock index 5b7495f8e67..b39696d1e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,6 +1706,7 @@ dependencies = [ name = "dash-platform-queries" version = "4.1.0" dependencies = [ + "ciborium", "dapi-grpc", "dash-context-provider", "dash-platform-macros", @@ -2232,6 +2233,7 @@ dependencies = [ "console-subscriber", "dapi-grpc", "dash-platform-macros", + "dash-platform-queries", "delegate", "derive_more 1.0.0", "dotenvy", @@ -2292,6 +2294,7 @@ dependencies = [ "platform-serialization", "platform-serialization-derive", "serde", + "serde_json", "tenderdash-abci", "thiserror 2.0.18", "tracing", diff --git a/packages/dash-platform-queries/Cargo.toml b/packages/dash-platform-queries/Cargo.toml index f3d76e7c747..7c9870a7392 100644 --- a/packages/dash-platform-queries/Cargo.toml +++ b/packages/dash-platform-queries/Cargo.toml @@ -17,6 +17,7 @@ mocks = [ ] [dependencies] +ciborium = { version = "0.2.2" } dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ "platform", "client", diff --git a/packages/dash-platform-queries/src/documents/document_query.rs b/packages/dash-platform-queries/src/documents/document_query.rs index 8f5fd18a96a..86a9e4c1b46 100644 --- a/packages/dash-platform-queries/src/documents/document_query.rs +++ b/packages/dash-platform-queries/src/documents/document_query.rs @@ -2,6 +2,7 @@ use std::sync::Arc; +use super::proto_conversions; use crate::error::Error; use dapi_grpc::platform::v0::get_documents_request::Version::{V0, V1}; use dapi_grpc::platform::v0::{ @@ -366,6 +367,341 @@ impl DocumentQuery { ) -> Result { GetDocumentsRequest::try_from_platform_versioned(self, platform_version) } + + /// Decode a wire-format [`GetDocumentsRequest`] back into a rich + /// [`DocumentQuery`] — the inverse of + /// [`Self::try_into_request_for_version`], and the piece that lets + /// an embedder verify a proved response given only the request + /// bytes it sent (see [`verify_documents_response`]). + /// + /// Both wire versions are handled, mirroring how the server + /// decodes each: + /// - **V0** carries `where` / `order_by` as CBOR-encoded arrays of + /// clause components; they are decoded exactly as + /// rs-drive-abci's `query_documents_v0` does (ciborium → + /// `Value::Array` → `WhereClause::from_components` / + /// `OrderClause::from_components`). V0 has no `select` / + /// `group_by` / `having` / `offset`; those default to the + /// documents-fetch shape. + /// - **V1** carries typed proto clauses; they are decoded through + /// the same [`proto_conversions`](super::proto_conversions) + /// functions the server's v1 handler runs, so client and server + /// cannot disagree on what the bytes mean. Multi-projection + /// `selects` (len > 1) is rejected — a `DocumentQuery` carries a + /// single projection, matching what the server evaluates. + /// `limit: Some(0)` is rejected, mirroring the server's uniform + /// `InvalidLimit` contract (`None` = server default → `0` + /// sentinel here; only positive caps are representable). + /// + /// The `prove` flag is intentionally ignored: `DocumentQuery` has + /// no prove field (its encoders always set `prove: true`, because + /// the `FromProof` decoders only handle proved responses). + /// + /// `contract` must be the data contract the request targets — the + /// request's `data_contract_id` is checked against `contract.id()` + /// and the named document type must exist on it. + pub fn try_from_request( + request: GetDocumentsRequest, + contract: Arc, + ) -> Result { + match request.version { + Some(V0(request_v0)) => Self::try_from_request_v0(request_v0, contract), + Some(V1(request_v1)) => Self::try_from_request_v1(request_v1, contract), + None => Err(Error::Protocol(ProtocolError::DecodingError( + "GetDocumentsRequest has no version set".to_string(), + ))), + } + } + + fn try_from_request_v0( + request: GetDocumentsRequestV0, + contract: Arc, + ) -> Result { + let GetDocumentsRequestV0 { + data_contract_id, + document_type, + r#where, + order_by, + limit, + // See `try_from_request`: DocumentQuery has no prove field. + prove: _, + start, + } = request; + + check_request_targets_contract(&contract, &data_contract_id, &document_type)?; + + let where_clauses = where_clauses_from_cbor(&r#where)?; + let order_by_clauses = order_clauses_from_cbor(&order_by)?; + + Ok(Self { + select: SelectProjection::documents(), + data_contract: contract, + document_type_name: document_type, + where_clauses, + group_by: Vec::new(), + having: Vec::new(), + order_by_clauses, + // V0's plain `uint32` uses the same `0` = "unset" sentinel + // as this struct — pass through. + limit, + offset: None, + start, + }) + } + + fn try_from_request_v1( + request: GetDocumentsRequestV1, + contract: Arc, + ) -> Result { + let GetDocumentsRequestV1 { + data_contract_id, + document_type, + where_clauses, + order_by, + limit, + start, + // See `try_from_request`: DocumentQuery has no prove field. + prove: _, + selects, + group_by, + having, + offset, + } = request; + + check_request_targets_contract(&contract, &data_contract_id, &document_type)?; + + let where_clauses = proto_conversions::where_clauses_from_proto(where_clauses)?; + let order_by_clauses = proto_conversions::order_clauses_from_proto(order_by)?; + let having = proto_conversions::having_clauses_from_proto(having)?; + + // Same shape the server's v1 handler accepts: 0 selects → + // default documents projection, 1 select → decode it, more → + // reject (a `DocumentQuery` carries a single projection; + // multi-projection is wire-only today and the server refuses + // it too). + if selects.len() > 1 { + return Err(Error::Protocol(ProtocolError::DecodingError(format!( + "multi-projection SELECT is not supported: a DocumentQuery carries a \ + single projection, got {} selects", + selects.len() + )))); + } + let select = selects + .into_iter() + .next() + .map(proto_conversions::select_from_proto) + .transpose()? + .unwrap_or_else(SelectProjection::documents); + + // Mirror the server's uniform v1 limit contract: `None` = use + // the server default (the `0` sentinel here), positive = + // explicit cap, `Some(0)` invalid (and unrepresentable — this + // struct's `0` means "unset"). + let limit = match limit { + None => 0, + Some(0) => { + return Err(Error::Protocol(ProtocolError::DecodingError( + "limit = 0 is not a valid wire value on the v1 `optional uint32` \ + field; omit `limit` (None) to use the server's default, or pass \ + a positive integer for an explicit cap" + .to_string(), + ))); + } + Some(n) => n, + }; + + // V1 ships its own `Start` enum with the same shape as V0's; + // this struct stores the V0 type (see `encode_v1` for the + // inverse translation). + let start = start.map(|s| match s { + V1Start::StartAfter(b) => Start::StartAfter(b), + V1Start::StartAt(b) => Start::StartAt(b), + }); + + Ok(Self { + select, + data_contract: contract, + document_type_name: document_type, + where_clauses, + group_by, + having, + order_by_clauses, + limit, + offset, + start, + }) + } +} + +/// Shared request-vs-contract consistency check for both wire +/// versions: the request must target the supplied contract, and the +/// named document type must exist on it. +fn check_request_targets_contract( + contract: &DataContract, + data_contract_id: &[u8], + document_type_name: &str, +) -> Result<(), Error> { + if data_contract_id != contract.id().as_slice() { + return Err(Error::Protocol(ProtocolError::DecodingError(format!( + "GetDocumentsRequest targets data contract {} but the supplied contract is {}", + hex::encode(data_contract_id), + contract.id() + )))); + } + contract + .document_type_for_name(document_type_name) + .map_err(ProtocolError::DataContractError)?; + Ok(()) +} + +/// Decode a V0 `where` field — CBOR bytes carrying an array of +/// `[field, operator, value]` component arrays — into structured +/// clauses. Byte-for-byte mirror of the decode the server's +/// `query_documents_v0` runs (empty bytes → no clauses; anything +/// else must be a CBOR array of arrays). +fn where_clauses_from_cbor(bytes: &[u8]) -> Result, Error> { + if bytes.is_empty() { + return Ok(Vec::new()); + } + let value: Value = ciborium::de::from_reader(bytes).map_err(|_| { + Error::Protocol(ProtocolError::DecodingError( + "unable to decode 'where' query from cbor".to_string(), + )) + })?; + match value { + Value::Null => Ok(Vec::new()), + Value::Array(clauses) => clauses + .iter() + .map(|wc| match wc { + Value::Array(components) => { + WhereClause::from_components(components).map_err(Error::Drive) + } + _ => Err(Error::Protocol(ProtocolError::DecodingError( + "where clause must be an array".to_string(), + ))), + }) + .collect(), + _ => Err(Error::Protocol(ProtocolError::DecodingError( + "where clause must be an array".to_string(), + ))), + } +} + +/// Decode a V0 `order_by` field — CBOR bytes carrying an array of +/// `[field, "asc"|"desc"]` component arrays — into structured +/// clauses. Mirror of the server-side decode, like +/// [`where_clauses_from_cbor`]. +fn order_clauses_from_cbor(bytes: &[u8]) -> Result, Error> { + if bytes.is_empty() { + return Ok(Vec::new()); + } + let value: Value = ciborium::de::from_reader(bytes).map_err(|_| { + Error::Protocol(ProtocolError::DecodingError( + "unable to decode 'order_by' query from cbor".to_string(), + )) + })?; + match value { + Value::Null => Ok(Vec::new()), + Value::Array(clauses) => clauses + .iter() + .map(|oc| match oc { + Value::Array(components) => { + OrderClause::from_components(components).map_err(|_| { + Error::Protocol(ProtocolError::DecodingError( + "invalid order_by clause components".to_string(), + )) + }) + } + _ => Err(Error::Protocol(ProtocolError::DecodingError( + "order_by clause must be an array".to_string(), + ))), + }) + .collect(), + _ => Err(Error::Protocol(ProtocolError::DecodingError( + "order_by must be an array".to_string(), + ))), + } +} + +/// Embedder entry point: verify a proved [`GetDocumentsResponse`] +/// directly against the wire request that produced it. +/// +/// This is the transport-free glue an embedder needs when it drives +/// its own transport: it holds the `GetDocumentsRequest` it sent and +/// the `GetDocumentsResponse` it got back, and this function does the +/// rest — decodes the request into a [`DocumentQuery`] (via +/// [`DocumentQuery::try_from_request`], on the same shared decoders +/// the server runs) and delegates to the existing +/// [`FromProof`] machinery, which resolves the +/// [`DriveDocumentQuery`] internally and cryptographically verifies +/// the proof against it. +/// +/// `contract` must be the data contract the request targets. If the +/// embedder's [`ContextProvider`] can resolve contracts, use +/// [`verify_documents_response_with_provider_contract`] instead and +/// skip the explicit parameter. +pub fn verify_documents_response( + request: GetDocumentsRequest, + contract: Arc, + response: platform_proto::GetDocumentsResponse, + network: Network, + platform_version: &PlatformVersion, + provider: &dyn ContextProvider, +) -> Result<(Option, ResponseMetadata, Proof), drive_proof_verifier::Error> { + let query = DocumentQuery::try_from_request(request, contract).map_err(|e| { + drive_proof_verifier::Error::RequestError { + error: format!("failed to decode GetDocumentsRequest into a DocumentQuery: {e}"), + } + })?; + >::maybe_from_proof_with_metadata( + query, + response, + network, + platform_version, + provider, + ) +} + +/// Variant of [`verify_documents_response`] that resolves the data +/// contract through the [`ContextProvider`] +/// ([`ContextProvider::get_data_contract`]) instead of taking it as a +/// parameter — for embedders whose provider already caches or fetches +/// contracts. +pub fn verify_documents_response_with_provider_contract( + request: GetDocumentsRequest, + response: platform_proto::GetDocumentsResponse, + network: Network, + platform_version: &PlatformVersion, + provider: &dyn ContextProvider, +) -> Result<(Option, ResponseMetadata, Proof), drive_proof_verifier::Error> { + let contract_id_bytes = match &request.version { + Some(V0(v0)) => v0.data_contract_id.as_slice(), + Some(V1(v1)) => v1.data_contract_id.as_slice(), + None => { + return Err(drive_proof_verifier::Error::RequestError { + error: "GetDocumentsRequest has no version set".to_string(), + }); + } + }; + let contract_id = Identifier::from_bytes(contract_id_bytes).map_err(|e| { + drive_proof_verifier::Error::RequestError { + error: format!("invalid data_contract_id in GetDocumentsRequest: {e}"), + } + })?; + let contract = provider + .get_data_contract(&contract_id, platform_version) + .map_err(drive_proof_verifier::Error::ContextProviderError)? + .ok_or_else(|| drive_proof_verifier::Error::RequestError { + error: format!("context provider has no data contract {contract_id}"), + })?; + verify_documents_response( + request, + contract, + response, + network, + platform_version, + provider, + ) } impl FromProof for Document { diff --git a/packages/dash-platform-queries/src/documents/mod.rs b/packages/dash-platform-queries/src/documents/mod.rs index caabab7e85a..6015390cfbf 100644 --- a/packages/dash-platform-queries/src/documents/mod.rs +++ b/packages/dash-platform-queries/src/documents/mod.rs @@ -22,5 +22,10 @@ pub mod document_split_sums; /// `FromProof` impl for the sum-side aggregate result. Mirrors /// `document_count`. Lights up alongside grovedb PR 670. pub mod document_sum; +/// Shared wire-proto → drive-type decoders for `getDocuments`, +/// used by both rs-drive-abci (server request decode) and +/// [`document_query::DocumentQuery::try_from_request`] (client +/// verification) so the two directions cannot drift. +pub mod proto_conversions; pub(crate) mod ranked_proof_helpers; pub(crate) mod sum_proof_helpers; diff --git a/packages/dash-platform-queries/src/documents/proto_conversions.rs b/packages/dash-platform-queries/src/documents/proto_conversions.rs new file mode 100644 index 00000000000..a368a38e7b7 --- /dev/null +++ b/packages/dash-platform-queries/src/documents/proto_conversions.rs @@ -0,0 +1,372 @@ +//! Wire-protobuf → drive type conversions for the `getDocuments` +//! query surface. +//! +//! This is the **single** proto-decode implementation, shared by: +//! - rs-drive-abci's v1 request handler (server side — decodes the +//! incoming request before routing/execution), and +//! - [`DocumentQuery::try_from_request`](super::document_query::DocumentQuery::try_from_request) +//! (client side — rebuilds the rich query from the wire request so +//! a proved response can be verified against exactly what was +//! asked). +//! +//! Both directions living on one implementation is the point: the +//! bytes the server decodes and the bytes the verifier decodes must +//! agree clause-for-clause, or a proof could verify against a +//! different query than the server answered. +//! +//! Conversion contract: +//! - Every fallible case maps to [`DecodeError::InvalidArgument`] +//! (malformed wire input, **not** future capability), except the +//! aggregate `ORDER BY` target which maps to +//! [`DecodeError::Unsupported`] (valid request shape, server +//! capability not yet wired). rs-drive-abci maps these onto its +//! `QueryError::InvalidArgument` / `QuerySyntaxError::Unsupported` +//! respectively, preserving its historical error surface. +//! - Conversion is schema-agnostic. `DocumentFieldValue` variants +//! map 1:1 to `dpp::platform_value::Value` variants without +//! consulting the document type's schema. The schema-driven +//! coercion (`document_type.serialize_value_for_key`) runs +//! downstream as it does for the CBOR-shaped v0 path — a `text` +//! variant against an identifier field decodes via base58, a +//! `bytes_value` against the same field decodes as raw 32-byte +//! identifier, and so on. The wire layer just names the +//! primitive; the schema decides the indexed type. + +use dapi_grpc::platform::v0::get_documents_request::{ + document_field_value, + get_documents_request_v1::{select, Select as ProtoSelect}, + having_aggregate, having_clause, order_clause, DocumentFieldValue as ProtoDocumentFieldValue, + HavingAggregate as ProtoHavingAggregate, HavingClause as ProtoHavingClause, + OrderClause as ProtoOrderClause, WhereClause as ProtoWhereClause, + WhereOperator as ProtoWhereOperator, +}; +use dpp::platform_value::Value; +use drive::query::{ + HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand, + OrderClause, SelectFunction, SelectProjection, WhereClause, WhereOperator, +}; + +/// Neutral decode error for the shared proto → drive conversions. +/// +/// Deliberately not a server or client error type: rs-drive-abci +/// maps it onto its `QueryError`, and the client-side +/// `DocumentQuery` decoding maps it onto the crate +/// [`Error`](crate::error::Error), each preserving its own error +/// surface. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum DecodeError { + /// Malformed wire input — bad discriminant, missing oneof arm, + /// over-deep list nesting. No future protocol version would make + /// this input valid. + #[error("{0}")] + InvalidArgument(String), + /// Well-formed wire input naming a capability the decode target + /// cannot represent yet (e.g. `ORDER BY` on an aggregate key). + /// The wording signals future capability, not malformed request. + #[error("{0}")] + Unsupported(String), +} + +/// Map a wire-level [`ProtoWhereOperator`] discriminant onto +/// drive's [`WhereOperator`]. Unknown discriminants are wire-level +/// garbage (no future protocol value would map a malformed integer +/// to a valid behavior), so they surface as +/// [`DecodeError::InvalidArgument`]. +pub fn where_operator_from_proto(op: i32) -> Result { + let proto_op = ProtoWhereOperator::try_from(op).map_err(|_| { + DecodeError::InvalidArgument(format!( + "unknown WhereOperator discriminant: {} (valid values: 0..=10, see \ + `get_documents_request::WhereOperator`)", + op + )) + })?; + Ok(match proto_op { + ProtoWhereOperator::Equal => WhereOperator::Equal, + ProtoWhereOperator::GreaterThan => WhereOperator::GreaterThan, + ProtoWhereOperator::GreaterThanOrEquals => WhereOperator::GreaterThanOrEquals, + ProtoWhereOperator::LessThan => WhereOperator::LessThan, + ProtoWhereOperator::LessThanOrEquals => WhereOperator::LessThanOrEquals, + ProtoWhereOperator::Between => WhereOperator::Between, + ProtoWhereOperator::BetweenExcludeBounds => WhereOperator::BetweenExcludeBounds, + ProtoWhereOperator::BetweenExcludeLeft => WhereOperator::BetweenExcludeLeft, + ProtoWhereOperator::BetweenExcludeRight => WhereOperator::BetweenExcludeRight, + ProtoWhereOperator::In => WhereOperator::In, + ProtoWhereOperator::StartsWith => WhereOperator::StartsWith, + }) +} + +/// Map a wire [`ProtoDocumentFieldValue`] onto a +/// `dpp::platform_value::Value`. Schema-agnostic — variants map +/// 1:1 by primitive type and recurse for `list` up to a depth of +/// 1 (the only nesting level the query surface needs: `IN` / +/// `BETWEEN*` take a flat list of scalars). Anything deeper is +/// rejected as malformed wire input rather than recursed into, +/// so a hostile client can't blow the call stack with +/// `list(list(list(...)))` before schema validation. +/// +/// `None` (oneof unset on the wire) is rejected — a where-clause +/// operand is always concrete; empty where-clauses are expressed +/// by an empty `where_clauses` field at the request level, not by +/// sending an empty `DocumentFieldValue`. +pub fn value_from_proto(value: ProtoDocumentFieldValue) -> Result { + value_from_proto_at_depth(value, 0) +} + +/// Recursion-bounded form of [`value_from_proto`]. `depth = 0` is +/// the request-level operand; the only legal child shape is a +/// flat list (`depth = 1` for `IN` / `BETWEEN*` candidates), so a +/// `list` encountered at `depth >= 1` is wire-malformed. +fn value_from_proto_at_depth( + value: ProtoDocumentFieldValue, + depth: u8, +) -> Result { + let variant = value.variant.ok_or_else(|| { + DecodeError::InvalidArgument( + "DocumentFieldValue has no variant set; a where-clause operand must \ + be a concrete value" + .to_string(), + ) + })?; + Ok(match variant { + document_field_value::Variant::BoolValue(b) => Value::Bool(b), + document_field_value::Variant::Int64Value(i) => Value::I64(i), + document_field_value::Variant::Uint64Value(u) => Value::U64(u), + document_field_value::Variant::DoubleValue(f) => Value::Float(f), + document_field_value::Variant::Text(s) => Value::Text(s), + document_field_value::Variant::BytesValue(b) => Value::Bytes(b), + document_field_value::Variant::List(list) => { + if depth >= 1 { + return Err(DecodeError::InvalidArgument( + "nested DocumentFieldValue.list is not supported; the v1 \ + query surface accepts at most one level of nesting \ + (`IN` / `BETWEEN*` candidate lists of scalars)" + .to_string(), + )); + } + Value::Array( + list.values + .into_iter() + .map(|v| value_from_proto_at_depth(v, depth + 1)) + .collect::, _>>()?, + ) + } + // The bool payload is a placeholder — picking the + // `null_value` variant means "this operand is null" and + // the bool itself is ignored. See the proto-side comment + // on the field for the rationale. + document_field_value::Variant::NullValue(_) => Value::Null, + }) +} + +/// Map a wire [`ProtoWhereClause`] onto drive's structured +/// [`WhereClause`]. Errors surface as +/// [`DecodeError::InvalidArgument`] for both operator-discriminant +/// and value-shape failures. +pub fn where_clause_from_proto(clause: ProtoWhereClause) -> Result { + let operator = where_operator_from_proto(clause.operator)?; + let value = clause.value.ok_or_else(|| { + DecodeError::InvalidArgument(format!( + "WhereClause on field '{}' has no value set; every clause must carry a \ + concrete `DocumentFieldValue`", + clause.field + )) + })?; + let value = value_from_proto(value)?; + Ok(WhereClause { + field: clause.field, + operator, + value, + }) +} + +/// Plural form of [`where_clause_from_proto`] for the request-level +/// `repeated WhereClause` field. Returns an error on the first +/// malformed clause. +pub fn where_clauses_from_proto( + clauses: Vec, +) -> Result, DecodeError> { + clauses.into_iter().map(where_clause_from_proto).collect() +} + +/// Map a wire [`ProtoOrderClause`] onto drive's [`OrderClause`]. +/// +/// The `target` oneof currently has two variants on the wire: +/// `field` (plain column name — evaluated today) and `aggregate` +/// (aggregate function applied to a field — wire-only, rejected +/// with [`DecodeError::Unsupported`]). Unset (`None`) is rejected +/// as malformed wire input. +pub fn order_clause_from_proto(clause: ProtoOrderClause) -> Result { + let ascending = clause.ascending; + match clause.target { + Some(order_clause::Target::Field(field)) => Ok(OrderClause { field, ascending }), + Some(order_clause::Target::Aggregate(_)) => Err(DecodeError::Unsupported( + "ORDER BY on aggregate keys is not yet implemented".to_string(), + )), + None => Err(DecodeError::InvalidArgument( + "OrderClause has no target set; every clause must carry either a \ + `field` (plain column name) or an `aggregate` (aggregate-function \ + ordering target)" + .to_string(), + )), + } +} + +/// Plural form of [`order_clause_from_proto`] for the request-level +/// `repeated OrderClause` field. Returns the first error +/// encountered. +pub fn order_clauses_from_proto( + clauses: Vec, +) -> Result, DecodeError> { + clauses.into_iter().map(order_clause_from_proto).collect() +} + +/// Map a wire [`having_aggregate::Function`] discriminant onto +/// drive's [`HavingAggregateFunction`]. Unknown discriminants are +/// wire-level garbage (no future protocol value would map a +/// malformed integer to a valid behavior), so they surface as +/// [`DecodeError::InvalidArgument`]. +fn having_function_from_proto(function: i32) -> Result { + let proto = having_aggregate::Function::try_from(function).map_err(|_| { + DecodeError::InvalidArgument(format!( + "unknown HavingAggregate.Function discriminant: {} (valid values: 0..=2, see \ + `get_documents_request::having_aggregate::Function`)", + function + )) + })?; + Ok(match proto { + having_aggregate::Function::Count => HavingAggregateFunction::Count, + having_aggregate::Function::Sum => HavingAggregateFunction::Sum, + having_aggregate::Function::Avg => HavingAggregateFunction::Avg, + }) +} + +/// Map a wire [`having_clause::Operator`] discriminant onto +/// drive's [`HavingOperator`]. Same error contract as +/// [`having_function_from_proto`]. +fn having_operator_from_proto(operator: i32) -> Result { + let proto = having_clause::Operator::try_from(operator).map_err(|_| { + DecodeError::InvalidArgument(format!( + "unknown HavingClause.Operator discriminant: {} (valid values: 0..=10, see \ + `get_documents_request::having_clause::Operator`)", + operator + )) + })?; + Ok(match proto { + having_clause::Operator::Equal => HavingOperator::Equal, + having_clause::Operator::NotEqual => HavingOperator::NotEqual, + having_clause::Operator::GreaterThan => HavingOperator::GreaterThan, + having_clause::Operator::GreaterThanOrEquals => HavingOperator::GreaterThanOrEquals, + having_clause::Operator::LessThan => HavingOperator::LessThan, + having_clause::Operator::LessThanOrEquals => HavingOperator::LessThanOrEquals, + having_clause::Operator::Between => HavingOperator::Between, + having_clause::Operator::BetweenExcludeBounds => HavingOperator::BetweenExcludeBounds, + having_clause::Operator::BetweenExcludeLeft => HavingOperator::BetweenExcludeLeft, + having_clause::Operator::BetweenExcludeRight => HavingOperator::BetweenExcludeRight, + having_clause::Operator::In => HavingOperator::In, + }) +} + +/// Map a wire [`ProtoHavingAggregate`] onto drive's +/// [`HavingAggregate`]. The aggregate-function ↔ field +/// consistency check (`field` required for everything except +/// `Count`) runs inside the evaluator when HAVING execution +/// lands; the converter only enforces that the proto shape is +/// well-formed. +fn having_aggregate_from_proto( + aggregate: ProtoHavingAggregate, +) -> Result { + Ok(HavingAggregate { + function: having_function_from_proto(aggregate.function)?, + field: aggregate.field, + }) +} + +/// Map a wire [`ProtoHavingClause`] onto drive's structured +/// [`HavingClause`]. Errors surface as +/// [`DecodeError::InvalidArgument`] for any wire-level +/// malformation: unknown discriminant on the aggregate function or +/// operator; missing aggregate; missing right operand (oneof unset +/// on the wire); inner value-shape failures on the literal-value +/// branch. +/// +/// `HAVING` is a boolean per-group predicate and nothing else, so the +/// wire's `right` oneof has exactly one arm and this function has +/// exactly one thing to decode. Cross-group ranking is expressed with +/// SQL's own ordering surface — `ORDER BY DESC +/// LIMIT n [OFFSET m]` — which arrives as an `OrderClause` and never +/// reaches here. +pub fn having_clause_from_proto(clause: ProtoHavingClause) -> Result { + let aggregate = clause.aggregate.ok_or_else(|| { + DecodeError::InvalidArgument( + "HavingClause has no aggregate set; every clause must carry an \ + aggregate function + field operand" + .to_string(), + ) + })?; + let aggregate = having_aggregate_from_proto(aggregate)?; + let operator = having_operator_from_proto(clause.operator)?; + let right = clause.right.ok_or_else(|| { + DecodeError::InvalidArgument( + "HavingClause has no right operand set; every clause must carry a \ + concrete `DocumentFieldValue` (`right.value`)" + .to_string(), + ) + })?; + let right = match right { + having_clause::Right::Value(v) => HavingRightOperand::Value(value_from_proto(v)?), + }; + Ok(HavingClause { + aggregate, + operator, + right, + }) +} + +/// Plural form of [`having_clause_from_proto`] for the request- +/// level `repeated HavingClause` field. Returns an error on the +/// first malformed clause. +pub fn having_clauses_from_proto( + clauses: Vec, +) -> Result, DecodeError> { + clauses.into_iter().map(having_clause_from_proto).collect() +} + +/// Map a wire [`select::Function`] discriminant onto drive's +/// [`SelectFunction`]. Unknown discriminants are wire-level +/// garbage (no future protocol value would map a malformed +/// integer to a valid behavior), so they surface as +/// [`DecodeError::InvalidArgument`]. +fn select_function_from_proto(function: i32) -> Result { + let proto = select::Function::try_from(function).map_err(|_| { + DecodeError::InvalidArgument(format!( + "unknown Select.Function discriminant: {} (valid values: 0..=5, see \ + `get_documents_request::get_documents_request_v1::select::Function`)", + function + )) + })?; + Ok(match proto { + select::Function::Documents => SelectFunction::Documents, + select::Function::Count => SelectFunction::Count, + select::Function::Sum => SelectFunction::Sum, + select::Function::Avg => SelectFunction::Avg, + select::Function::Min => SelectFunction::Min, + select::Function::Max => SelectFunction::Max, + }) +} + +/// Map a wire [`ProtoSelect`] onto drive's [`SelectProjection`]. +/// An unset `select` field on the request decodes as the proto- +/// default `Select { function: DOCUMENTS, field: "" }`, which +/// maps to [`SelectProjection::documents()`] — keeps callers that +/// don't set the field on the v0-style document-fetch path. +/// +/// Per-function field constraints (e.g. `DOCUMENTS` must have +/// empty `field`, `SUM`/`AVG` require non-empty) are checked at +/// routing time by the server's `validate_and_route`, not here, so +/// the converter only enforces well-formed proto. +pub fn select_from_proto(select: ProtoSelect) -> Result { + Ok(SelectProjection { + function: select_function_from_proto(select.function)?, + field: select.field, + }) +} diff --git a/packages/dash-platform-queries/src/error.rs b/packages/dash-platform-queries/src/error.rs index 0d8727763ce..e296034a3c4 100644 --- a/packages/dash-platform-queries/src/error.rs +++ b/packages/dash-platform-queries/src/error.rs @@ -23,6 +23,22 @@ pub enum Error { Protocol(#[from] ProtocolError), } +impl From for Error { + fn from(value: crate::documents::proto_conversions::DecodeError) -> Self { + use crate::documents::proto_conversions::DecodeError; + match value { + // Malformed wire bytes — a decoding failure, not a + // misconfiguration. + DecodeError::InvalidArgument(msg) => Self::Protocol(ProtocolError::DecodingError(msg)), + // Well-formed wire shape the decode target can't express + // yet — same classification the server gives it. + DecodeError::Unsupported(msg) => Self::Drive(drive::error::Error::Query( + drive::error::query::QuerySyntaxError::Unsupported(msg), + )), + } + } +} + impl From for Error { fn from(value: ConsensusError) -> Self { Self::Protocol(ProtocolError::ConsensusError(Box::new(value))) diff --git a/packages/dash-platform-queries/tests/document_query_wire_roundtrip.rs b/packages/dash-platform-queries/tests/document_query_wire_roundtrip.rs new file mode 100644 index 00000000000..127881cc062 --- /dev/null +++ b/packages/dash-platform-queries/tests/document_query_wire_roundtrip.rs @@ -0,0 +1,293 @@ +//! Round-trip tests for the wire codec of [`DocumentQuery`]: +//! `DocumentQuery` → [`GetDocumentsRequest`] → +//! [`DocumentQuery::try_from_request`] must reproduce the original +//! query exactly, on both the V0 (CBOR clause) and V1 (typed proto +//! clause) wire encodings — this is what lets an embedder verify a +//! proved response against nothing but the request bytes it sent. + +use std::sync::Arc; + +use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v0::Start; +use dapi_grpc::platform::v0::get_documents_request::{GetDocumentsRequestV0, Version}; +use dapi_grpc::platform::v0::GetDocumentsRequest; +use dash_platform_queries::documents::document_query::DocumentQuery; +use dash_platform_queries::Error; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::platform_value::Value; +use dpp::prelude::DataContract; +use dpp::tests::fixtures::get_data_contract_fixture; +use dpp::version::PlatformVersion; +use drive::query::{OrderClause, SelectProjection, WhereClause, WhereOperator}; + +fn test_contract() -> Arc { + let platform_version = PlatformVersion::latest(); + Arc::new( + get_data_contract_fixture(None, 0, platform_version.protocol_version).data_contract_owned(), + ) +} + +/// A protocol version whose `drive_abci.query.document_query` +/// feature-version is `0` — encodes onto the V0 (CBOR-clause) wire. +fn v0_platform_version() -> &'static PlatformVersion { + let version = PlatformVersion::get(1).expect("protocol version 1 exists"); + assert_eq!( + version + .drive_abci + .query + .document_query + .default_current_version, + 0, + "protocol version 1 should encode the V0 documents wire" + ); + version +} + +/// The latest protocol version — encodes onto the V1 (typed proto +/// clause) wire. +fn v1_platform_version() -> &'static PlatformVersion { + let version = PlatformVersion::latest(); + assert_eq!( + version + .drive_abci + .query + .document_query + .default_current_version, + 1, + "latest protocol version should encode the V1 documents wire" + ); + version +} + +fn roundtrip(query: &DocumentQuery, platform_version: &PlatformVersion) -> DocumentQuery { + let contract = Arc::clone(&query.data_contract); + let request = query + .clone() + .try_into_request_for_version(platform_version) + .expect("query should encode onto the wire"); + DocumentQuery::try_from_request(request, contract) + .expect("wire request should decode back into a query") +} + +#[test] +fn v1_roundtrip_documents_query_full_surface() { + let contract = test_contract(); + let mut query = DocumentQuery::new(Arc::clone(&contract), "niceDocument") + .expect("document type exists") + .with_where(WhereClause { + field: "firstName".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("Alice".to_string()), + }) + .with_where(WhereClause { + field: "age".to_string(), + operator: WhereOperator::In, + value: Value::Array(vec![Value::U64(21), Value::U64(42)]), + }) + .with_where(WhereClause { + field: "balance".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::I64(-5), + }) + .with_order_by(OrderClause { + field: "firstName".to_string(), + ascending: true, + }) + .with_order_by(OrderClause { + field: "age".to_string(), + ascending: false, + }) + .with_limit(42) + .with_offset(7); + query.start = Some(Start::StartAt(vec![1u8; 32])); + + assert_eq!(roundtrip(&query, v1_platform_version()), query); +} + +#[test] +fn v1_roundtrip_start_after() { + let contract = test_contract(); + let mut query = DocumentQuery::new(contract, "niceDocument").expect("document type exists"); + query.start = Some(Start::StartAfter(vec![2u8; 32])); + + assert_eq!(roundtrip(&query, v1_platform_version()), query); +} + +#[test] +fn v1_roundtrip_grouped_count() { + let contract = test_contract(); + let query = DocumentQuery::new(contract, "niceDocument") + .expect("document type exists") + .with_select(SelectProjection::count_star()) + .with_group_by("age") + .with_where(WhereClause { + field: "age".to_string(), + operator: WhereOperator::GreaterThanOrEquals, + value: Value::U64(18), + }) + .with_limit(5); + + assert_eq!(roundtrip(&query, v1_platform_version()), query); +} + +#[test] +fn v0_roundtrip_documents_query() { + let contract = test_contract(); + let mut query = DocumentQuery::new(Arc::clone(&contract), "niceDocument") + .expect("document type exists") + .with_where(WhereClause { + field: "firstName".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("Alice".to_string()), + }) + .with_where(WhereClause { + field: "age".to_string(), + operator: WhereOperator::In, + value: Value::Array(vec![Value::U64(21), Value::U64(42)]), + }) + .with_where(WhereClause { + field: "balance".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::I64(-5), + }) + .with_order_by(OrderClause { + field: "firstName".to_string(), + ascending: true, + }) + .with_order_by(OrderClause { + field: "age".to_string(), + ascending: false, + }) + .with_limit(10); + query.start = Some(Start::StartAfter(vec![3u8; 32])); + + let request = query + .clone() + .try_into_request_for_version(v0_platform_version()) + .expect("query should encode onto the V0 wire"); + assert!( + matches!(request.version, Some(Version::V0(_))), + "protocol version 1 must produce the V0 wire shape" + ); + let decoded = DocumentQuery::try_from_request(request, contract) + .expect("V0 wire request should decode back into a query"); + assert_eq!(decoded, query); +} + +#[test] +fn v0_rejects_malformed_where_cbor() { + let contract = test_contract(); + let request = GetDocumentsRequest { + version: Some(Version::V0(GetDocumentsRequestV0 { + data_contract_id: contract.id().to_vec(), + document_type: "niceDocument".to_string(), + r#where: vec![0x9F], // truncated CBOR array + order_by: vec![], + limit: 0, + prove: true, + start: None, + })), + }; + + let error = DocumentQuery::try_from_request(request, contract) + .expect_err("truncated where CBOR must be rejected"); + assert!( + error.to_string().contains("unable to decode 'where' query"), + "unexpected error: {error}" + ); +} + +#[test] +fn v1_rejects_unknown_where_operator() { + let contract = test_contract(); + let query = DocumentQuery::new(Arc::clone(&contract), "niceDocument") + .expect("document type exists") + .with_where(WhereClause { + field: "firstName".to_string(), + operator: WhereOperator::Equal, + value: Value::Text("Alice".to_string()), + }); + let mut request = query + .try_into_request_for_version(v1_platform_version()) + .expect("query should encode onto the wire"); + let Some(Version::V1(request_v1)) = &mut request.version else { + panic!("expected the V1 wire shape"); + }; + request_v1.where_clauses[0].operator = 99; + + let error = DocumentQuery::try_from_request(request, contract) + .expect_err("unknown operator discriminant must be rejected"); + assert!( + error + .to_string() + .contains("unknown WhereOperator discriminant: 99"), + "unexpected error: {error}" + ); +} + +#[test] +fn v1_rejects_explicit_zero_limit() { + let contract = test_contract(); + let query = + DocumentQuery::new(Arc::clone(&contract), "niceDocument").expect("document type exists"); + let mut request = query + .try_into_request_for_version(v1_platform_version()) + .expect("query should encode onto the wire"); + let Some(Version::V1(request_v1)) = &mut request.version else { + panic!("expected the V1 wire shape"); + }; + request_v1.limit = Some(0); + + let error = DocumentQuery::try_from_request(request, contract) + .expect_err("explicit zero limit must be rejected, mirroring the server"); + assert!( + error.to_string().contains("limit = 0"), + "unexpected error: {error}" + ); +} + +#[test] +fn v1_rejects_multi_projection_select() { + let contract = test_contract(); + let query = + DocumentQuery::new(Arc::clone(&contract), "niceDocument").expect("document type exists"); + let mut request = query + .try_into_request_for_version(v1_platform_version()) + .expect("query should encode onto the wire"); + let Some(Version::V1(request_v1)) = &mut request.version else { + panic!("expected the V1 wire shape"); + }; + let extra_select = request_v1.selects[0].clone(); + request_v1.selects.push(extra_select); + + let error = DocumentQuery::try_from_request(request, contract) + .expect_err("multi-projection SELECT must be rejected"); + assert!( + error.to_string().contains("multi-projection SELECT"), + "unexpected error: {error}" + ); +} + +#[test] +fn rejects_contract_mismatch() { + let contract = test_contract(); + let query = + DocumentQuery::new(Arc::clone(&contract), "niceDocument").expect("document type exists"); + let mut request = query + .try_into_request_for_version(v1_platform_version()) + .expect("query should encode onto the wire"); + let Some(Version::V1(request_v1)) = &mut request.version else { + panic!("expected the V1 wire shape"); + }; + request_v1.data_contract_id = vec![9u8; 32]; + + let error = DocumentQuery::try_from_request(request, contract) + .expect_err("mismatched contract id must be rejected"); + assert!( + matches!(error, Error::Protocol(_)), + "unexpected error: {error}" + ); + assert!( + error.to_string().contains("targets data contract"), + "unexpected error: {error}" + ); +} diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 23069f0158e..668415ae2a6 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -42,6 +42,7 @@ dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ "server", "platform", ] } +dash-platform-queries = { path = "../dash-platform-queries", default-features = false } tracing-subscriber = { version = "0.3.22", default-features = false, features = [ "env-filter", "ansi", diff --git a/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs b/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs index fa5c4da140b..3f67747e5dd 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/conversions.rs @@ -1,367 +1,77 @@ //! Wire-protobuf → drive type conversions for the v1 document //! query surface. //! -//! Lives next to the v1 handler because rs-drive-abci is the only -//! crate that needs the proto-decode direction (the SDK ships the -//! inverse direction in -//! `rs-sdk/src/platform/documents/document_query.rs`). Keeping the -//! two directions in their respective crates avoids forcing -//! `dapi-grpc` into rs-drive's dependency graph just to host shared -//! conversion code. -//! -//! Conversion contract: -//! - Every fallible case maps to [`QueryError::InvalidArgument`] -//! (malformed wire input, **not** future capability). The v1 -//! handler distinguishes this from -//! [`QuerySyntaxError::Unsupported`] (valid request shape, server -//! capability not yet wired) — see `v1/mod.rs`'s +//! The decode logic itself lives in +//! `dash_platform_queries::documents::proto_conversions`, shared +//! with the client-side `DocumentQuery::try_from_request` so the +//! bytes the server decodes and the bytes the proof verifier decodes +//! cannot drift. This module only maps the shared crate's neutral +//! [`DecodeError`] onto this crate's [`QueryError`] surface: +//! - [`DecodeError::InvalidArgument`] (malformed wire input) → +//! [`QueryError::InvalidArgument`]. The v1 handler distinguishes +//! this from [`QuerySyntaxError::Unsupported`] (valid request +//! shape, server capability not yet wired) — see `v1/mod.rs`'s //! `not_yet_implemented` helper. -//! - Conversion is schema-agnostic. `DocumentFieldValue` variants -//! map 1:1 to `dpp::platform_value::Value` variants without -//! consulting the document type's schema. The schema-driven -//! coercion (`document_type.serialize_value_for_key`) runs -//! downstream as it does for the CBOR-shaped v0 path — a `text` -//! variant against an identifier field decodes via base58, a -//! `bytes_value` against the same field decodes as raw 32-byte -//! identifier, and so on. The wire layer just names the -//! primitive; the schema decides the indexed type. +//! - [`DecodeError::Unsupported`] (well-formed shape the decoder +//! deliberately refuses, e.g. `ORDER BY` on aggregate keys) → +//! [`QueryError::Query`]\([`QuerySyntaxError::Unsupported`]\). +//! +//! Both mappings preserve the exact message strings this module +//! produced when it owned the decode logic, so the server's error +//! surface is unchanged. use crate::error::query::QueryError; use dapi_grpc::platform::v0::get_documents_request::{ - document_field_value, - get_documents_request_v1::{select, Select as ProtoSelect}, - having_aggregate, having_clause, order_clause, DocumentFieldValue as ProtoDocumentFieldValue, - HavingAggregate as ProtoHavingAggregate, HavingClause as ProtoHavingClause, + get_documents_request_v1::Select as ProtoSelect, HavingClause as ProtoHavingClause, OrderClause as ProtoOrderClause, WhereClause as ProtoWhereClause, - WhereOperator as ProtoWhereOperator, }; -use dpp::platform_value::Value; -use drive::query::{ - HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand, - OrderClause, SelectFunction, SelectProjection, WhereClause, WhereOperator, -}; - -/// Map a wire-level [`ProtoWhereOperator`] discriminant onto -/// drive's [`WhereOperator`]. Unknown discriminants are wire-level -/// garbage (no future protocol value would map a malformed integer -/// to a valid behavior), so they surface as -/// [`QueryError::InvalidArgument`] — not `not_yet_implemented`. -pub(super) fn where_operator_from_proto(op: i32) -> Result { - let proto_op = ProtoWhereOperator::try_from(op).map_err(|_| { - QueryError::InvalidArgument(format!( - "unknown WhereOperator discriminant: {} (valid values: 0..=10, see \ - `get_documents_request::WhereOperator`)", - op - )) - })?; - Ok(match proto_op { - ProtoWhereOperator::Equal => WhereOperator::Equal, - ProtoWhereOperator::GreaterThan => WhereOperator::GreaterThan, - ProtoWhereOperator::GreaterThanOrEquals => WhereOperator::GreaterThanOrEquals, - ProtoWhereOperator::LessThan => WhereOperator::LessThan, - ProtoWhereOperator::LessThanOrEquals => WhereOperator::LessThanOrEquals, - ProtoWhereOperator::Between => WhereOperator::Between, - ProtoWhereOperator::BetweenExcludeBounds => WhereOperator::BetweenExcludeBounds, - ProtoWhereOperator::BetweenExcludeLeft => WhereOperator::BetweenExcludeLeft, - ProtoWhereOperator::BetweenExcludeRight => WhereOperator::BetweenExcludeRight, - ProtoWhereOperator::In => WhereOperator::In, - ProtoWhereOperator::StartsWith => WhereOperator::StartsWith, - }) -} - -/// Map a wire [`ProtoDocumentFieldValue`] onto a -/// `dpp::platform_value::Value`. Schema-agnostic — variants map -/// 1:1 by primitive type and recurse for `list` up to a depth of -/// 1 (the only nesting level the query surface needs: `IN` / -/// `BETWEEN*` take a flat list of scalars). Anything deeper is -/// rejected as malformed wire input rather than recursed into, -/// so a hostile client can't blow the call stack with -/// `list(list(list(...)))` before schema validation. -/// -/// `None` (oneof unset on the wire) is rejected — a where-clause -/// operand is always concrete; empty where-clauses are expressed -/// by an empty `where_clauses` field at the request level, not by -/// sending an empty `DocumentFieldValue`. -pub(super) fn value_from_proto(value: ProtoDocumentFieldValue) -> Result { - value_from_proto_at_depth(value, 0) -} - -/// Recursion-bounded form of [`value_from_proto`]. `depth = 0` is -/// the request-level operand; the only legal child shape is a -/// flat list (`depth = 1` for `IN` / `BETWEEN*` candidates), so a -/// `list` encountered at `depth >= 1` is wire-malformed. -fn value_from_proto_at_depth( - value: ProtoDocumentFieldValue, - depth: u8, -) -> Result { - let variant = value.variant.ok_or_else(|| { - QueryError::InvalidArgument( - "DocumentFieldValue has no variant set; a where-clause operand must \ - be a concrete value" - .to_string(), - ) - })?; - Ok(match variant { - document_field_value::Variant::BoolValue(b) => Value::Bool(b), - document_field_value::Variant::Int64Value(i) => Value::I64(i), - document_field_value::Variant::Uint64Value(u) => Value::U64(u), - document_field_value::Variant::DoubleValue(f) => Value::Float(f), - document_field_value::Variant::Text(s) => Value::Text(s), - document_field_value::Variant::BytesValue(b) => Value::Bytes(b), - document_field_value::Variant::List(list) => { - if depth >= 1 { - return Err(QueryError::InvalidArgument( - "nested DocumentFieldValue.list is not supported; the v1 \ - query surface accepts at most one level of nesting \ - (`IN` / `BETWEEN*` candidate lists of scalars)" - .to_string(), - )); - } - Value::Array( - list.values - .into_iter() - .map(|v| value_from_proto_at_depth(v, depth + 1)) - .collect::, _>>()?, - ) - } - // The bool payload is a placeholder — picking the - // `null_value` variant means "this operand is null" and - // the bool itself is ignored. See the proto-side comment - // on the field for the rationale. - document_field_value::Variant::NullValue(_) => Value::Null, - }) -} - -/// Map a wire [`ProtoWhereClause`] onto drive's structured -/// [`WhereClause`]. Errors surface as -/// [`QueryError::InvalidArgument`] for both operator-discriminant -/// and value-shape failures. -pub(super) fn where_clause_from_proto(clause: ProtoWhereClause) -> Result { - let operator = where_operator_from_proto(clause.operator)?; - let value = clause.value.ok_or_else(|| { - QueryError::InvalidArgument(format!( - "WhereClause on field '{}' has no value set; every clause must carry a \ - concrete `DocumentFieldValue`", - clause.field - )) - })?; - let value = value_from_proto(value)?; - Ok(WhereClause { - field: clause.field, - operator, - value, - }) +use dash_platform_queries::documents::proto_conversions::{self as shared, DecodeError}; +use drive::error::query::QuerySyntaxError; +use drive::query::{HavingClause, OrderClause, SelectProjection, WhereClause}; + +fn map_decode_error(error: DecodeError) -> QueryError { + match error { + DecodeError::InvalidArgument(msg) => QueryError::InvalidArgument(msg), + DecodeError::Unsupported(msg) => QueryError::Query(QuerySyntaxError::Unsupported(msg)), + } } -/// Plural form of [`where_clause_from_proto`] for the request-level -/// `repeated WhereClause` field. Returns an error on the first -/// malformed clause; the v1 handler surfaces this through +/// Decode the request-level `repeated WhereClause` field via the +/// shared decoder. Returns an error on the first malformed clause; +/// the v1 handler surfaces this through /// `QueryValidationResult::new_with_error` so the caller sees the /// rejection on the same response shape as a downstream validation /// failure. pub(super) fn where_clauses_from_proto( clauses: Vec, ) -> Result, QueryError> { - clauses.into_iter().map(where_clause_from_proto).collect() + shared::where_clauses_from_proto(clauses).map_err(map_decode_error) } -/// Map a wire [`ProtoOrderClause`] onto drive's [`OrderClause`]. -/// -/// The `target` oneof currently has two variants on the wire: -/// `field` (plain column name — evaluated today) and `aggregate` -/// (aggregate function applied to a field — wire-only, rejected -/// at routing time with `Unsupported("ORDER BY on aggregate …")`). -/// Unset (`None`) is rejected as malformed wire input. -pub(super) fn order_clause_from_proto(clause: ProtoOrderClause) -> Result { - let ascending = clause.ascending; - match clause.target { - Some(order_clause::Target::Field(field)) => Ok(OrderClause { field, ascending }), - Some(order_clause::Target::Aggregate(_)) => Err(QueryError::Query( - drive::error::query::QuerySyntaxError::Unsupported( - "ORDER BY on aggregate keys is not yet implemented".to_string(), - ), - )), - None => Err(QueryError::InvalidArgument( - "OrderClause has no target set; every clause must carry either a \ - `field` (plain column name) or an `aggregate` (aggregate-function \ - ordering target)" - .to_string(), - )), - } -} - -/// Plural form of [`order_clause_from_proto`] for the request-level -/// `repeated OrderClause` field. Returns the first error -/// encountered. +/// Decode the request-level `repeated OrderClause` field via the +/// shared decoder. Aggregate ordering targets are rejected with +/// `Unsupported("ORDER BY on aggregate keys is not yet implemented")`. pub(super) fn order_clauses_from_proto( clauses: Vec, ) -> Result, QueryError> { - clauses.into_iter().map(order_clause_from_proto).collect() -} - -// The `having_*_from_proto` family below decodes clauses the server -// then refuses: `having` evaluation is not implemented, so every -// non-empty HAVING is rejected at routing. Decoding still runs first -// (see `query_documents_v1`) so wire-malformed clauses surface as -// `InvalidArgument` rather than being masked by the capability -// rejection. The inner helpers keep a per-function -// `#[allow(dead_code)]` — rather than module-wide — so any future -// addition outside this family still trips the lint. - -/// Map a wire [`having_aggregate::Function`] discriminant onto -/// drive's [`HavingAggregateFunction`]. Unknown discriminants are -/// wire-level garbage (no future protocol value would map a -/// malformed integer to a valid behavior), so they surface as -/// [`QueryError::InvalidArgument`]. -#[allow(dead_code)] -fn having_function_from_proto(function: i32) -> Result { - let proto = having_aggregate::Function::try_from(function).map_err(|_| { - QueryError::InvalidArgument(format!( - "unknown HavingAggregate.Function discriminant: {} (valid values: 0..=2, see \ - `get_documents_request::having_aggregate::Function`)", - function - )) - })?; - Ok(match proto { - having_aggregate::Function::Count => HavingAggregateFunction::Count, - having_aggregate::Function::Sum => HavingAggregateFunction::Sum, - having_aggregate::Function::Avg => HavingAggregateFunction::Avg, - }) -} - -/// Map a wire [`having_clause::Operator`] discriminant onto -/// drive's [`HavingOperator`]. Same error contract as -/// [`having_function_from_proto`]. -#[allow(dead_code)] -fn having_operator_from_proto(operator: i32) -> Result { - let proto = having_clause::Operator::try_from(operator).map_err(|_| { - QueryError::InvalidArgument(format!( - "unknown HavingClause.Operator discriminant: {} (valid values: 0..=10, see \ - `get_documents_request::having_clause::Operator`)", - operator - )) - })?; - Ok(match proto { - having_clause::Operator::Equal => HavingOperator::Equal, - having_clause::Operator::NotEqual => HavingOperator::NotEqual, - having_clause::Operator::GreaterThan => HavingOperator::GreaterThan, - having_clause::Operator::GreaterThanOrEquals => HavingOperator::GreaterThanOrEquals, - having_clause::Operator::LessThan => HavingOperator::LessThan, - having_clause::Operator::LessThanOrEquals => HavingOperator::LessThanOrEquals, - having_clause::Operator::Between => HavingOperator::Between, - having_clause::Operator::BetweenExcludeBounds => HavingOperator::BetweenExcludeBounds, - having_clause::Operator::BetweenExcludeLeft => HavingOperator::BetweenExcludeLeft, - having_clause::Operator::BetweenExcludeRight => HavingOperator::BetweenExcludeRight, - having_clause::Operator::In => HavingOperator::In, - }) + shared::order_clauses_from_proto(clauses).map_err(map_decode_error) } -/// Map a wire [`ProtoHavingAggregate`] onto drive's -/// [`HavingAggregate`]. The aggregate-function ↔ field -/// consistency check (`field` required for everything except -/// `Count`) runs inside the evaluator when HAVING execution -/// lands; the converter only enforces that the proto shape is -/// well-formed. -#[allow(dead_code)] -fn having_aggregate_from_proto( - aggregate: ProtoHavingAggregate, -) -> Result { - Ok(HavingAggregate { - function: having_function_from_proto(aggregate.function)?, - field: aggregate.field, - }) -} - -/// Map a wire [`ProtoHavingClause`] onto drive's structured -/// [`HavingClause`]. Errors surface as -/// [`QueryError::InvalidArgument`] for any wire-level -/// malformation: unknown discriminant on the aggregate function or -/// operator; missing aggregate; missing right operand (oneof unset -/// on the wire); inner value-shape failures on the literal-value -/// branch. -/// -/// `HAVING` is a boolean per-group predicate and nothing else, so the -/// wire's `right` oneof has exactly one arm and this function has -/// exactly one thing to decode. Cross-group ranking is expressed with -/// SQL's own ordering surface — `ORDER BY DESC -/// LIMIT n [OFFSET m]` — which arrives as an `OrderClause` and never -/// reaches here. -#[allow(dead_code)] -pub(super) fn having_clause_from_proto( - clause: ProtoHavingClause, -) -> Result { - let aggregate = clause.aggregate.ok_or_else(|| { - QueryError::InvalidArgument( - "HavingClause has no aggregate set; every clause must carry an \ - aggregate function + field operand" - .to_string(), - ) - })?; - let aggregate = having_aggregate_from_proto(aggregate)?; - let operator = having_operator_from_proto(clause.operator)?; - let right = clause.right.ok_or_else(|| { - QueryError::InvalidArgument( - "HavingClause has no right operand set; every clause must carry a \ - concrete `DocumentFieldValue` (`right.value`)" - .to_string(), - ) - })?; - let right = match right { - having_clause::Right::Value(v) => HavingRightOperand::Value(value_from_proto(v)?), - }; - Ok(HavingClause { - aggregate, - operator, - right, - }) -} - -/// Plural form of [`having_clause_from_proto`] for the request- -/// level `repeated HavingClause` field. Returns an error on the -/// first malformed clause. -#[allow(dead_code)] +/// Decode the request-level `repeated HavingClause` field via the +/// shared decoder. Decoding runs before the capability rejection +/// (HAVING evaluation is not implemented) so wire-malformed clauses +/// surface as `InvalidArgument` rather than being masked by the +/// blanket "not yet implemented". pub(super) fn having_clauses_from_proto( clauses: Vec, ) -> Result, QueryError> { - clauses.into_iter().map(having_clause_from_proto).collect() -} - -/// Map a wire [`select::Function`] discriminant onto drive's -/// [`SelectFunction`]. Unknown discriminants are wire-level -/// garbage (no future protocol value would map a malformed -/// integer to a valid behavior), so they surface as -/// [`QueryError::InvalidArgument`]. -fn select_function_from_proto(function: i32) -> Result { - let proto = select::Function::try_from(function).map_err(|_| { - QueryError::InvalidArgument(format!( - "unknown Select.Function discriminant: {} (valid values: 0..=5, see \ - `get_documents_request::get_documents_request_v1::select::Function`)", - function - )) - })?; - Ok(match proto { - select::Function::Documents => SelectFunction::Documents, - select::Function::Count => SelectFunction::Count, - select::Function::Sum => SelectFunction::Sum, - select::Function::Avg => SelectFunction::Avg, - select::Function::Min => SelectFunction::Min, - select::Function::Max => SelectFunction::Max, - }) + shared::having_clauses_from_proto(clauses).map_err(map_decode_error) } -/// Map a wire [`ProtoSelect`] onto drive's [`SelectProjection`]. -/// An unset `select` field on the request decodes as the proto- -/// default `Select { function: DOCUMENTS, field: "" }`, which -/// maps to [`SelectProjection::documents()`] — keeps callers that -/// don't set the field on the v0-style document-fetch path. -/// -/// Per-function field constraints (e.g. `DOCUMENTS` must have -/// empty `field`, `SUM`/`AVG` require non-empty) are checked at -/// routing time in `validate_and_route`, not here, so the -/// converter only enforces well-formed proto. +/// Decode a wire `Select` into drive's [`SelectProjection`] via the +/// shared decoder. Per-function field constraints (e.g. `DOCUMENTS` +/// must have empty `field`, `SUM`/`AVG` require non-empty) are +/// checked at routing time in `validate_and_route`, not here. pub(super) fn select_from_proto(select: ProtoSelect) -> Result { - Ok(SelectProjection { - function: select_function_from_proto(select.function)?, - field: select.field, - }) + shared::select_from_proto(select).map_err(map_decode_error) } From afee7e043f519018d637aa505b5acbe1e31ffd6f Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 7 Aug 2026 18:23:00 -0500 Subject: [PATCH 4/8] refactor(sdk): extract pure DPNS and DashPay document builders Move the document *content* assembly of rs-sdk's networked DPNS and DashPay flows into transport-free functions in dash-platform-queries, so offline/embedder consumers and rs-sdk share one implementation: - build_dpns_preorder_and_domain_documents assembles the preorder and domain documents exactly as register_dpns_name did: both ids from the same entropy via generate_document_id_v0, saltedDomainHash = sha256d(salt || normalized_label + ".dash"), and the full domain property map (parentDomainName/normalizedParentDomainName, label, normalizedLabel, preorderSalt, records.identity, subdomainRules.allowSubdomains=false). It additionally rejects labels failing is_valid_username up front - previously only enforced by rs-sdk-ffi and platform consensus - so register_dpns_name now fails locally on an invalid label instead of after a network round-trip. - build_contact_request_document assembles the DIP-15 contactRequest id and property map from already-derived crypto material (encrypted xpub/label bytes, key indices, entropy). ECDH, encryption, the 69-byte compact-xpub check, key purpose checks, and recipient fetching stay in rs-sdk; the ciphertext size validations (96-byte xpub, 48-80-byte label, 38-102-byte autoAcceptProof) moved into the builder, with validate_auto_accept_proof also called early in create_contact_request to keep the pre-fetch fail-fast. - ensure_entropy_matches_document_id and prepare_document_for_transition moved from put_document.rs into dash_platform_queries::transition::put_document; rs-sdk re-exports and keeps calling them. Entropy/salt generation and all networking remain in rs-sdk. Builder validation errors surface through the new dash_platform_queries::Error::InvalidInput variant, which rs-sdk maps back to Error::Generic with the exact pre-move messages. New unit tests in dash-platform-queries pin a DPNS known vector (document ids and property maps for fixed label/entropy/salt), mirror the entropy-derives-id relation for contact requests, and cover the negative validation paths. --- packages/dash-platform-queries/src/dashpay.rs | 294 ++++++++++++++++++ .../src/dpns_usernames.rs | 285 ++++++++++++++++- packages/dash-platform-queries/src/error.rs | 6 + packages/dash-platform-queries/src/lib.rs | 1 + .../src/transition/mod.rs | 1 + .../src/transition/put_document.rs | 176 +++++++++++ packages/rs-sdk/src/error.rs | 3 + .../src/platform/dashpay/contact_request.rs | 110 ++----- packages/rs-sdk/src/platform/dashpay/mod.rs | 3 + .../rs-sdk/src/platform/dpns_usernames/mod.rs | 113 +------ .../src/platform/transition/put_document.rs | 166 +--------- 11 files changed, 817 insertions(+), 341 deletions(-) create mode 100644 packages/dash-platform-queries/src/dashpay.rs create mode 100644 packages/dash-platform-queries/src/transition/put_document.rs diff --git a/packages/dash-platform-queries/src/dashpay.rs b/packages/dash-platform-queries/src/dashpay.rs new file mode 100644 index 00000000000..6690237830f --- /dev/null +++ b/packages/dash-platform-queries/src/dashpay.rs @@ -0,0 +1,294 @@ +//! Transport-free DashPay contact request document assembly. +//! +//! The Sdk-bound DashPay surface (recipient fetching, ECDH, encryption, +//! broadcasting) lives in `dash-sdk`; this module is the pure DIP-15 +//! `contactRequest` document assembly it shares with embedders. All crypto +//! material arrives here as bytes — key derivation and encryption stay with +//! the caller. + +use crate::Error; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::DataContract; +use dpp::document::Document; +use dpp::platform_value::Value; +use dpp::prelude::Identifier; +use std::collections::BTreeMap; + +/// Already-derived crypto material and metadata for a DIP-15 +/// `contactRequest` document. +/// +/// Everything here is plain data: the ECDH/encryption that produced +/// `encrypted_public_key` and `encrypted_account_label`, and the randomness +/// that produced `entropy`, happen in the caller (`dash-sdk` or an +/// embedder). +#[derive(Debug, Clone)] +pub struct ContactRequestDocumentParams { + /// The sender's identity id (the document owner) + pub sender_id: Identifier, + /// The recipient's identity id (`toUserId`) + pub recipient_id: Identifier, + /// The sender's encryption key index used for ECDH + pub sender_key_index: u32, + /// The recipient's key index used for ECDH + pub recipient_key_index: u32, + /// Reference to the DashPay receiving account + pub account_reference: u32, + /// ECDH-encrypted extended public key: exactly 96 bytes + /// (16-byte IV + 80 bytes of encrypted DIP-15 compact xpub) + pub encrypted_public_key: Vec, + /// Optional encrypted account label: 48-80 bytes + /// (16-byte IV + 32-64 bytes of encrypted data) + pub encrypted_account_label: Option>, + /// Optional auto-accept proof (38-102 bytes) - not encrypted + pub auto_accept_proof: Option>, + /// The entropy that derives the document id; the same entropy must be + /// attached to the create transition, or platform consensus rejects it + /// with `InvalidDocumentTransitionIdError`. + pub entropy: [u8; 32], +} + +/// Validate the size of a DIP-15 `autoAcceptProof` (38-102 bytes). +pub fn validate_auto_accept_proof(proof: &[u8]) -> Result<(), Error> { + if proof.len() < 38 || proof.len() > 102 { + return Err(Error::InvalidInput(format!( + "autoAcceptProof must be 38-102 bytes, got {}", + proof.len() + ))); + } + Ok(()) +} + +/// Build the id and property map of a DIP-15 `contactRequest` document from +/// already-derived crypto material. +/// +/// This is the pure document-assembly half of `dash-sdk`'s +/// `create_contact_request`: the document id derives from +/// `params.entropy`, and the property map carries exactly the fields the +/// DashPay contract defines (`toUserId`, `encryptedPublicKey`, +/// `senderKeyIndex`, `recipientKeyIndex`, `accountReference`, plus the +/// optional `encryptedAccountLabel` and `autoAcceptProof`). +/// +/// Returns `(document_id, properties)`. +pub fn build_contact_request_document( + contract: &DataContract, + params: ContactRequestDocumentParams, +) -> Result<(Identifier, BTreeMap), Error> { + if let Some(ref proof) = params.auto_accept_proof { + validate_auto_accept_proof(proof)?; + } + + // Validate encrypted public key size (must be exactly 96 bytes: 16-byte IV + 80-byte encrypted data) + if params.encrypted_public_key.len() != 96 { + return Err(Error::InvalidInput(format!( + "Encrypted public key size mismatch: expected 96 bytes, got {}", + params.encrypted_public_key.len() + ))); + } + + // Validate encrypted label size (48-80 bytes: 16-byte IV + 32-64 byte encrypted data) + if let Some(ref label) = params.encrypted_account_label { + if label.len() < 48 || label.len() > 80 { + return Err(Error::InvalidInput(format!( + "Encrypted account label size out of range: expected 48-80 bytes, got {}", + label.len() + ))); + } + } + + let contact_request_document_type = + contract + .document_type_for_name("contactRequest") + .map_err(|_| { + Error::InvalidInput("DashPay contactRequest document type not found".to_string()) + })?; + + let document_id = Document::generate_document_id_v0( + &contract.id(), + ¶ms.sender_id, + contact_request_document_type.name(), + params.entropy.as_slice(), + ); + + let mut properties = BTreeMap::new(); + properties.insert( + "toUserId".to_string(), + Value::Identifier(params.recipient_id.to_buffer()), + ); + properties.insert( + "encryptedPublicKey".to_string(), + Value::Bytes(params.encrypted_public_key), + ); + properties.insert( + "senderKeyIndex".to_string(), + Value::U32(params.sender_key_index), + ); + properties.insert( + "recipientKeyIndex".to_string(), + Value::U32(params.recipient_key_index), + ); + properties.insert( + "accountReference".to_string(), + Value::U32(params.account_reference), + ); + + if let Some(label) = params.encrypted_account_label { + properties.insert("encryptedAccountLabel".to_string(), Value::Bytes(label)); + } + if let Some(proof) = params.auto_accept_proof { + properties.insert("autoAcceptProof".to_string(), Value::Bytes(proof)); + } + + Ok((document_id, properties)) +} + +#[cfg(test)] +mod tests { + use super::*; + use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; + use dpp::version::PlatformVersion; + + fn dashpay_contract() -> DataContract { + load_system_data_contract(SystemDataContract::Dashpay, PlatformVersion::latest()) + .expect("should load DashPay system contract") + } + + fn valid_params() -> ContactRequestDocumentParams { + ContactRequestDocumentParams { + sender_id: Identifier::from([2u8; 32]), + recipient_id: Identifier::from([3u8; 32]), + sender_key_index: 1, + recipient_key_index: 2, + account_reference: 7, + encrypted_public_key: vec![0xAA; 96], + encrypted_account_label: None, + auto_accept_proof: None, + entropy: [5u8; 32], + } + } + + #[test] + fn entropy_derives_built_document_id() { + // Mirror of rs-sdk's contact_request_result_entropy_derives_returned_id: + // the id the builder returns must be exactly what consensus recomputes + // from the entropy attached to the create transition. + let contract = dashpay_contract(); + let params = valid_params(); + let entropy = params.entropy; + let sender_id = params.sender_id; + + let (id, _) = + build_contact_request_document(&contract, params).expect("valid params must build"); + + assert_eq!( + id, + Document::generate_document_id_v0( + &contract.id(), + &sender_id, + "contactRequest", + entropy.as_slice() + ), + "built document id must derive from the supplied entropy" + ); + } + + #[test] + fn builds_expected_property_map() { + let contract = dashpay_contract(); + let mut params = valid_params(); + params.encrypted_account_label = Some(vec![0xBB; 48]); + params.auto_accept_proof = Some(vec![0xCC; 38]); + + let (_, properties) = + build_contact_request_document(&contract, params).expect("valid params must build"); + + assert_eq!( + properties, + BTreeMap::from([ + ( + "toUserId".to_string(), + Value::Identifier(Identifier::from([3u8; 32]).to_buffer()) + ), + ( + "encryptedPublicKey".to_string(), + Value::Bytes(vec![0xAA; 96]) + ), + ("senderKeyIndex".to_string(), Value::U32(1)), + ("recipientKeyIndex".to_string(), Value::U32(2)), + ("accountReference".to_string(), Value::U32(7)), + ( + "encryptedAccountLabel".to_string(), + Value::Bytes(vec![0xBB; 48]) + ), + ("autoAcceptProof".to_string(), Value::Bytes(vec![0xCC; 38])), + ]) + ); + } + + #[test] + fn optional_fields_are_omitted_when_absent() { + let contract = dashpay_contract(); + let (_, properties) = build_contact_request_document(&contract, valid_params()) + .expect("valid params must build"); + + assert_eq!(properties.len(), 5); + assert!(!properties.contains_key("encryptedAccountLabel")); + assert!(!properties.contains_key("autoAcceptProof")); + } + + #[test] + fn rejects_wrong_encrypted_public_key_size() { + let contract = dashpay_contract(); + for bad_len in [0, 95, 97] { + let mut params = valid_params(); + params.encrypted_public_key = vec![0xAA; bad_len]; + assert!( + matches!( + build_contact_request_document(&contract, params), + Err(Error::InvalidInput(_)) + ), + "encrypted public key of {bad_len} bytes must be rejected" + ); + } + } + + #[test] + fn rejects_out_of_range_auto_accept_proof() { + let contract = dashpay_contract(); + for bad_len in [0, 37, 103] { + let mut params = valid_params(); + params.auto_accept_proof = Some(vec![0xCC; bad_len]); + assert!( + matches!( + build_contact_request_document(&contract, params), + Err(Error::InvalidInput(_)) + ), + "auto accept proof of {bad_len} bytes must be rejected" + ); + } + for good_len in [38, 70, 102] { + let mut params = valid_params(); + params.auto_accept_proof = Some(vec![0xCC; good_len]); + assert!( + build_contact_request_document(&contract, params).is_ok(), + "auto accept proof of {good_len} bytes must be accepted" + ); + } + } + + #[test] + fn rejects_out_of_range_encrypted_account_label() { + let contract = dashpay_contract(); + for bad_len in [0, 47, 81] { + let mut params = valid_params(); + params.encrypted_account_label = Some(vec![0xBB; bad_len]); + assert!( + matches!( + build_contact_request_document(&contract, params), + Err(Error::InvalidInput(_)) + ), + "encrypted account label of {bad_len} bytes must be rejected" + ); + } + } +} diff --git a/packages/dash-platform-queries/src/dpns_usernames.rs b/packages/dash-platform-queries/src/dpns_usernames.rs index 3f452519b29..4116b7255a3 100644 --- a/packages/dash-platform-queries/src/dpns_usernames.rs +++ b/packages/dash-platform-queries/src/dpns_usernames.rs @@ -1,8 +1,151 @@ //! Transport-free DPNS username helpers. //! //! The Sdk-bound DPNS surface (registration, availability checks, name -//! resolution) lives in `dash-sdk`; these free functions are pure string -//! validation/normalization shared with embedders. +//! resolution) lives in `dash-sdk`; the free functions here are the pure +//! pieces shared with embedders: string validation/normalization and the +//! preorder/domain document assembly used to register a name. + +use crate::Error; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::DataContract; +use dpp::document::{Document, DocumentV0}; +use dpp::platform_value::Value; +use dpp::prelude::Identifier; +use std::collections::BTreeMap; + +/// Hash a buffer twice using SHA256 (double SHA256) +fn hash_double(data: Vec) -> [u8; 32] { + use dpp::dashcore::hashes::{sha256d, Hash}; + // sha256d already does double SHA256 + let hash = sha256d::Hash::hash(&data); + hash.to_byte_array() +} + +/// Build the DPNS `preorder` and `domain` documents that register +/// `label`.dash for `identity_id`, exactly as platform consensus expects +/// them. +/// +/// This is the pure document-assembly half of `dash-sdk`'s +/// `register_dpns_name`: no networking, and no randomness — the caller +/// supplies the `entropy` that derives both document ids (the same entropy +/// must later be attached to both create transitions) and the preorder +/// `salt`, whose double-SHA256 over `salt ‖ ".dash"` +/// becomes the preorder's `saltedDomainHash`. +/// +/// The `label` must satisfy [`is_valid_username`]; the raw label is stored +/// in the domain document's `label` property while its +/// [homograph-safe](convert_to_homograph_safe_chars) form is stored in +/// `normalizedLabel`. +/// +/// Returns `(preorder_document, domain_document)`. +pub fn build_dpns_preorder_and_domain_documents( + contract: &DataContract, + identity_id: Identifier, + label: &str, + entropy: [u8; 32], + salt: [u8; 32], +) -> Result<(Document, Document), Error> { + if !is_valid_username(label) { + return Err(Error::InvalidInput(format!( + "Invalid DPNS label \"{label}\": must be 3-63 characters, alphanumeric and hyphens \ + only, starting and ending with an alphanumeric character, without consecutive hyphens" + ))); + } + + let preorder_document_type = contract + .document_type_for_name("preorder") + .map_err(|_| Error::InvalidInput("DPNS preorder document type not found".to_string()))?; + + let domain_document_type = contract + .document_type_for_name("domain") + .map_err(|_| Error::InvalidInput("DPNS domain document type not found".to_string()))?; + + let preorder_id = Document::generate_document_id_v0( + &contract.id(), + &identity_id, + preorder_document_type.name(), + entropy.as_slice(), + ); + let domain_id = Document::generate_document_id_v0( + &contract.id(), + &identity_id, + domain_document_type.name(), + entropy.as_slice(), + ); + + // Create salted domain hash for preorder + let normalized_label = convert_to_homograph_safe_chars(label); + let mut salted_domain_buffer: Vec = vec![]; + salted_domain_buffer.extend(salt); + salted_domain_buffer.extend((normalized_label.clone() + ".dash").as_bytes()); + let salted_domain_hash = hash_double(salted_domain_buffer); + + let preorder_document = Document::V0(DocumentV0 { + id: preorder_id, + owner_id: identity_id, + properties: BTreeMap::from([( + "saltedDomainHash".to_string(), + Value::Bytes32(salted_domain_hash), + )]), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + + let domain_document = Document::V0(DocumentV0 { + id: domain_id, + owner_id: identity_id, + properties: BTreeMap::from([ + ( + "parentDomainName".to_string(), + Value::Text("dash".to_string()), + ), + ( + "normalizedParentDomainName".to_string(), + Value::Text("dash".to_string()), + ), + ("label".to_string(), Value::Text(label.to_string())), + ("normalizedLabel".to_string(), Value::Text(normalized_label)), + ("preorderSalt".to_string(), Value::Bytes32(salt)), + ( + "records".to_string(), + Value::Map(vec![( + Value::Text("identity".to_string()), + Value::Identifier(identity_id.to_buffer()), + )]), + ), + ( + "subdomainRules".to_string(), + Value::Map(vec![( + Value::Text("allowSubdomains".to_string()), + Value::Bool(false), + )]), + ), + ]), + revision: None, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + + Ok((preorder_document, domain_document)) +} /// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' /// with '0', '1', and '1' respectively to prevent homograph attacks @@ -100,6 +243,144 @@ pub fn is_contested_username(label: &str) -> bool { #[cfg(test)] mod tests { use super::*; + use dpp::document::DocumentV0Getters; + use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; + use dpp::version::PlatformVersion; + + fn dpns_contract() -> DataContract { + load_system_data_contract(SystemDataContract::DPNS, PlatformVersion::latest()) + .expect("should load DPNS system contract") + } + + #[test] + fn build_dpns_documents_known_vector() { + // Fixed (label, entropy, salt) must always produce the same document + // ids and property maps: platform consensus recomputes the ids from + // the entropy, and the resolved name matches on these exact fields. + let contract = dpns_contract(); + let identity_id = Identifier::from([2u8; 32]); + let entropy = [3u8; 32]; + let salt = [4u8; 32]; + + let (preorder, domain) = build_dpns_preorder_and_domain_documents( + &contract, + identity_id, + "Alice", + entropy, + salt, + ) + .expect("valid label must build"); + + // Pinned vectors: any drift in the id derivation (contract id, owner, + // type name, entropy layout) or the salted-hash preimage + // (salt ‖ "a11ce.dash", double SHA256) changes these values. + assert_eq!( + preorder + .id() + .to_string(dpp::platform_value::string_encoding::Encoding::Base58), + "8orwov4SyqCiCppTiEogdtFHSpGyPJUfR4MtHZW8mPBB" + ); + assert_eq!( + domain + .id() + .to_string(dpp::platform_value::string_encoding::Encoding::Base58), + "CeNRVgX6wseeTeoiJEspAJstmACSV57VfsRjHChDh5ec" + ); + + // Both ids derive from the SAME entropy (only the document type name + // differs), which is what lets one entropy drive both create + // transitions. + assert_eq!( + preorder.id(), + Document::generate_document_id_v0( + &contract.id(), + &identity_id, + "preorder", + entropy.as_slice() + ) + ); + assert_eq!( + domain.id(), + Document::generate_document_id_v0( + &contract.id(), + &identity_id, + "domain", + entropy.as_slice() + ) + ); + assert_eq!(preorder.owner_id(), identity_id); + assert_eq!(domain.owner_id(), identity_id); + assert_eq!(preorder.revision(), None); + assert_eq!(domain.revision(), None); + + // saltedDomainHash = sha256d(salt ‖ "a11ce.dash"), pinned as a vector. + let expected_hash: [u8; 32] = + hex::decode("5396e080af450f80f4f8ddbfc3eb0a885674c9cf6edbea815dad7305b558e253") + .expect("valid hex") + .try_into() + .expect("32 bytes"); + assert_eq!( + preorder.properties(), + &BTreeMap::from([( + "saltedDomainHash".to_string(), + Value::Bytes32(expected_hash) + )]) + ); + + assert_eq!( + domain.properties(), + &BTreeMap::from([ + ( + "parentDomainName".to_string(), + Value::Text("dash".to_string()) + ), + ( + "normalizedParentDomainName".to_string(), + Value::Text("dash".to_string()) + ), + ("label".to_string(), Value::Text("Alice".to_string())), + ( + "normalizedLabel".to_string(), + Value::Text("a11ce".to_string()) + ), + ("preorderSalt".to_string(), Value::Bytes32(salt)), + ( + "records".to_string(), + Value::Map(vec![( + Value::Text("identity".to_string()), + Value::Identifier(identity_id.to_buffer()) + )]) + ), + ( + "subdomainRules".to_string(), + Value::Map(vec![( + Value::Text("allowSubdomains".to_string()), + Value::Bool(false) + )]) + ), + ]) + ); + } + + #[test] + fn build_dpns_documents_rejects_invalid_label() { + let contract = dpns_contract(); + let identity_id = Identifier::from([2u8; 32]); + + for bad in ["", "ab", "-alice", "alice-", "alice--bob", "alice_bob"] { + let result = build_dpns_preorder_and_domain_documents( + &contract, + identity_id, + bad, + [3u8; 32], + [4u8; 32], + ); + assert!( + matches!(result, Err(Error::InvalidInput(_))), + "label {bad:?} must be rejected" + ); + } + } #[test] fn test_convert_to_homograph_safe_chars() { diff --git a/packages/dash-platform-queries/src/error.rs b/packages/dash-platform-queries/src/error.rs index e296034a3c4..f30573c3a72 100644 --- a/packages/dash-platform-queries/src/error.rs +++ b/packages/dash-platform-queries/src/error.rs @@ -15,6 +15,12 @@ pub enum Error { /// Query is not configured properly for the target platform version #[error("SDK misconfigured: {0}")] Config(String), + /// Input to a document builder failed validation (bad label, wrong + /// ciphertext length, unknown document type, ...). `dash-sdk` maps this + /// to its `Error::Generic`, preserving the messages these checks + /// produced before they moved here. + #[error("{0}")] + InvalidInput(String), /// Drive error #[error("Drive error: {0}")] Drive(#[from] drive::error::Error), diff --git a/packages/dash-platform-queries/src/lib.rs b/packages/dash-platform-queries/src/lib.rs index b644ca13c44..772bf1de440 100644 --- a/packages/dash-platform-queries/src/lib.rs +++ b/packages/dash-platform-queries/src/lib.rs @@ -12,6 +12,7 @@ #![allow(clippy::result_large_err)] pub mod block_info_from_metadata; +pub mod dashpay; pub mod documents; pub mod dpns_usernames; pub mod error; diff --git a/packages/dash-platform-queries/src/transition/mod.rs b/packages/dash-platform-queries/src/transition/mod.rs index 3a0f1376adb..097952677ce 100644 --- a/packages/dash-platform-queries/src/transition/mod.rs +++ b/packages/dash-platform-queries/src/transition/mod.rs @@ -1,2 +1,3 @@ //! Transport-free state transition helpers. +pub mod put_document; pub mod validation; diff --git a/packages/dash-platform-queries/src/transition/put_document.rs b/packages/dash-platform-queries/src/transition/put_document.rs new file mode 100644 index 00000000000..3449bb5e672 --- /dev/null +++ b/packages/dash-platform-queries/src/transition/put_document.rs @@ -0,0 +1,176 @@ +//! Transport-free helpers for document create/replace transitions. +//! +//! `dash-sdk`'s `PutDocument` broadcast path calls these; embedders that +//! assemble their own transitions share the same preparation and +//! entropy/id consistency check. + +use crate::Error; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::data_contract::document_type::DocumentType; +use dpp::document::{Document, DocumentV0Getters}; +use dpp::prelude::Identifier; + +/// Returns a copy of `document` with its properties sanitized for the given +/// document type (e.g. integer arrays coerced back into byte arrays after a +/// WASM boundary crossing), leaving the caller's document untouched. +pub fn prepare_document_for_transition( + document: &Document, + document_type: &DocumentType, +) -> Document { + let mut document = document.clone(); + document_type + .as_ref() + .sanitize_document_properties(document.properties_mut()); + document +} + +/// Ensures a caller-supplied `entropy` derives the same document id already set +/// on a create document. +/// +/// A document-create state transition carries both the document id and the +/// entropy, and Drive recomputes the id from the entropy during +/// `advanced_structure` validation, rejecting the transition with +/// `InvalidDocumentTransitionIdError` when they disagree. Because the +/// broadcast path trusts the caller's id verbatim when entropy is supplied, +/// a two-phase caller whose id and entropy have drifted would only discover +/// the mismatch after paying (a bumped identity-contract nonce). This check +/// surfaces the mismatch locally before broadcasting. +pub fn ensure_entropy_matches_document_id( + contract_id: &Identifier, + owner_id: &Identifier, + document_type_name: &str, + entropy: &[u8; 32], + document_id: Identifier, +) -> Result<(), Error> { + let expected_id = Document::generate_document_id_v0( + contract_id, + owner_id, + document_type_name, + entropy.as_slice(), + ); + if expected_id != document_id { + return Err(Error::InvalidInput(format!( + "document id {document_id} does not match the id {expected_id} derived from the \ + supplied entropy; the entropy must be the one used to generate the document id" + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use dpp::data_contract::config::DataContractConfig; + use dpp::document::{DocumentV0, INITIAL_REVISION}; + use dpp::platform_value::{platform_value, Value}; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + fn contract_id() -> Identifier { + Identifier::from([1u8; 32]) + } + + fn owner_id() -> Identifier { + Identifier::from([2u8; 32]) + } + + #[test] + fn matching_entropy_and_id_pass() { + let entropy = [7u8; 32]; + let id = Document::generate_document_id_v0( + &contract_id(), + &owner_id(), + "contactRequest", + entropy.as_slice(), + ); + + ensure_entropy_matches_document_id( + &contract_id(), + &owner_id(), + "contactRequest", + &entropy, + id, + ) + .expect("id derived from the supplied entropy must be accepted"); + } + + #[test] + fn mismatched_entropy_and_id_error_before_broadcast() { + // The id was derived from E1, but the caller passes E2 != E1 (mirroring + // the very drift consensus rejects with InvalidDocumentTransitionIdError). + let entropy_used = [1u8; 32]; + let id = Document::generate_document_id_v0( + &contract_id(), + &owner_id(), + "contactRequest", + entropy_used.as_slice(), + ); + + let different_entropy = [2u8; 32]; + let result = ensure_entropy_matches_document_id( + &contract_id(), + &owner_id(), + "contactRequest", + &different_entropy, + id, + ); + + assert!( + matches!(result, Err(Error::InvalidInput(_))), + "a document id derived from a different entropy must be rejected locally" + ); + } + + #[test] + fn should_normalize_wasm_uint8_array_property_without_mutating_caller_document() { + let platform_version = PlatformVersion::latest(); + let config = DataContractConfig::default_for_version(platform_version) + .expect("should create default data contract config"); + let document_type = DocumentType::try_from_schema( + contract_id(), + 1, + config.version(), + "preorder", + platform_value!({ + "type": "object", + "properties": { + "saltedDomainHash": { + "type": "array", + "byteArray": true, + "minItems": 32_u32, + "maxItems": 32_u32, + "position": 0 + } + }, + "required": ["saltedDomainHash"], + "additionalProperties": false, + }), + None, + &BTreeMap::new(), + &config, + false, + &mut Vec::new(), + platform_version, + ) + .expect("should create DPNS-like document type"); + let integer_array = Value::Array(vec![Value::U64(7); 32]); + let document = Document::V0(DocumentV0 { + id: Identifier::new([3; 32]), + owner_id: owner_id(), + properties: BTreeMap::from([("saltedDomainHash".to_string(), integer_array.clone())]), + revision: Some(INITIAL_REVISION), + ..Default::default() + }); + + let prepared = prepare_document_for_transition(&document, &document_type); + + assert_eq!( + prepared.properties().get("saltedDomainHash"), + Some(&Value::Bytes32([7; 32])) + ); + assert_eq!( + document.properties().get("saltedDomainHash"), + Some(&integer_array) + ); + } +} diff --git a/packages/rs-sdk/src/error.rs b/packages/rs-sdk/src/error.rs index cc8309ebcd4..89ade69e741 100644 --- a/packages/rs-sdk/src/error.rs +++ b/packages/rs-sdk/src/error.rs @@ -137,6 +137,9 @@ impl From for Error { fn from(value: dash_platform_queries::Error) -> Self { match value { dash_platform_queries::Error::Config(msg) => Self::Config(msg), + // Builder input validation moved to the query core keeps surfacing + // as Generic with the exact messages it produced inside this crate. + dash_platform_queries::Error::InvalidInput(msg) => Self::Generic(msg), dash_platform_queries::Error::Drive(e) => Self::Drive(e), dash_platform_queries::Error::Protocol(e) => Self::Protocol(e), } diff --git a/packages/rs-sdk/src/platform/dashpay/contact_request.rs b/packages/rs-sdk/src/platform/dashpay/contact_request.rs index d1f59bc9cab..0bcf9cd88a7 100644 --- a/packages/rs-sdk/src/platform/dashpay/contact_request.rs +++ b/packages/rs-sdk/src/platform/dashpay/contact_request.rs @@ -5,11 +5,13 @@ use crate::platform::transition::put_document::PutDocument; use crate::platform::Document; use crate::{Error, Sdk}; +use dash_platform_queries::dashpay::{ + build_contact_request_document, validate_auto_accept_proof, ContactRequestDocumentParams, +}; use dpp::dashcore::secp256k1::rand::rngs::StdRng; use dpp::dashcore::secp256k1::rand::{RngCore, SeedableRng}; use dpp::dashcore::secp256k1::{PublicKey, SecretKey}; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::document::DocumentV0; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; @@ -205,14 +207,11 @@ impl Sdk { H: FnOnce(u32) -> Hut, Hut: std::future::Future, Error>>, { - // Validate auto accept proof size if provided + // Validate auto accept proof size if provided. The shared builder + // validates again, but checking here first keeps the failure local — + // before the recipient fetch and ECDH work below. if let Some(ref proof) = input.auto_accept_proof { - if proof.len() < 38 || proof.len() > 102 { - return Err(Error::Generic(format!( - "autoAcceptProof must be 38-102 bytes, got {}", - proof.len() - ))); - } + validate_auto_accept_proof(proof)?; } // Fetch recipient identity if only ID was provided @@ -308,90 +307,45 @@ impl Sdk { let mut xpub_iv = [0u8; 16]; rng.fill_bytes(&mut xpub_iv); - // Encrypt the extended public key (includes IV prepended) + // Encrypt the extended public key (includes IV prepended). The shared + // builder rejects any ciphertext that isn't exactly 96 bytes + // (16-byte IV + 80-byte encrypted data). let encrypted_public_key = encrypt_extended_public_key(&shared_key, &xpub_iv, &extended_public_key); - // Validate encrypted public key size (must be exactly 96 bytes: 16-byte IV + 80-byte encrypted data) - if encrypted_public_key.len() != 96 { - return Err(Error::Generic(format!( - "Encrypted public key size mismatch: expected 96 bytes, got {}", - encrypted_public_key.len() - ))); - } - - // Encrypt the account label if provided (includes IV prepended) - let encrypted_account_label = if let Some(ref label) = input.account_label { + // Encrypt the account label if provided (includes IV prepended). The + // shared builder rejects any ciphertext outside 48-80 bytes + // (16-byte IV + 32-64 byte encrypted data). + let encrypted_account_label = input.account_label.as_ref().map(|label| { let mut label_iv = [0u8; 16]; rng.fill_bytes(&mut label_iv); - let encrypted = encrypt_account_label(&shared_key, &label_iv, label); - - // Validate encrypted label size (48-80 bytes: 16-byte IV + 32-64 byte encrypted data) - if encrypted.len() < 48 || encrypted.len() > 80 { - return Err(Error::Generic(format!( - "Encrypted account label size out of range: expected 48-80 bytes, got {}", - encrypted.len() - ))); - } - Some(encrypted) - } else { - None - }; + encrypt_account_label(&shared_key, &label_iv, label) + }); // Fetch DashPay contract let dashpay_contract = self.fetch_dashpay_contract().await?; - // Get contactRequest document type - let contact_request_document_type = dashpay_contract - .document_type_for_name("contactRequest") - .map_err(|_| { - Error::Generic("DashPay contactRequest document type not found".to_string()) - })?; - // Generate entropy for document ID let mut rng = StdRng::from_entropy(); let entropy = Bytes32::random_with_rng(&mut rng); - // Generate document ID + // Assemble the document in the shared transport-free builder, so + // networked and embedder flows produce byte-identical documents. let sender_id = input.sender_identity.id().to_owned(); - let document_id = Document::generate_document_id_v0( - &dashpay_contract.id(), - &sender_id, - contact_request_document_type.name(), - entropy.as_slice(), - ); - - // Build document properties - let mut properties = BTreeMap::new(); - let recipient_id = recipient_identity.id().to_owned(); - properties.insert( - "toUserId".to_string(), - Value::Identifier(recipient_id.to_buffer()), - ); - properties.insert( - "encryptedPublicKey".to_string(), - Value::Bytes(encrypted_public_key), - ); - properties.insert( - "senderKeyIndex".to_string(), - Value::U32(input.sender_key_index), - ); - properties.insert( - "recipientKeyIndex".to_string(), - Value::U32(input.recipient_key_index), - ); - properties.insert( - "accountReference".to_string(), - Value::U32(input.account_reference), - ); - - // Add optional fields - if let Some(label) = encrypted_account_label { - properties.insert("encryptedAccountLabel".to_string(), Value::Bytes(label)); - } - if let Some(proof) = input.auto_accept_proof { - properties.insert("autoAcceptProof".to_string(), Value::Bytes(proof)); - } + let (document_id, properties) = build_contact_request_document( + &dashpay_contract, + ContactRequestDocumentParams { + sender_id, + recipient_id: recipient_identity.id().to_owned(), + sender_key_index: input.sender_key_index, + recipient_key_index: input.recipient_key_index, + account_reference: input.account_reference, + encrypted_public_key, + encrypted_account_label, + auto_accept_proof: input.auto_accept_proof, + entropy: entropy.0, + }, + )?; // Return the essential fields for the contact request, including the // entropy that derived `document_id` so the broadcast path can reuse it. diff --git a/packages/rs-sdk/src/platform/dashpay/mod.rs b/packages/rs-sdk/src/platform/dashpay/mod.rs index ce482872996..e9f69831cb6 100644 --- a/packages/rs-sdk/src/platform/dashpay/mod.rs +++ b/packages/rs-sdk/src/platform/dashpay/mod.rs @@ -11,6 +11,9 @@ pub use contact_request::{ RecipientIdentity, SendContactRequestInput, SendContactRequestResult, }; pub use contact_request_queries::ContactRequestDocuments; +pub use dash_platform_queries::dashpay::{ + build_contact_request_document, validate_auto_accept_proof, ContactRequestDocumentParams, +}; use crate::platform::Fetch; use crate::{Error, Sdk}; diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index 6de3e2950d1..15021d9042d 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -3,7 +3,8 @@ mod queries; pub use contested_queries::ContestedDpnsUsername; pub use dash_platform_queries::dpns_usernames::{ - convert_to_homograph_safe_chars, is_contested_username, is_valid_username, + build_dpns_preorder_and_domain_documents, convert_to_homograph_safe_chars, + is_contested_username, is_valid_username, }; pub use queries::DpnsUsername; @@ -14,14 +15,12 @@ use dash_context_provider::ContextProvider; use dpp::dashcore::secp256k1::rand::rngs::StdRng; use dpp::dashcore::secp256k1::rand::{Rng, SeedableRng}; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::document::{DocumentV0, DocumentV0Getters}; +use dpp::document::DocumentV0Getters; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::signer::Signer; use dpp::identity::{Identity, IdentityPublicKey}; use dpp::platform_value::{Bytes32, Value}; use dpp::prelude::Identifier; -use std::collections::BTreeMap; use std::sync::Arc; fn extract_dpns_label(name: &str) -> &str { @@ -45,14 +44,6 @@ fn normalize_dpns_label(input: &str) -> String { convert_to_homograph_safe_chars(extract_dpns_label(input)) } -/// Hash a buffer twice using SHA256 (double SHA256) -fn hash_double(data: Vec) -> [u8; 32] { - use dpp::dashcore::hashes::{sha256d, Hash}; - // sha256d already does double SHA256 - let hash = sha256d::Hash::hash(&data); - hash.to_byte_array() -} - /// Callback type for preorder document pub type PreorderCallback = Box; @@ -164,95 +155,17 @@ impl Sdk { let entropy = Bytes32::random_with_rng(&mut rng); let salt: [u8; 32] = rng.gen(); - // Generate document IDs - let identity_id = input.identity.id().to_owned(); - let preorder_id = Document::generate_document_id_v0( - &dpns_contract.id(), - &identity_id, - preorder_document_type.name(), - entropy.as_slice(), - ); - let domain_id = Document::generate_document_id_v0( - &dpns_contract.id(), - &identity_id, - domain_document_type.name(), - entropy.as_slice(), - ); - - // Create salted domain hash for preorder + // Assemble both documents in the shared transport-free builder, so + // networked and embedder flows produce byte-identical documents. + let (preorder_document, domain_document) = build_dpns_preorder_and_domain_documents( + &dpns_contract, + input.identity.id().to_owned(), + &input.label, + entropy.0, + salt, + )?; + let normalized_label = convert_to_homograph_safe_chars(&input.label); - let mut salted_domain_buffer: Vec = vec![]; - salted_domain_buffer.extend(salt); - salted_domain_buffer.extend((normalized_label.clone() + ".dash").as_bytes()); - let salted_domain_hash = hash_double(salted_domain_buffer); - - // Create preorder document - let preorder_document = Document::V0(DocumentV0 { - id: preorder_id, - owner_id: identity_id, - properties: BTreeMap::from([( - "saltedDomainHash".to_string(), - Value::Bytes32(salted_domain_hash), - )]), - revision: None, - created_at: None, - updated_at: None, - transferred_at: None, - created_at_block_height: None, - updated_at_block_height: None, - transferred_at_block_height: None, - created_at_core_block_height: None, - updated_at_core_block_height: None, - transferred_at_core_block_height: None, - creator_id: None, - }); - - // Create domain document - let domain_document = Document::V0(DocumentV0 { - id: domain_id, - owner_id: identity_id, - properties: BTreeMap::from([ - ( - "parentDomainName".to_string(), - Value::Text("dash".to_string()), - ), - ( - "normalizedParentDomainName".to_string(), - Value::Text("dash".to_string()), - ), - ("label".to_string(), Value::Text(input.label.clone())), - ( - "normalizedLabel".to_string(), - Value::Text(normalized_label.clone()), - ), - ("preorderSalt".to_string(), Value::Bytes32(salt)), - ( - "records".to_string(), - Value::Map(vec![( - Value::Text("identity".to_string()), - Value::Identifier(identity_id.to_buffer()), - )]), - ), - ( - "subdomainRules".to_string(), - Value::Map(vec![( - Value::Text("allowSubdomains".to_string()), - Value::Bool(false), - )]), - ), - ]), - revision: None, - created_at: None, - updated_at: None, - transferred_at: None, - created_at_block_height: None, - updated_at_block_height: None, - transferred_at_block_height: None, - created_at_core_block_height: None, - updated_at_core_block_height: None, - transferred_at_core_block_height: None, - creator_id: None, - }); // Submit preorder document first let platform_preorder_document = preorder_document diff --git a/packages/rs-sdk/src/platform/transition/put_document.rs b/packages/rs-sdk/src/platform/transition/put_document.rs index fa85a30a0dc..75503ba1428 100644 --- a/packages/rs-sdk/src/platform/transition/put_document.rs +++ b/packages/rs-sdk/src/platform/transition/put_document.rs @@ -3,15 +3,18 @@ use super::validation::ensure_valid_state_transition_structure; use super::waitable::Waitable; use crate::platform::transition::put_settings::PutSettings; use crate::{Error, Sdk}; +// Transport-free helpers shared with embedders; the implementations moved to +// `dash-platform-queries`. +pub use dash_platform_queries::transition::put_document::{ + ensure_entropy_matches_document_id, prepare_document_for_transition, +}; use dpp::dashcore::secp256k1::rand::rngs::StdRng; use dpp::dashcore::secp256k1::rand::{Rng, SeedableRng}; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::data_contract::document_type::DocumentType; use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters, INITIAL_REVISION}; use dpp::identity::signer::Signer; use dpp::identity::IdentityPublicKey; -use dpp::prelude::Identifier; use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use dpp::state_transition::batch_transition::BatchTransition; use dpp::state_transition::StateTransition; @@ -162,162 +165,3 @@ impl> PutDocument for Document { Self::wait_for_response(sdk, state_transition, settings).await } } - -fn prepare_document_for_transition(document: &Document, document_type: &DocumentType) -> Document { - let mut document = document.clone(); - document_type - .as_ref() - .sanitize_document_properties(document.properties_mut()); - document -} - -/// Ensures a caller-supplied `entropy` derives the same document id already set -/// on a create document. -/// -/// A document-create state transition carries both the document id and the -/// entropy, and Drive recomputes the id from the entropy during -/// `advanced_structure` validation, rejecting the transition with -/// `InvalidDocumentTransitionIdError` when they disagree. Because -/// [`PutDocument::put_to_platform`] trusts the caller's id verbatim in the -/// `Some(entropy)` arm, a two-phase caller whose id and entropy have drifted -/// would only discover the mismatch after paying (a bumped identity-contract -/// nonce). This check surfaces the mismatch locally before broadcasting. -fn ensure_entropy_matches_document_id( - contract_id: &Identifier, - owner_id: &Identifier, - document_type_name: &str, - entropy: &[u8; 32], - document_id: Identifier, -) -> Result<(), Error> { - let expected_id = Document::generate_document_id_v0( - contract_id, - owner_id, - document_type_name, - entropy.as_slice(), - ); - if expected_id != document_id { - return Err(Error::Generic(format!( - "document id {document_id} does not match the id {expected_id} derived from the \ - supplied entropy; the entropy must be the one used to generate the document id" - ))); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use dpp::data_contract::config::DataContractConfig; - use dpp::document::DocumentV0; - use dpp::platform_value::{platform_value, Value}; - use dpp::version::PlatformVersion; - use std::collections::BTreeMap; - - fn contract_id() -> Identifier { - Identifier::from([1u8; 32]) - } - - fn owner_id() -> Identifier { - Identifier::from([2u8; 32]) - } - - #[test] - fn matching_entropy_and_id_pass() { - let entropy = [7u8; 32]; - let id = Document::generate_document_id_v0( - &contract_id(), - &owner_id(), - "contactRequest", - entropy.as_slice(), - ); - - ensure_entropy_matches_document_id( - &contract_id(), - &owner_id(), - "contactRequest", - &entropy, - id, - ) - .expect("id derived from the supplied entropy must be accepted"); - } - - #[test] - fn mismatched_entropy_and_id_error_before_broadcast() { - // The id was derived from E1, but the caller passes E2 != E1 (mirroring - // the very drift consensus rejects with InvalidDocumentTransitionIdError). - let entropy_used = [1u8; 32]; - let id = Document::generate_document_id_v0( - &contract_id(), - &owner_id(), - "contactRequest", - entropy_used.as_slice(), - ); - - let different_entropy = [2u8; 32]; - let result = ensure_entropy_matches_document_id( - &contract_id(), - &owner_id(), - "contactRequest", - &different_entropy, - id, - ); - - assert!( - matches!(result, Err(Error::Generic(_))), - "a document id derived from a different entropy must be rejected locally" - ); - } - - #[test] - fn should_normalize_wasm_uint8_array_property_without_mutating_caller_document() { - let platform_version = PlatformVersion::latest(); - let config = DataContractConfig::default_for_version(platform_version) - .expect("should create default data contract config"); - let document_type = DocumentType::try_from_schema( - contract_id(), - 1, - config.version(), - "preorder", - platform_value!({ - "type": "object", - "properties": { - "saltedDomainHash": { - "type": "array", - "byteArray": true, - "minItems": 32_u32, - "maxItems": 32_u32, - "position": 0 - } - }, - "required": ["saltedDomainHash"], - "additionalProperties": false, - }), - None, - &BTreeMap::new(), - &config, - false, - &mut Vec::new(), - platform_version, - ) - .expect("should create DPNS-like document type"); - let integer_array = Value::Array(vec![Value::U64(7); 32]); - let document = Document::V0(DocumentV0 { - id: Identifier::new([3; 32]), - owner_id: owner_id(), - properties: BTreeMap::from([("saltedDomainHash".to_string(), integer_array.clone())]), - revision: Some(INITIAL_REVISION), - ..Default::default() - }); - - let prepared = prepare_document_for_transition(&document, &document_type); - - assert_eq!( - prepared.properties().get("saltedDomainHash"), - Some(&Value::Bytes32([7; 32])) - ); - assert_eq!( - document.properties().get("saltedDomainHash"), - Some(&integer_array) - ); - } -} From 94a80b328560fb6318cdbec78c7ed078d353b619 Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 7 Aug 2026 18:27:29 -0500 Subject: [PATCH 5/8] test(sdk): add proof-vector regression corpus for drive-proof-verifier drive-proof-verifier had no integration tests at all: its verification pipeline (grovedb proof replay + tenderdash quorum signature check) was exercised only indirectly through rs-sdk's mock replay. Add a standalone corpus of 16 fixture cases under tests/vectors/, generated from a real Drive state (platform v4.0.0 fixtures, protocol version 12): identity balance/nonce/contract-nonce/keys, DPNS exact and prefix document queries, DashPay profile and contact requests, contested vote state (active/finished/absent), and quorum-signature positive and negative cases. Each case is a self-contained directory with an explicit manifest.json (request parameters, block metadata, expected outcome, pinned root hash) plus proof/signature/quorum-key blobs. The loader synthesizes the DAPI response protobuf from components and drives the real FromProof entry points with a ContextProvider serving the per-case quorum key, so every positive case runs the full pipeline including a genuine BLS check - all 16 drive proofs commit to the same root hash, which is exactly the app hash the quorum signature signs. Negative cases pin clean failures: a corrupted proof fails as a GroveDB error, a tampered signature at point decompression, a wrong quorum key or block-id hash at signature verification - never a panic. The corpus doubles as a cross-implementation anchor: the same fixtures are replayed byte-exact by Dash Core's platform GUI implementation, so drift between what Drive proves and what any client verifies fails loudly here. --- packages/rs-drive-proof-verifier/Cargo.toml | 11 + .../tests/common/mod.rs | 282 ++++++++++++++++++ .../contested-vote-state-absent/manifest.json | 37 +++ .../contested-vote-state-absent/proof.hex | 1 + .../quorum_pubkey.hex | 1 + .../contested-vote-state-absent/signature.hex | 1 + .../contested-vote-state-active/manifest.json | 51 ++++ .../contested-vote-state-active/proof.hex | 1 + .../quorum_pubkey.hex | 1 + .../contested-vote-state-active/signature.hex | 1 + .../manifest.json | 51 ++++ .../contested-vote-state-finished/proof.hex | 1 + .../quorum_pubkey.hex | 1 + .../signature.hex | 1 + .../dashpay-contacts-incoming/manifest.json | 35 +++ .../dashpay-contacts-incoming/proof.hex | 1 + .../quorum_pubkey.hex | 1 + .../dashpay-contacts-incoming/signature.hex | 1 + .../vectors/dashpay-profile/manifest.json | 33 ++ .../tests/vectors/dashpay-profile/proof.hex | 1 + .../vectors/dashpay-profile/quorum_pubkey.hex | 1 + .../vectors/dashpay-profile/signature.hex | 1 + .../vectors/dpns-domain-exact/manifest.json | 34 +++ .../tests/vectors/dpns-domain-exact/proof.hex | 1 + .../dpns-domain-exact/quorum_pubkey.hex | 1 + .../vectors/dpns-domain-exact/signature.hex | 1 + .../vectors/dpns-domain-prefix/manifest.json | 34 +++ .../vectors/dpns-domain-prefix/proof.hex | 1 + .../dpns-domain-prefix/quorum_pubkey.hex | 1 + .../vectors/dpns-domain-prefix/signature.hex | 1 + .../manifest.json | 30 ++ .../proof.hex | 1 + .../quorum_pubkey.hex | 1 + .../signature.hex | 1 + .../vectors/identity-balance/manifest.json | 31 ++ .../tests/vectors/identity-balance/proof.hex | 1 + .../identity-balance/quorum_pubkey.hex | 1 + .../vectors/identity-balance/signature.hex | 1 + .../identity-contract-nonce/manifest.json | 32 ++ .../vectors/identity-contract-nonce/proof.hex | 1 + .../identity-contract-nonce/quorum_pubkey.hex | 1 + .../identity-contract-nonce/signature.hex | 1 + .../tests/vectors/identity-keys/manifest.json | 35 +++ .../tests/vectors/identity-keys/proof.hex | 1 + .../vectors/identity-keys/quorum_pubkey.hex | 1 + .../tests/vectors/identity-keys/signature.hex | 1 + .../vectors/identity-nonce/manifest.json | 31 ++ .../tests/vectors/identity-nonce/proof.hex | 1 + .../vectors/identity-nonce/quorum_pubkey.hex | 1 + .../vectors/identity-nonce/signature.hex | 1 + .../manifest.json | 30 ++ .../quorum-sig-tampered-signature/proof.hex | 1 + .../quorum_pubkey.hex | 1 + .../signature.hex | 1 + .../vectors/quorum-sig-valid/manifest.json | 31 ++ .../tests/vectors/quorum-sig-valid/proof.hex | 1 + .../quorum-sig-valid/quorum_pubkey.hex | 1 + .../vectors/quorum-sig-valid/signature.hex | 1 + .../manifest.json | 30 ++ .../quorum-sig-wrong-block-id-hash/proof.hex | 1 + .../quorum_pubkey.hex | 1 + .../signature.hex | 1 + .../quorum-sig-wrong-quorum-key/manifest.json | 30 ++ .../quorum-sig-wrong-quorum-key/proof.hex | 1 + .../quorum_pubkey.hex | 1 + .../quorum-sig-wrong-quorum-key/signature.hex | 1 + .../tests/vectors_contested.rs | 177 +++++++++++ .../tests/vectors_documents.rs | 236 +++++++++++++++ .../tests/vectors_identity.rs | 260 ++++++++++++++++ .../tests/vectors_quorum_sig.rs | 100 +++++++ 70 files changed, 1669 insertions(+) create mode 100644 packages/rs-drive-proof-verifier/tests/common/mod.rs create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-balance/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-balance/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-balance/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-balance/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-keys/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-keys/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-keys/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-keys/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/manifest.json create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/proof.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/quorum_pubkey.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/signature.hex create mode 100644 packages/rs-drive-proof-verifier/tests/vectors_contested.rs create mode 100644 packages/rs-drive-proof-verifier/tests/vectors_documents.rs create mode 100644 packages/rs-drive-proof-verifier/tests/vectors_identity.rs create mode 100644 packages/rs-drive-proof-verifier/tests/vectors_quorum_sig.rs diff --git a/packages/rs-drive-proof-verifier/Cargo.toml b/packages/rs-drive-proof-verifier/Cargo.toml index 5ef2372aee3..d0246c9a8eb 100644 --- a/packages/rs-drive-proof-verifier/Cargo.toml +++ b/packages/rs-drive-proof-verifier/Cargo.toml @@ -47,3 +47,14 @@ derive_more = { version = "1.0", features = ["from"] } dpp = { path = "../rs-dpp", features = [ "fixtures-and-mocks", ], default-features = false } +dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ + "platform", + "client", +] } +drive = { path = "../rs-drive", default-features = false, features = [ + "verify", +] } +hex = { version = "0.4.3" } +indexmap = { version = "2.6.0" } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = { version = "1.0" } diff --git a/packages/rs-drive-proof-verifier/tests/common/mod.rs b/packages/rs-drive-proof-verifier/tests/common/mod.rs new file mode 100644 index 00000000000..ba341f6c3b5 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/common/mod.rs @@ -0,0 +1,282 @@ +//! Loader for the proof-vector regression corpus in `tests/vectors/`. +//! +//! Each case directory carries a `manifest.json` (request parameters, block +//! metadata, expected outcome) plus the raw blobs (`proof.hex`, +//! `signature.hex`, `quorum_pubkey.hex`). The corpus was generated from the +//! Dash Core fixture set (`drive_query_vectors.json` / +//! `quorum_sig_vectors.json`, platform v4.0.0 state, protocol version 12); +//! every grovedb proof commits to the same root hash, which the fixture +//! quorum signed, so positive cases run the full grovedb + tenderdash +//! verification pipeline with real key material. + +// Each integration-test binary compiles its own copy of this module and uses +// a different subset of it. +#![allow(dead_code)] + +use std::path::PathBuf; +use std::sync::Arc; + +use dapi_grpc::platform::v0::{Proof, ResponseMetadata}; +use dpp::dashcore::Network; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::TokenConfiguration; +use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +use dpp::version::PlatformVersion; +use drive_proof_verifier::{ContextProvider, ContextProviderError}; +use serde::Deserialize; + +/// The network the fixture chain id (`dash-testnet-51`) belongs to. +pub const NETWORK: Network = Network::Testnet; + +#[derive(Deserialize)] +pub struct Manifest { + pub description: String, + pub request: RequestSpec, + pub block: BlockMeta, + pub proof_meta: ProofMeta, + pub expected: Expected, + #[serde(default)] + pub expected_root_hash_hex: Option, +} + +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum RequestSpec { + IdentityBalance { + identity_id: String, + }, + IdentityNonce { + identity_id: String, + }, + IdentityContractNonce { + identity_id: String, + contract_id: String, + }, + IdentityKeys { + identity_id: String, + }, + DocumentsDpnsExact { + normalized_label: String, + limit: u16, + }, + DocumentsDpnsPrefix { + normalized_prefix: String, + limit: u16, + }, + DocumentsDashpayProfile { + owner_id: String, + }, + DocumentsDashpayContacts { + identity_id: String, + to_identity: bool, + limit: u16, + }, + ContestedVoteState { + contract_id: String, + document_type_name: String, + index_name: String, + index_values: Vec, + count: u16, + }, +} + +#[derive(Deserialize)] +pub struct BlockMeta { + pub height: u64, + pub core_chain_locked_height: u32, + pub epoch: u32, + pub time_ms: u64, + pub protocol_version: u32, + pub chain_id: String, +} + +#[derive(Deserialize)] +pub struct ProofMeta { + pub round: u32, + pub quorum_type: u32, + pub quorum_hash_hex: String, + pub block_id_hash_hex: String, +} + +#[derive(Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum Expected { + IdentityBalance { + balance: u64, + }, + IdentityNonce { + nonce: u64, + }, + IdentityContractNonce { + nonce: u64, + }, + IdentityKeys { + serialized_keys: Vec, + }, + /// The fixture grovedb state stores placeholder payloads at document + /// positions; the grovedb layer must verify and yield exactly these + /// bytes, and decoding them as DPP documents must fail cleanly. + DocumentsPlaceholder { + serialized_documents: Vec, + }, + Contested { + contenders: Vec, + abstain_votes: Option, + lock_votes: Option, + finished: bool, + winner_identity_id: Option, + }, + ContestedAbsent, + Error { + class: ErrorClass, + }, +} + +#[derive(Deserialize)] +pub struct ExpectedContender { + pub identity_id: String, + pub votes: Option, +} + +#[derive(Deserialize, Clone, Copy, PartialEq, Eq, Debug)] +#[serde(rename_all = "snake_case")] +pub enum ErrorClass { + InvalidSignature, + ProofInvalid, +} + +pub struct Case { + pub name: String, + pub manifest: Manifest, + pub grovedb_proof: Vec, + pub signature: Vec, + pub quorum_pubkey: [u8; 48], +} + +pub fn hex_vec(s: &str) -> Vec { + hex::decode(s.trim()).expect("corpus hex blob must decode") +} + +pub fn hex32(s: &str) -> [u8; 32] { + hex_vec(s).try_into().expect("expected 32 bytes of hex") +} + +pub fn identifier(s: &str) -> Identifier { + Identifier::from_bytes(&hex_vec(s)).expect("corpus identifier must be 32 bytes") +} + +pub fn load_case(name: &str) -> Case { + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/vectors") + .join(name); + let read = |file: &str| { + std::fs::read_to_string(dir.join(file)) + .unwrap_or_else(|e| panic!("read corpus file {name}/{file}: {e}")) + }; + let manifest: Manifest = + serde_json::from_str(&read("manifest.json")).expect("parse corpus manifest"); + Case { + name: name.to_string(), + grovedb_proof: hex_vec(&read("proof.hex")), + signature: hex_vec(&read("signature.hex")), + quorum_pubkey: hex_vec(&read("quorum_pubkey.hex")) + .try_into() + .expect("quorum public key must be 48 bytes"), + manifest, + } +} + +impl Case { + /// The tenderdash proof envelope for the DAPI response. + pub fn grpc_proof(&self) -> Proof { + Proof { + grovedb_proof: self.grovedb_proof.clone(), + quorum_hash: hex_vec(&self.manifest.proof_meta.quorum_hash_hex), + signature: self.signature.clone(), + round: self.manifest.proof_meta.round, + block_id_hash: hex_vec(&self.manifest.proof_meta.block_id_hash_hex), + quorum_type: self.manifest.proof_meta.quorum_type, + } + } + + /// The response metadata (block context the quorum signed over). + pub fn metadata(&self) -> ResponseMetadata { + ResponseMetadata { + height: self.manifest.block.height, + core_chain_locked_height: self.manifest.block.core_chain_locked_height, + epoch: self.manifest.block.epoch, + time_ms: self.manifest.block.time_ms, + protocol_version: self.manifest.block.protocol_version, + chain_id: self.manifest.block.chain_id.clone(), + } + } + + /// The platform version the vectors were generated with. The proofs are + /// self-contained, so they must keep verifying under this version even + /// as the crate's latest version moves on. + pub fn platform_version(&self) -> &'static PlatformVersion { + PlatformVersion::get(self.manifest.block.protocol_version) + .expect("corpus protocol version must be known") + } + + /// A [ContextProvider] serving this case's quorum public key and the + /// system data contracts referenced by the fixture proofs. + pub fn provider(&self) -> VectorContextProvider { + VectorContextProvider { + quorum_type: self.manifest.proof_meta.quorum_type, + quorum_hash: hex32(&self.manifest.proof_meta.quorum_hash_hex), + quorum_pubkey: self.quorum_pubkey, + } + } +} + +/// [ContextProvider] backed by the per-case corpus quorum key material. +pub struct VectorContextProvider { + quorum_type: u32, + quorum_hash: [u8; 32], + quorum_pubkey: [u8; 48], +} + +impl ContextProvider for VectorContextProvider { + fn get_data_contract( + &self, + id: &Identifier, + platform_version: &PlatformVersion, + ) -> Result>, ContextProviderError> { + for system_contract in [SystemDataContract::DPNS, SystemDataContract::Dashpay] { + let contract = load_system_data_contract(system_contract, platform_version) + .map_err(|e| ContextProviderError::DataContractFailure(e.to_string()))?; + if contract.id() == *id { + return Ok(Some(Arc::new(contract))); + } + } + Ok(None) + } + + fn get_token_configuration( + &self, + _token_id: &Identifier, + ) -> Result, ContextProviderError> { + Ok(None) + } + + fn get_quorum_public_key( + &self, + quorum_type: u32, + quorum_hash: [u8; 32], + _core_chain_locked_height: u32, + ) -> Result<[u8; 48], ContextProviderError> { + if quorum_type != self.quorum_type || quorum_hash != self.quorum_hash { + return Err(ContextProviderError::InvalidQuorum(format!( + "unexpected quorum requested: type {quorum_type}, hash {}", + hex::encode(quorum_hash) + ))); + } + Ok(self.quorum_pubkey) + } + + fn get_platform_activation_height(&self) -> Result { + Ok(1) + } +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/manifest.json new file mode 100644 index 00000000000..b34245cddd1 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/manifest.json @@ -0,0 +1,37 @@ +{ + "description": "Absent DPNS name contest ('carol'): proof of non-existence, FromProof returns Ok(None).", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "contested_vote_state", + "contract_id": "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "document_type_name": "domain", + "index_name": "parentNameAndLabel", + "index_values": [ + "dash", + "carol" + ], + "count": 100 + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "contested_absent" + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/proof.hex new file mode 100644 index 00000000000..48db66e0a38 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/proof.hex @@ -0,0 +1 @@ +0100b201039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a02dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1004017000050201016300ade908a28152707d22ed66d575d6f3ce38c27447c295a245272dc100dd0c4a621111010170002a040163000502010170001e5c60ed1b4311b6baf76a42f41550bcbba0f9f87d71b48e76d800c95f22cccc01016300490401700024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155005401be78056200f8a616eddb15fbd41fbc4204330111709350c20535528492b5010170004e0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000a020106646f6d61696e004412968f90b3c569b0e5b9760130f021137946563c0d05c6cb64233e4a35a57f0120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f0406646f6d61696e00050201010100bba90275bf4b39ce726f641378ede6c4fcd17162611f992a34fea2ad303f4eeb0106646f6d61696e002d0401010008020104646173680060b4e46b6e0fca7c9c1634f80d1d3a18a9188165d64e81782800392ba3f08c5901010100310404646173680009020105616c6963650078f7ad26be337eb5f110a07a8d21736265aef4cb370c25b3aaa3f8d354ee191101046461736800470246f1e3eb733288ba8ee508b3b972872149b7ee87c6b0c05bbd4bb5982d93e7140503626f62818e82b602d0fff0c045f136090f3816039b90af1ac6f33f3ec84deba88908d61100 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-absent/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/manifest.json new file mode 100644 index 00000000000..01ea67f5d3b --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/manifest.json @@ -0,0 +1,51 @@ +{ + "description": "Active DPNS name contest ('alice'): two contenders plus abstain/lock tallies, no winner yet.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "contested_vote_state", + "contract_id": "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "document_type_name": "domain", + "index_name": "parentNameAndLabel", + "index_values": [ + "dash", + "alice" + ], + "count": 100 + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "contested", + "contenders": [ + { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777", + "votes": 5 + }, + { + "identity_id": "8888888888888888888888888888888888888888888888888888888888888888", + "votes": 2 + } + ], + "abstain_votes": 2, + "lock_votes": 3, + "finished": false, + "winner_identity_id": null + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/proof.hex new file mode 100644 index 00000000000..e88ff11ea0a --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/proof.hex @@ -0,0 +1 @@ +0100b201039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a02dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1004017000050201016300ade908a28152707d22ed66d575d6f3ce38c27447c295a245272dc100dd0c4a621111010170002a040163000502010170001e5c60ed1b4311b6baf76a42f41550bcbba0f9f87d71b48e76d800c95f22cccc01016300490401700024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155005401be78056200f8a616eddb15fbd41fbc4204330111709350c20535528492b5010170004e0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000a020106646f6d61696e004412968f90b3c569b0e5b9760130f021137946563c0d05c6cb64233e4a35a57f0120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f0406646f6d61696e00050201010100bba90275bf4b39ce726f641378ede6c4fcd17162611f992a34fea2ad303f4eeb0106646f6d61696e002d0401010008020104646173680060b4e46b6e0fca7c9c1634f80d1d3a18a9188165d64e81782800392ba3f08c5901010100310404646173680009020105616c6963650078f7ad26be337eb5f110a07a8d21736265aef4cb370c25b3aaa3f8d354ee1911010464617368006f0405616c6963650024020120777777777777777777777777777777777777777777777777777777777777777700ba2fead8aaa37d1be007db47f2e9779b773387336b1fcb738551a2a8ca295ef201a5ddbf84df65edf5876f0141adfa01d1988f011c18020496c3e93599f57dd5d5110105616c69636500fb016703200000000000000000000000000000000000000000000000000000000000000000001b0018000003fd0000018bcfe56800fc0001e078fc001e84760400000420000000000000000000000000000000000000000000000000000000000000000100050201010100ac8e80c26488285c9ef7ce4167825d9e9bef26e737bb7f7f1340d69af3a8eb5910042000000000000000000000000000000000000000000000000000000000000000020005020101010012ec6171d927bc5d9f132f407acd3ab0e75049f021389f6430dd8d4ba87ce26d1104207777777777777777777777777777777777777777777777777777777777777777000502010100008bbee91750be1729b067ae752c218d46345cee65af3996e02cc4aa079d9b26fd10042088888888888888888888888888888888888888888888888888888888888888880005020101000092fab297d83548fabdbefbad2d5e3c26a4005332274f31ff9cd51c9436d401971104200000000000000000000000000000000000000000000000000000000000000001006b1c01010025040120c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c10400c1d2a7d2d0355e6a6e662719e9e8475444c47022e4f1eddddfb5b45a8686acea006a29c24ef5eb604e398952790112a98973d5485490ab94ec88844dd50ac7525100200000000000000000000000000000000000000000000000000000000000000002006b1c01010025040120c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2060035878fa5d0c12a20364a12f71fc2124b8ddc22d9d81368bd79b20400da6eee8800efe92f879d4c44f631bb52241d7606b85bbbab020cb55fd4395a887bd97101db00207777777777777777777777777777777777777777777777777777777777777777008d025ef5cd4abe01c5cc27c851dca48ca596628dfc45b454ee847bcfc824f17312b61c01010025040120a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a10a00f5b15a04f171a7a3fe3cf9dc367b0bfe597fea84cd7239384715570ce4551fe800a706a5fc391f817996f9e274f0b37ff0b8a61b579bf079a8da92846c9252d5511100208888888888888888888888888888888888888888888888888888888888888888008d02e6ee602462484dae8b0c314ddc157efc77b293ea2350cb615962612be3d768bf1c01010025040120b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b104002bb7188bd669db6e98f44ee502c9742d85da29c4e7824b4fbd27f9587ad2758a009b9c598e017cb597ea60903480c868289dd951ce282f67f97459c79366a9d0d31100 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-active/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/manifest.json new file mode 100644 index 00000000000..5cff9b4d2e4 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/manifest.json @@ -0,0 +1,51 @@ +{ + "description": "Finished DPNS name contest ('bob'): winner recorded with finalization block info.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "contested_vote_state", + "contract_id": "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155", + "document_type_name": "domain", + "index_name": "parentNameAndLabel", + "index_values": [ + "dash", + "bob" + ], + "count": 100 + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "contested", + "contenders": [ + { + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777", + "votes": 5 + }, + { + "identity_id": "8888888888888888888888888888888888888888888888888888888888888888", + "votes": 2 + } + ], + "abstain_votes": 2, + "lock_votes": 1, + "finished": true, + "winner_identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/proof.hex new file mode 100644 index 00000000000..300633f010e --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/proof.hex @@ -0,0 +1 @@ +0100b201039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a02dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1004017000050201016300ade908a28152707d22ed66d575d6f3ce38c27447c295a245272dc100dd0c4a621111010170002a040163000502010170001e5c60ed1b4311b6baf76a42f41550bcbba0f9f87d71b48e76d800c95f22cccc01016300490401700024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155005401be78056200f8a616eddb15fbd41fbc4204330111709350c20535528492b5010170004e0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000a020106646f6d61696e004412968f90b3c569b0e5b9760130f021137946563c0d05c6cb64233e4a35a57f0120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f0406646f6d61696e00050201010100bba90275bf4b39ce726f641378ede6c4fcd17162611f992a34fea2ad303f4eeb0106646f6d61696e002d0401010008020104646173680060b4e46b6e0fca7c9c1634f80d1d3a18a9188165d64e81782800392ba3f08c5901010100310404646173680009020105616c6963650078f7ad26be337eb5f110a07a8d21736265aef4cb370c25b3aaa3f8d354ee1911010464617368006d0246f1e3eb733288ba8ee508b3b972872149b7ee87c6b0c05bbd4bb5982d93e7140403626f620024020120000000000000000000000000000000000000000000000000000000000000000000818e82b602d0fff0c045f136090f3816039b90af1ac6f33f3ec84deba88908d6110103626f6200fb018403200000000000000000000000000000000000000000000000000000000000000000016000fb015b00010400777777777777777777777777777777777777777777777777777777777777777702a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a104a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a20100888888888888888888888888888888888888888888888888888888888888888801b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1020101c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1020201c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c201fd0000018bcfe56800fc0001e078fc001e847604fd0000018bcfed0920fc0001e208fc001e847f04017777777777777777777777777777777777777777777777777777777777777777017777777777777777777777777777777777777777777777777777777777777777000000 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/contested-vote-state-finished/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/manifest.json new file mode 100644 index 00000000000..024981b8176 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/manifest.json @@ -0,0 +1,35 @@ +{ + "description": "Dashpay incoming contact requests query proof (toUserId); grovedb layer verifies, placeholder document payload must fail decoding cleanly.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "documents_dashpay_contacts", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777", + "to_identity": true, + "limit": 100 + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "documents_placeholder", + "serialized_documents": [ + "70726f7665642d636f6e746163742d646f63756d656e74" + ] + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/proof.hex new file mode 100644 index 00000000000..e24661821a4 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/proof.hex @@ -0,0 +1 @@ +0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b0420a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc000502010101005b6467dac53627bd556c06b7de5e97bd2d50688788cc29c251144d866715c4bf02b8d9d2e8f03bb4b317f5fc24e2741a89ea9f5bb6cb43a44ebbf99bf056fc6105100120a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc0030040101000b02010770726f66696c650084e1535a92501db14221531aacd7009467348130c1807153e9b3af041b75f6140101010060040e636f6e7461637452657175657374000c020108246f776e6572496400c59531534d16dc4ec238c5c8a30fb6414b28cdbb977c82c77bfacf55ab8d3dcd028434a8d562d84df39d1971af05bd8fdd748581b35202e723eda1d4f737d7e93710010e636f6e74616374526571756573740094014b4c08659a34e5bccbbc817edffcec379dc3300ad974a50d539307d25bd505c202241ddcbc852479eec397f2fd32bcaf69ebadd79675ddbf92504d7e42436c1774100408746f5573657249640024020120777777777777777777777777777777777777777777777777777777777777777700902c3fed2d96edf795511e59795d4fa6fd6d36220a32a9aa13450e9562c5018c110108746f557365724964005204207777777777777777777777777777777777777777777777777777777777777777000e02010a2463726561746564417400c75709cd8917bb5116f06f15c1b88c22917b42e472c4f4ea2fecfe993291976b01207777777777777777777777777777777777777777777777777777777777777777003a040a24637265617465644174000c0201080000018bcfe56800005d8532c133405f01f9a65173c587d6a363a865b0c1e6fdc6a6b8b52d13f68575010a24637265617465644174003104080000018bcfe56800000502010100000dedbe3b16265a48243462cef6c2fbcdfcad43aed206f5310dbc84ad1a39d98901080000018bcfe5680000490401000024020120d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d300a2ab6c494b389894e8a4efe5a4e0673566880fd6a1d376962621108ced3d6120010100005e0620d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3001a001770726f7665642d636f6e746163742d646f63756d656e740007547e597229008c8eed5a86719458be289d9d088f8943866923c9c3cd230da400 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-contacts-incoming/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/manifest.json new file mode 100644 index 00000000000..45fbfbbb511 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/manifest.json @@ -0,0 +1,33 @@ +{ + "description": "Dashpay profile-by-owner query proof; grovedb layer verifies, placeholder document payload must fail decoding cleanly.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "documents_dashpay_profile", + "owner_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "documents_placeholder", + "serialized_documents": [ + "70726f7665642d70726f66696c652d646f63756d656e74" + ] + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/proof.hex new file mode 100644 index 00000000000..5a262525560 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/proof.hex @@ -0,0 +1 @@ +0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b0420a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc000502010101005b6467dac53627bd556c06b7de5e97bd2d50688788cc29c251144d866715c4bf02b8d9d2e8f03bb4b317f5fc24e2741a89ea9f5bb6cb43a44ebbf99bf056fc6105100120a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc0030040101000b02010770726f66696c650084e1535a92501db14221531aacd7009467348130c1807153e9b3af041b75f61401010100520181acb67debb4c2c3aeaf6481bba22c23e59118b587c1adffcbe3c42e4754a6d5040770726f66696c650005020101000064064ce41f142aa0218b83251b9511d2d4affbb8012187cb1348f2062e61008710010770726f66696c65007202b077d63006fc15cfb2db05ddbee6d6dd7cf56291575215cee5216f7f1552a1250408246f776e657249640024020120777777777777777777777777777777777777777777777777777777777777777700282c1c78c773ac1bbcd84b55ad6bc8aa39653642a01744998b92045ce37949c7110108246f776e657249640049042077777777777777777777777777777777777777777777777777777777777777770005020101000071ea7f7cb98ab3eb3eae2895ce3a94c2714e806bb98c0e8c1177dde712022e8a01207777777777777777777777777777777777777777777777777777777777777777003f060100001a001770726f7665642d70726f66696c652d646f63756d656e7400da1581c51512f64b6e931eff13bd4fb05003f6befbbbc3b2962d208a582958df00 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dashpay-profile/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/manifest.json new file mode 100644 index 00000000000..6dcd1bb8c90 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/manifest.json @@ -0,0 +1,34 @@ +{ + "description": "DPNS domain-by-exact-label query proof (label 'alice'); grovedb layer verifies, placeholder document payload must fail decoding cleanly.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "documents_dpns_exact", + "normalized_label": "alice", + "limit": 1 + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "documents_placeholder", + "serialized_documents": [ + "70726f7665642d64706e732d646f63756d656e74" + ] + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/proof.hex new file mode 100644 index 00000000000..c857a730a41 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/proof.hex @@ -0,0 +1 @@ +0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b013af9b5c483d29ba0cb144b3c8d0b00acb6894c2ba54526ee525ecba262f25bbe0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000502010101005dfffca61718377c8e28755c8c4c41cd3281e43700cc17ef2fb2ce858f8d9e08100120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f040101000a020106646f6d61696e00aef97d6723e7dfdd1a37d4051a65d1851a7183ee391f51b154add1042479806001010100480406646f6d61696e001e02011a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6500c0db47fafbb7b914aca5112d697ebb013ac3f9de326d8135d68029a191e89cb20106646f6d61696e008a01bb6f7c477bd80200888c4db39eee8829d38f1ab953cc25c13f82c05fc5b25f32041a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6500080201046461736800aeb4ddee9352a25186598ca471577e45325bd9cf1b02c80bb31d03ab58ba336810014fd3d5c7b045c62f2ac81c9f51f067a4450901d41999b0f82a3f15b103df4a6a11011a6e6f726d616c697a6564506172656e74446f6d61696e4e616d65003b040464617368001302010f6e6f726d616c697a65644c6162656c000127fd05259b354ce1f58158158364d18a5a6c9f07227e9170a8ebda0e5e7fa3010464617368003c040f6e6f726d616c697a65644c6162656c0009020105616c696365001e7beb5cde43e295777634dc595f1536f62e2885343dca251fd9c3551285b8f6010f6e6f726d616c697a65644c6162656c002e0405616c69636500050201010000ecb69a79bd697e495ef181171cb50b0443bdc72b78e3c0446e75ae9f68ffeb2c0105616c696365003c0601000017001470726f7665642d64706e732d646f63756d656e7400a711904e44b1b6aec95a008065047732ea534162d3e3e0934617a6faa218739500 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-exact/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/manifest.json new file mode 100644 index 00000000000..f84480e2ed7 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/manifest.json @@ -0,0 +1,34 @@ +{ + "description": "DPNS domain-by-prefix query proof (prefix 'ali', limit 25); grovedb layer verifies, placeholder document payload must fail decoding cleanly.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "documents_dpns_prefix", + "normalized_prefix": "ali", + "limit": 25 + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "documents_placeholder", + "serialized_documents": [ + "70726f7665642d64706e732d646f63756d656e74" + ] + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/proof.hex new file mode 100644 index 00000000000..c857a730a41 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/proof.hex @@ -0,0 +1 @@ +0100d101039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f237100401400024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500fbda163cb3ac4d5ca76ccab587620f53bdcbe46a96dbcb1e0ed411ace9395b2202dae5ae1465037089786f368eee27aa0437d77fbaf34d14dd299aae3d6a54168b1001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010140006b013af9b5c483d29ba0cb144b3c8d0b00acb6894c2ba54526ee525ecba262f25bbe0420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155000502010101005dfffca61718377c8e28755c8c4c41cd3281e43700cc17ef2fb2ce858f8d9e08100120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002f040101000a020106646f6d61696e00aef97d6723e7dfdd1a37d4051a65d1851a7183ee391f51b154add1042479806001010100480406646f6d61696e001e02011a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6500c0db47fafbb7b914aca5112d697ebb013ac3f9de326d8135d68029a191e89cb20106646f6d61696e008a01bb6f7c477bd80200888c4db39eee8829d38f1ab953cc25c13f82c05fc5b25f32041a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6500080201046461736800aeb4ddee9352a25186598ca471577e45325bd9cf1b02c80bb31d03ab58ba336810014fd3d5c7b045c62f2ac81c9f51f067a4450901d41999b0f82a3f15b103df4a6a11011a6e6f726d616c697a6564506172656e74446f6d61696e4e616d65003b040464617368001302010f6e6f726d616c697a65644c6162656c000127fd05259b354ce1f58158158364d18a5a6c9f07227e9170a8ebda0e5e7fa3010464617368003c040f6e6f726d616c697a65644c6162656c0009020105616c696365001e7beb5cde43e295777634dc595f1536f62e2885343dca251fd9c3551285b8f6010f6e6f726d616c697a65644c6162656c002e0405616c69636500050201010000ecb69a79bd697e495ef181171cb50b0443bdc72b78e3c0446e75ae9f68ffeb2c0105616c696365003c0601000017001470726f7665642d64706e732d646f63756d656e7400a711904e44b1b6aec95a008065047732ea534162d3e3e0934617a6faa218739500 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/dpns-domain-prefix/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/manifest.json new file mode 100644 index 00000000000..0cbd1fb8d0f --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/manifest.json @@ -0,0 +1,30 @@ +{ + "description": "Identity balance proof with one bit flipped mid-stream; grovedb verification must fail with a clean error (no panic), and any surviving root hash cannot match the signed app hash.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "identity_balance", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "error", + "class": "proof_invalid" + } +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/proof.hex new file mode 100644 index 00000000000..34afee48bc6 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/proof.hex @@ -0,0 +1 @@ +0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777767777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance-corrupted-proof/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/manifest.json new file mode 100644 index 00000000000..3daf42e7b0e --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/manifest.json @@ -0,0 +1,31 @@ +{ + "description": "Balance proof for the fixture identity; positive end-to-end verification.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "identity_balance", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "identity_balance", + "balance": 5000000000 + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/proof.hex new file mode 100644 index 00000000000..255f940a0c5 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/proof.hex @@ -0,0 +1 @@ +0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-balance/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/manifest.json new file mode 100644 index 00000000000..ee623fa84ed --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/manifest.json @@ -0,0 +1,32 @@ +{ + "description": "Identity contract nonce proof against the DPNS contract; positive.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "identity_contract_nonce", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777", + "contract_id": "e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "identity_contract_nonce", + "nonce": 11 + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/proof.hex new file mode 100644 index 00000000000..8a8943d0c2e --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/proof.hex @@ -0,0 +1 @@ +01008d01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a0401200024020120777777777777777777777777777777777777777777777777777777777777777700a2f8eb7d55888b059806841f7a2e92d211d65049ed09fe2bab4d1cd1f72aa0e41001eb804ecd6886810d1d7c79a0f1b6b35791037224de02c64c11ce42338455528d1101012000490420777777777777777777777777777777777777777777777777777777777777777700050201018000aee59dc5acac5fe8d4b452c1fd0f22601b6c1d876e28d0e3512a2ff39d18bcee0120777777777777777777777777777777777777777777777777777777777777777700af0401200024020120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155008b708962e5e42245c8220f3a06dbcc9ba3a7e655df4fed3dba6e8cff9426f43d024e6a60642818a48d5f2e7c3647a08b69f48d05941c95aaa24536b810b3c49b681002cbeddd5e01bcb90c5002f6a710bc7957af406d2a6a9df600dbfa36951abf3534100185d286a688ac17d96cdb2880a8f7174e151dfb90742370effeeaae391ee359df1101012000490420e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500050201010000411ce8bb44ce6a4f4169880f3111dbad167ccd51ab08199b006247e83b1fd1510120e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c531550010030100000b0008000000000000000b0000 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-contract-nonce/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/manifest.json new file mode 100644 index 00000000000..f3d066a6de2 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/manifest.json @@ -0,0 +1,35 @@ +{ + "description": "All-keys proof for the fixture identity; keys compared byte-for-byte.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "identity_keys", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "identity_keys", + "serialized_keys": [ + "0000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa00", + "000100020000002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f2700", + "0002010300000021035ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c3001fd0000018bcfe5687b" + ] + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/proof.hex new file mode 100644 index 00000000000..8a623ed819b --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/proof.hex @@ -0,0 +1 @@ +01008d01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a0401200024020120777777777777777777777777777777777777777777777777777777777777777700a2f8eb7d55888b059806841f7a2e92d211d65049ed09fe2bab4d1cd1f72aa0e41001eb804ecd6886810d1d7c79a0f1b6b35791037224de02c64c11ce42338455528d1101012000490420777777777777777777777777777777777777777777777777777777777777777700050201018000aee59dc5acac5fe8d4b452c1fd0f22601b6c1d876e28d0e3512a2ff39d18bcee01207777777777777777777777777777777777777777777777777777777777777777006e017b204423f6e41b597890ac921d8f72453128cad7e268750f6fc87d648bcc72aa04018000050201010100d8e4a96433656c05d1cfef00c047f123db4140e64fd3e509993825149a64a5d0100185d286a688ac17d96cdb2880a8f7174e151dfb90742370effeeaae391ee359df1101018000a1030100002d002a0000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa0000030101002d002a000100020000002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27000010030102003600330002010300000021035ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c3001fd0000018bcfe5687b001100 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-keys/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/manifest.json new file mode 100644 index 00000000000..4930c4ee341 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/manifest.json @@ -0,0 +1,31 @@ +{ + "description": "Identity nonce proof; positive end-to-end verification.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "identity_nonce", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "identity_nonce", + "nonce": 7 + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/proof.hex new file mode 100644 index 00000000000..78436a05fef --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/proof.hex @@ -0,0 +1 @@ +01008d01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a0401200024020120777777777777777777777777777777777777777777777777777777777777777700a2f8eb7d55888b059806841f7a2e92d211d65049ed09fe2bab4d1cd1f72aa0e41001eb804ecd6886810d1d7c79a0f1b6b35791037224de02c64c11ce42338455528d1101012000490420777777777777777777777777777777777777777777777777777777777777777700050201018000aee59dc5acac5fe8d4b452c1fd0f22601b6c1d876e28d0e3512a2ff39d18bcee01207777777777777777777777777777777777777777777777777777777777777777007601b44b3a90d97444a14074c2bf69ff971d88fd27e3ba2da8b11cf3910f087dedf6030140000b00080000000000000007001002cbeddd5e01bcb90c5002f6a710bc7957af406d2a6a9df600dbfa36951abf3534100185d286a688ac17d96cdb2880a8f7174e151dfb90742370effeeaae391ee359df1100 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/identity-nonce/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/manifest.json new file mode 100644 index 00000000000..2198d5ccc35 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/manifest.json @@ -0,0 +1,30 @@ +{ + "description": "One bit flipped in the BLS signature; verification must fail with an invalid-signature error.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "identity_balance", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "error", + "class": "invalid_signature" + } +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/proof.hex new file mode 100644 index 00000000000..255f940a0c5 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/proof.hex @@ -0,0 +1 @@ +0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/signature.hex new file mode 100644 index 00000000000..e22b25531cb --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-tampered-signature/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da83d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/manifest.json new file mode 100644 index 00000000000..54af7a37e8b --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/manifest.json @@ -0,0 +1,31 @@ +{ + "description": "Envelope grovedb proof with a valid quorum signature; full tenderdash verification must pass.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "identity_balance", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "identity_balance", + "balance": 5000000000 + }, + "expected_root_hash_hex": "dad905d8fddd7a31089ed57521ff006ec5946b5648d48056bce493357675ab72" +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/proof.hex new file mode 100644 index 00000000000..255f940a0c5 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/proof.hex @@ -0,0 +1 @@ +0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-valid/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/manifest.json new file mode 100644 index 00000000000..3b49b649f4b --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/manifest.json @@ -0,0 +1,30 @@ +{ + "description": "Proof metadata carries a block id hash the quorum never signed; must fail with an invalid-signature error.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "identity_balance", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215171b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "error", + "class": "invalid_signature" + } +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/proof.hex new file mode 100644 index 00000000000..255f940a0c5 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/proof.hex @@ -0,0 +1 @@ +0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/quorum_pubkey.hex new file mode 100644 index 00000000000..a1912bdde28 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/quorum_pubkey.hex @@ -0,0 +1 @@ +b6087e2054b847ec64a1e18af3930d39fb040b73eb8a10620f9adcb1be85bf7d5ea961dc2458d601ba9003b1e39ead15 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-block-id-hash/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/manifest.json b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/manifest.json new file mode 100644 index 00000000000..af9c549bde3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/manifest.json @@ -0,0 +1,30 @@ +{ + "description": "Valid signature checked against a different quorum's public key; must fail with an invalid-signature error.", + "source": { + "platform_repo_tag": "v4.0.0", + "grovedb_repo_tag": "v5.0.0", + "generated_protocol_version": 12 + }, + "request": { + "type": "identity_balance", + "identity_id": "7777777777777777777777777777777777777777777777777777777777777777" + }, + "block": { + "height": 123456, + "core_chain_locked_height": 2000000, + "epoch": 0, + "time_ms": 1700000000000, + "protocol_version": 12, + "chain_id": "dash-testnet-51" + }, + "proof_meta": { + "round": 0, + "quorum_type": 106, + "quorum_hash_hex": "01080f161d242b323940474e555c636a71787f868d949ba2a9b0b7bec5ccd3da", + "block_id_hash_hex": "090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d606366" + }, + "expected": { + "kind": "error", + "class": "invalid_signature" + } +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/proof.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/proof.hex new file mode 100644 index 00000000000..255f940a0c5 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/proof.hex @@ -0,0 +1 @@ +0100da01039118e921c386a7a9c166dbad8a7df9b87fa7f3dc8e3bd4fb42e124cffaa65a02cc9a023abb3efe685ac357116032ee5eb34884d6c3e5861ea7ac662ab511f2371001282192d9f288b3dd8ceeba34fda695ec21d080458ed5d9503eb5916a63494a7a040160002d0401207777777777777777777777777777777777777777777777777777777777777777fd00000002540be400006ab14e39df7d262e55bae17e60bab5d996cb66d6ded567f4d445ba536eef3b931001f61ef78f4632c6b8cb48fd3087644cf1775a597afa62b67660943fccfabd887b1111010160002f03207777777777777777777777777777777777777777777777777777777777777777000b03fd00000002540be4000000 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/quorum_pubkey.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/quorum_pubkey.hex new file mode 100644 index 00000000000..42440c3cdbf --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/quorum_pubkey.hex @@ -0,0 +1 @@ +99d85ad48c7ca9ffbe49f170e444a77ca98035a446c0fe9919180860757ef0d73e8a04abb18f5b67558baed45e748100 diff --git a/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/signature.hex b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/signature.hex new file mode 100644 index 00000000000..0ca209f79f3 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/quorum-sig-wrong-quorum-key/signature.hex @@ -0,0 +1 @@ +a4fca3d223938deeb9da82d0021a2cfb8cd7463412f7613c8943cd86a4f26b9aa81daf9d218c310ed3bb9e9c6a80b38118d16d27ff981dcc7ca46a65c95be36036eb0e1c4bc31801d2aa5699fc3d9932295f375bec3073e03c4279668429d319 diff --git a/packages/rs-drive-proof-verifier/tests/vectors_contested.rs b/packages/rs-drive-proof-verifier/tests/vectors_contested.rs new file mode 100644 index 00000000000..d812d4dc183 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors_contested.rs @@ -0,0 +1,177 @@ +//! Contested-resource (DPNS name contest) proof-vector regression tests: +//! an active contest, a finished contest with a winner, and proof of +//! absence. Requests are built through [`TryFromRequest::try_to_request`] +//! so the gRPC round-trip the SDK relies on is exercised too. + +#![cfg(feature = "mocks")] + +mod common; + +use common::{identifier, load_case, Case, Expected, RequestSpec, NETWORK}; +use dapi_grpc::platform::v0::{self as platform, get_contested_resource_vote_state_response}; +use dpp::platform_value::Value; +use dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo; +use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; +use drive::query::vote_poll_vote_state_query::{ + ContestedDocumentVotePollDriveQuery, ContestedDocumentVotePollDriveQueryResultType, +}; +use drive_proof_verifier::from_request::TryFromRequest; +use drive_proof_verifier::types::Contenders; +use drive_proof_verifier::FromProof; + +fn contested_pair( + case: &Case, +) -> ( + platform::GetContestedResourceVoteStateRequest, + platform::GetContestedResourceVoteStateResponse, +) { + let RequestSpec::ContestedVoteState { + contract_id, + document_type_name, + index_name, + index_values, + count, + } = &case.manifest.request + else { + panic!("{}: expected a contested_vote_state request", case.name); + }; + let query = ContestedDocumentVotePollDriveQuery { + vote_poll: ContestedDocumentResourceVotePoll { + contract_id: identifier(contract_id), + document_type_name: document_type_name.clone(), + index_name: index_name.clone(), + index_values: index_values + .iter() + .map(|value| Value::Text(value.clone())) + .collect(), + }, + result_type: ContestedDocumentVotePollDriveQueryResultType::VoteTally, + offset: None, + limit: Some(*count), + start_at: None, + allow_include_locked_and_abstaining_vote_tally: true, + }; + let request = query + .try_to_request() + .expect("contested query must convert to a gRPC request"); + let response = platform::GetContestedResourceVoteStateResponse { + version: Some(get_contested_resource_vote_state_response::Version::V0( + get_contested_resource_vote_state_response::GetContestedResourceVoteStateResponseV0 { + metadata: Some(case.metadata()), + result: Some(get_contested_resource_vote_state_response::get_contested_resource_vote_state_response_v0::Result::Proof( + case.grpc_proof(), + )), + }, + )), + }; + (request, response) +} + +fn verify_contested(case: &Case) -> Option { + let (request, response) = contested_pair(case); + Contenders::maybe_from_proof( + request, + response, + NETWORK, + case.platform_version(), + &case.provider(), + ) + .unwrap_or_else(|e| panic!("{}: contested proof must verify: {e}", case.name)) +} + +fn assert_contenders(case: &Case, contenders: &Contenders) { + let Expected::Contested { + contenders: expected_contenders, + abstain_votes, + lock_votes, + .. + } = &case.manifest.expected + else { + panic!("{}: manifest expectation mismatch", case.name); + }; + assert_eq!( + contenders.contenders.len(), + expected_contenders.len(), + "{}: contender count", + case.name + ); + for expected in expected_contenders { + let id = identifier(&expected.identity_id); + let contender = contenders + .contenders + .get(&id) + .unwrap_or_else(|| panic!("{}: contender {} missing", case.name, expected.identity_id)); + assert_eq!( + contender.vote_tally(), + expected.votes, + "{}: votes for {}", + case.name, + expected.identity_id + ); + } + assert_eq!( + contenders.abstain_vote_tally, *abstain_votes, + "{}: abstain votes", + case.name + ); + assert_eq!( + contenders.lock_vote_tally, *lock_votes, + "{}: lock votes", + case.name + ); +} + +#[test] +fn contested_vote_state_active() { + let case = load_case("contested-vote-state-active"); + let contenders = verify_contested(&case).expect("active contest must be found"); + assert_contenders(&case, &contenders); + assert!( + contenders.winner.is_none(), + "active contest must have no winner yet" + ); +} + +#[test] +fn contested_vote_state_finished() { + let case = load_case("contested-vote-state-finished"); + let contenders = verify_contested(&case).expect("finished contest must be found"); + assert_contenders(&case, &contenders); + let Expected::Contested { + finished, + winner_identity_id, + .. + } = &case.manifest.expected + else { + panic!("manifest expectation mismatch"); + }; + assert!(*finished); + let (winner_info, finalization_block) = contenders + .winner + .as_ref() + .expect("finished contest must carry winner info"); + let expected_winner = winner_identity_id + .as_deref() + .map(identifier) + .expect("finished fixture names a winner"); + assert_eq!( + *winner_info, + ContestedDocumentVotePollWinnerInfo::WonByIdentity(expected_winner) + ); + assert!( + finalization_block.time_ms > 0, + "finalization block info must be present" + ); +} + +#[test] +fn contested_vote_state_absent() { + let case = load_case("contested-vote-state-absent"); + let Expected::ContestedAbsent = case.manifest.expected else { + panic!("manifest expectation mismatch"); + }; + assert!( + verify_contested(&case).is_none(), + "absent contest must verify as proof of non-existence (Ok(None))" + ); +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors_documents.rs b/packages/rs-drive-proof-verifier/tests/vectors_documents.rs new file mode 100644 index 00000000000..0615cd88a50 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors_documents.rs @@ -0,0 +1,236 @@ +//! Documents-family (DPNS / Dashpay) proof-vector regression tests. +//! +//! The fixture grovedb state stores placeholder ASCII payloads at the +//! document positions, so these vectors pin two things: +//! +//! 1. the [`DriveDocumentQuery`] shape each DAPI query maps to still matches +//! the proof the server generated -- checked by verifying the proof with +//! `verify_proof_keep_serialized` and comparing the root hash and the +//! recovered serialized payloads byte-for-byte; +//! 2. [`FromProof`] fails cleanly (an `Err`, never a panic) when the proven +//! payload cannot be decoded as a DPP document. + +#![cfg(feature = "mocks")] + +mod common; + +use std::collections::BTreeMap; + +use common::{hex_vec, identifier, load_case, Case, Expected, RequestSpec, NETWORK}; +use dapi_grpc::platform::v0::{self as platform, get_documents_response}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::platform_value::Value; +use dpp::prelude::DataContract; +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +use drive::query::{DriveDocumentQuery, InternalClauses, OrderClause, WhereClause, WhereOperator}; +use drive_proof_verifier::types::Documents; +use drive_proof_verifier::{Error, FromProof}; +use indexmap::IndexMap; + +fn equal_clause(field: &str, value: Value) -> (String, WhereClause) { + ( + field.to_string(), + WhereClause { + field: field.to_string(), + operator: WhereOperator::Equal, + value, + }, + ) +} + +fn asc_order(field: &str) -> IndexMap { + let mut order_by = IndexMap::new(); + order_by.insert( + field.to_string(), + OrderClause { + field: field.to_string(), + ascending: true, + }, + ); + order_by +} + +/// Builds the [`DriveDocumentQuery`] for the case, mirroring the query +/// shapes the Dash Core Platform GUI transport sends (pinned by the fixture +/// vectors). +fn document_query<'a>(case: &Case, contract: &'a DataContract) -> DriveDocumentQuery<'a> { + let (document_type_name, internal_clauses, order_by, limit) = match &case.manifest.request { + RequestSpec::DocumentsDpnsExact { + normalized_label, + limit, + } => ( + "domain", + InternalClauses { + equal_clauses: BTreeMap::from([ + equal_clause( + "normalizedParentDomainName", + Value::Text("dash".to_string()), + ), + equal_clause("normalizedLabel", Value::Text(normalized_label.to_string())), + ]), + ..Default::default() + }, + IndexMap::new(), + *limit, + ), + RequestSpec::DocumentsDpnsPrefix { + normalized_prefix, + limit, + } => ( + "domain", + InternalClauses { + equal_clauses: BTreeMap::from([equal_clause( + "normalizedParentDomainName", + Value::Text("dash".to_string()), + )]), + range_clause: Some(WhereClause { + field: "normalizedLabel".to_string(), + operator: WhereOperator::StartsWith, + value: Value::Text(normalized_prefix.to_string()), + }), + ..Default::default() + }, + asc_order("normalizedLabel"), + *limit, + ), + RequestSpec::DocumentsDashpayProfile { owner_id } => ( + "profile", + InternalClauses { + equal_clauses: BTreeMap::from([equal_clause( + "$ownerId", + Value::Identifier(identifier(owner_id).into_buffer()), + )]), + ..Default::default() + }, + IndexMap::new(), + 1, + ), + RequestSpec::DocumentsDashpayContacts { + identity_id, + to_identity, + limit, + } => ( + "contactRequest", + InternalClauses { + equal_clauses: BTreeMap::from([equal_clause( + if *to_identity { "toUserId" } else { "$ownerId" }, + Value::Identifier(identifier(identity_id).into_buffer()), + )]), + ..Default::default() + }, + asc_order("$createdAt"), + *limit, + ), + _ => panic!("{}: not a documents request", case.name), + }; + DriveDocumentQuery { + contract, + document_type: contract + .document_type_for_name(document_type_name) + .expect("system contract document type"), + internal_clauses, + offset: None, + limit: Some(limit), + order_by, + start_at: None, + start_at_included: false, + block_time_ms: None, + } +} + +fn contract_for(case: &Case) -> SystemDataContract { + match &case.manifest.request { + RequestSpec::DocumentsDpnsExact { .. } | RequestSpec::DocumentsDpnsPrefix { .. } => { + SystemDataContract::DPNS + } + RequestSpec::DocumentsDashpayProfile { .. } + | RequestSpec::DocumentsDashpayContacts { .. } => SystemDataContract::Dashpay, + _ => panic!("{}: not a documents request", case.name), + } +} + +fn run_documents_case(name: &str) { + let case = load_case(name); + let Expected::DocumentsPlaceholder { + serialized_documents, + } = &case.manifest.expected + else { + panic!("{name}: manifest expectation mismatch"); + }; + let platform_version = case.platform_version(); + let contract = load_system_data_contract(contract_for(&case), platform_version) + .expect("load system data contract"); + let query = document_query(&case, &contract); + + // The grovedb layer must verify: same root hash the fixture quorum + // signed, and exactly the recorded placeholder payloads. + let (root_hash, serialized) = query + .verify_proof_keep_serialized(&case.grovedb_proof, platform_version) + .unwrap_or_else(|e| panic!("{name}: grovedb layer must verify: {e}")); + assert_eq!( + hex::encode(root_hash), + case.manifest + .expected_root_hash_hex + .as_deref() + .expect("documents cases pin a root hash"), + "{name}: root hash" + ); + assert_eq!( + serialized, + serialized_documents + .iter() + .map(|hex| hex_vec(hex)) + .collect::>(), + "{name}: proven serialized payloads" + ); + + // Through the crate's public API the placeholder payload cannot decode + // into a DPP document: FromProof must surface a clean error. + let response = platform::GetDocumentsResponse { + version: Some(get_documents_response::Version::V0( + get_documents_response::GetDocumentsResponseV0 { + metadata: Some(case.metadata()), + result: Some( + get_documents_response::get_documents_response_v0::Result::Proof( + case.grpc_proof(), + ), + ), + }, + )), + }; + let error = >::maybe_from_proof_with_metadata( + query, + response, + NETWORK, + platform_version, + &case.provider(), + ) + .expect_err("placeholder document payload must fail to decode"); + assert!( + matches!( + error, + Error::DriveError { .. } | Error::ProtocolError { .. } + ), + "{name}: expected a document decode error, got: {error:?}" + ); +} + +#[test] +fn dpns_domain_exact() { + run_documents_case("dpns-domain-exact"); +} + +#[test] +fn dpns_domain_prefix() { + run_documents_case("dpns-domain-prefix"); +} + +#[test] +fn dashpay_profile() { + run_documents_case("dashpay-profile"); +} + +#[test] +fn dashpay_contacts_incoming() { + run_documents_case("dashpay-contacts-incoming"); +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors_identity.rs b/packages/rs-drive-proof-verifier/tests/vectors_identity.rs new file mode 100644 index 00000000000..beab23a2230 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors_identity.rs @@ -0,0 +1,260 @@ +//! Identity-family proof-vector regression tests: balance, nonce, contract +//! nonce, and public keys, plus the corrupted-proof negative case. Every +//! case replays a recorded Dash Core fixture proof through +//! [`FromProof::maybe_from_proof_with_metadata`] with the real fixture +//! quorum signature, so both the grovedb and tenderdash layers are checked. + +#![cfg(feature = "mocks")] + +mod common; + +use common::{identifier, load_case, Case, Expected, RequestSpec, NETWORK}; +use dapi_grpc::platform::v0::{ + self as platform, get_identity_balance_request, get_identity_balance_response, + get_identity_contract_nonce_request, get_identity_contract_nonce_response, + get_identity_keys_request, get_identity_keys_response, get_identity_nonce_request, + get_identity_nonce_response, key_request_type, AllKeys, KeyRequestType, +}; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::{IdentityPublicKey, KeyID}; +use dpp::serialization::PlatformDeserializable; +use drive_proof_verifier::types::{ + IdentityBalance, IdentityContractNonceFetcher, IdentityNonceFetcher, IdentityPublicKeys, +}; +use drive_proof_verifier::{Error, FromProof}; + +fn balance_pair( + case: &Case, +) -> ( + platform::GetIdentityBalanceRequest, + platform::GetIdentityBalanceResponse, +) { + let RequestSpec::IdentityBalance { identity_id } = &case.manifest.request else { + panic!("{}: expected an identity_balance request", case.name); + }; + let request = platform::GetIdentityBalanceRequest { + version: Some(get_identity_balance_request::Version::V0( + get_identity_balance_request::GetIdentityBalanceRequestV0 { + id: identifier(identity_id).to_vec(), + prove: true, + }, + )), + }; + let response = platform::GetIdentityBalanceResponse { + version: Some(get_identity_balance_response::Version::V0( + get_identity_balance_response::GetIdentityBalanceResponseV0 { + metadata: Some(case.metadata()), + result: Some( + get_identity_balance_response::get_identity_balance_response_v0::Result::Proof( + case.grpc_proof(), + ), + ), + }, + )), + }; + (request, response) +} + +// The crate's own Error type is what the public API returns; its size is +// not this test's concern. +#[allow(clippy::result_large_err)] +fn verify_balance_case(case: &Case) -> Result, Error> { + let (request, response) = balance_pair(case); + IdentityBalance::maybe_from_proof( + request, + response, + NETWORK, + case.platform_version(), + &case.provider(), + ) +} + +#[test] +fn identity_balance() { + let case = load_case("identity-balance"); + let Expected::IdentityBalance { balance: expected } = case.manifest.expected else { + panic!("manifest expectation mismatch"); + }; + let (request, response) = balance_pair(&case); + let (balance, metadata, proof) = IdentityBalance::maybe_from_proof_with_metadata( + request, + response, + NETWORK, + case.platform_version(), + &case.provider(), + ) + .expect("identity balance proof must verify"); + assert_eq!(balance, Some(expected)); + assert_eq!(metadata.height, case.manifest.block.height); + assert_eq!(metadata.chain_id, case.manifest.block.chain_id); + assert_eq!(proof.grovedb_proof, case.grovedb_proof); +} + +#[test] +fn identity_nonce() { + let case = load_case("identity-nonce"); + let RequestSpec::IdentityNonce { identity_id } = &case.manifest.request else { + panic!("manifest request mismatch"); + }; + let Expected::IdentityNonce { nonce: expected } = case.manifest.expected else { + panic!("manifest expectation mismatch"); + }; + let request = platform::GetIdentityNonceRequest { + version: Some(get_identity_nonce_request::Version::V0( + get_identity_nonce_request::GetIdentityNonceRequestV0 { + identity_id: identifier(identity_id).to_vec(), + prove: true, + }, + )), + }; + let response = platform::GetIdentityNonceResponse { + version: Some(get_identity_nonce_response::Version::V0( + get_identity_nonce_response::GetIdentityNonceResponseV0 { + metadata: Some(case.metadata()), + result: Some( + get_identity_nonce_response::get_identity_nonce_response_v0::Result::Proof( + case.grpc_proof(), + ), + ), + }, + )), + }; + let fetcher = IdentityNonceFetcher::maybe_from_proof( + request, + response, + NETWORK, + case.platform_version(), + &case.provider(), + ) + .expect("identity nonce proof must verify") + .expect("nonce must be proven present"); + assert_eq!(fetcher.0, expected); +} + +#[test] +fn identity_contract_nonce() { + let case = load_case("identity-contract-nonce"); + let RequestSpec::IdentityContractNonce { + identity_id, + contract_id, + } = &case.manifest.request + else { + panic!("manifest request mismatch"); + }; + let Expected::IdentityContractNonce { nonce: expected } = case.manifest.expected else { + panic!("manifest expectation mismatch"); + }; + let request = platform::GetIdentityContractNonceRequest { + version: Some(get_identity_contract_nonce_request::Version::V0( + get_identity_contract_nonce_request::GetIdentityContractNonceRequestV0 { + identity_id: identifier(identity_id).to_vec(), + contract_id: identifier(contract_id).to_vec(), + prove: true, + }, + )), + }; + let response = platform::GetIdentityContractNonceResponse { + version: Some(get_identity_contract_nonce_response::Version::V0( + get_identity_contract_nonce_response::GetIdentityContractNonceResponseV0 { + metadata: Some(case.metadata()), + result: Some(get_identity_contract_nonce_response::get_identity_contract_nonce_response_v0::Result::Proof( + case.grpc_proof(), + )), + }, + )), + }; + let fetcher = IdentityContractNonceFetcher::maybe_from_proof( + request, + response, + NETWORK, + case.platform_version(), + &case.provider(), + ) + .expect("identity contract nonce proof must verify") + .expect("contract nonce must be proven present"); + assert_eq!(fetcher.0, expected); +} + +#[test] +fn identity_keys() { + let case = load_case("identity-keys"); + let RequestSpec::IdentityKeys { identity_id } = &case.manifest.request else { + panic!("manifest request mismatch"); + }; + let Expected::IdentityKeys { serialized_keys } = &case.manifest.expected else { + panic!("manifest expectation mismatch"); + }; + let request = platform::GetIdentityKeysRequest { + version: Some(get_identity_keys_request::Version::V0( + get_identity_keys_request::GetIdentityKeysRequestV0 { + identity_id: identifier(identity_id).to_vec(), + request_type: Some(KeyRequestType { + request: Some(key_request_type::Request::AllKeys(AllKeys {})), + }), + limit: None, + offset: None, + prove: true, + }, + )), + }; + let response = platform::GetIdentityKeysResponse { + version: Some(get_identity_keys_response::Version::V0( + get_identity_keys_response::GetIdentityKeysResponseV0 { + metadata: Some(case.metadata()), + result: Some( + get_identity_keys_response::get_identity_keys_response_v0::Result::Proof( + case.grpc_proof(), + ), + ), + }, + )), + }; + let keys = IdentityPublicKeys::maybe_from_proof( + request, + response, + NETWORK, + case.platform_version(), + &case.provider(), + ) + .expect("identity keys proof must verify") + .expect("keys must be proven present"); + + let expected: Vec = serialized_keys + .iter() + .map(|serialized| { + IdentityPublicKey::deserialize_from_bytes(&common::hex_vec(serialized)) + .expect("expected fixture key must deserialize") + }) + .collect(); + assert_eq!(keys.len(), expected.len()); + for expected_key in &expected { + let key = keys + .get(&expected_key.id()) + .unwrap_or_else(|| panic!("key id {} missing from proof", expected_key.id())) + .as_ref() + .unwrap_or_else(|| panic!("key id {} proven absent", expected_key.id())); + assert_eq!(key, expected_key); + } + assert_eq!( + keys.keys().copied().collect::>(), + vec![0, 1, 2], + "keys must arrive ordered by id" + ); +} + +// A flipped bit inside the grovedb proof must surface as a clean proof +// error before the quorum signature is ever consulted -- never a panic and +// never an Ok result. +#[test] +fn corrupted_grovedb_proof_is_rejected() { + let case = load_case("identity-balance-corrupted-proof"); + let Expected::Error { class } = case.manifest.expected else { + panic!("manifest expectation mismatch"); + }; + assert_eq!(class, common::ErrorClass::ProofInvalid); + let error = verify_balance_case(&case).expect_err("corrupted proof must not verify"); + assert!( + matches!(error, Error::GroveDBError { .. } | Error::DriveError { .. }), + "expected a grovedb/drive proof error, got: {error:?}" + ); +} diff --git a/packages/rs-drive-proof-verifier/tests/vectors_quorum_sig.rs b/packages/rs-drive-proof-verifier/tests/vectors_quorum_sig.rs new file mode 100644 index 00000000000..9e6abf07c24 --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors_quorum_sig.rs @@ -0,0 +1,100 @@ +//! Tenderdash quorum-signature regression tests, seeded from the Dash Core +//! `quorum_sig_vectors.json` fixture (tenderdash v1.5.1, basic BLS scheme). +//! The positive case runs a full identity-balance verification whose +//! envelope carries a real quorum signature over the state; the negatives +//! flip exactly one ingredient each (signature bit, quorum key, block id +//! hash) and must fail with an invalid-signature error -- proving the +//! signature actually binds those inputs. + +#![cfg(feature = "mocks")] + +mod common; + +use common::{identifier, load_case, Case, ErrorClass, Expected, RequestSpec, NETWORK}; +use dapi_grpc::platform::v0::{ + self as platform, get_identity_balance_request, get_identity_balance_response, +}; +use drive_proof_verifier::types::IdentityBalance; +use drive_proof_verifier::{Error, FromProof}; + +// The crate's own Error type is what the public API returns; its size is +// not this test's concern. +#[allow(clippy::result_large_err)] +fn verify_balance(case: &Case) -> Result, Error> { + let RequestSpec::IdentityBalance { identity_id } = &case.manifest.request else { + panic!("{}: expected an identity_balance request", case.name); + }; + let request = platform::GetIdentityBalanceRequest { + version: Some(get_identity_balance_request::Version::V0( + get_identity_balance_request::GetIdentityBalanceRequestV0 { + id: identifier(identity_id).to_vec(), + prove: true, + }, + )), + }; + let response = platform::GetIdentityBalanceResponse { + version: Some(get_identity_balance_response::Version::V0( + get_identity_balance_response::GetIdentityBalanceResponseV0 { + metadata: Some(case.metadata()), + result: Some( + get_identity_balance_response::get_identity_balance_response_v0::Result::Proof( + case.grpc_proof(), + ), + ), + }, + )), + }; + IdentityBalance::maybe_from_proof( + request, + response, + NETWORK, + case.platform_version(), + &case.provider(), + ) +} + +fn expect_invalid_signature(name: &str) { + let case = load_case(name); + let Expected::Error { class } = case.manifest.expected else { + panic!("{name}: manifest expectation mismatch"); + }; + assert_eq!(class, ErrorClass::InvalidSignature); + let error = verify_balance(&case).expect_err("quorum signature check must fail"); + // A wrong key or wrong signed payload fails the pairing check + // (InvalidSignature); a bit-flipped signature may already fail G2 point + // decompression (SignatureVerificationError). Both are proper + // rejections of a bad quorum signature. + assert!( + matches!( + error, + Error::InvalidSignature { .. } | Error::SignatureVerificationError { .. } + ), + "{name}: expected a signature rejection, got: {error:?}" + ); +} + +#[test] +fn valid_quorum_signature_verifies() { + let case = load_case("quorum-sig-valid"); + let Expected::IdentityBalance { balance: expected } = case.manifest.expected else { + panic!("manifest expectation mismatch"); + }; + let balance = verify_balance(&case) + .expect("envelope with a valid quorum signature must verify end to end"); + assert_eq!(balance, Some(expected)); +} + +#[test] +fn tampered_signature_is_rejected() { + expect_invalid_signature("quorum-sig-tampered-signature"); +} + +#[test] +fn wrong_quorum_key_is_rejected() { + expect_invalid_signature("quorum-sig-wrong-quorum-key"); +} + +#[test] +fn wrong_block_id_hash_is_rejected() { + expect_invalid_signature("quorum-sig-wrong-block-id-hash"); +} From 82b14e562e841c0b148fc9f4375d23d7ad5deb99 Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 7 Aug 2026 18:29:50 -0500 Subject: [PATCH 6/8] ci: cover the transport-free feature cuts Feature unification hides transport-stack regressions in whole-workspace builds, so add a PR-time step checking the standalone graphs (types-only dapi-grpc, drive-proof-verifier, dash-platform-queries) and failing if hyper, rustls, or tower leaks into drive-proof-verifier's tree. Add both verification crates to the nightly per-feature check matrix and to the check-features tool's crate list. --- .../workflows/tests-rs-nightly-long-running.yml | 12 +++++++++++- .github/workflows/tests-rs-workspace.yml | 17 +++++++++++++++++ packages/check-features/src/main.rs | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-rs-nightly-long-running.yml b/.github/workflows/tests-rs-nightly-long-running.yml index 5ba1d236fe7..f65b87a37ec 100644 --- a/.github/workflows/tests-rs-nightly-long-running.yml +++ b/.github/workflows/tests-rs-nightly-long-running.yml @@ -20,7 +20,17 @@ jobs: strategy: fail-fast: false matrix: - package: [dash-sdk, rs-dapi-client, rs-dapi, dapi-grpc, dpp, drive-abci] + package: + [ + dash-sdk, + rs-dapi-client, + rs-dapi, + dapi-grpc, + dpp, + drive-abci, + drive-proof-verifier, + dash-platform-queries, + ] steps: - name: Check out repo uses: actions/checkout@v4 diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index 8883e9dd3ae..b137171d89b 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -193,6 +193,23 @@ jobs: cargo install cargo-machete 2>/dev/null || true cargo machete + # The transport-free cuts are how embedders with their own networking + # (Dash Core's platform GUI, explorers) consume verification: feature + # unification hides regressions in whole-workspace builds, so check the + # standalone graphs and assert the transport stack stays out of the + # proof-verification tree. + - name: Check transport-free feature cuts + run: | + cargo check -p dapi-grpc --no-default-features --features core,platform,client --locked + cargo check -p drive-proof-verifier --locked + cargo check -p dash-platform-queries --locked + for banned in hyper rustls tower; do + if cargo tree -p drive-proof-verifier -e normal -i "$banned" 2>/dev/null | grep -q .; then + echo "::error::$banned leaked into drive-proof-verifier's dependency tree" + exit 1 + fi + done + - name: Detect immutable structure changes if: github.event_name == 'pull_request' run: | diff --git a/packages/check-features/src/main.rs b/packages/check-features/src/main.rs index ce31fce686d..8cba6485410 100644 --- a/packages/check-features/src/main.rs +++ b/packages/check-features/src/main.rs @@ -10,6 +10,7 @@ fn main() { ("rs-drive", vec![]), ("rs-drive-proof-verifier", vec![]), ("rs-platform-wallet", vec![]), + ("dash-platform-queries", vec![]), ]; for (specific_crate, to_ignore) in crates { From 2579ed8f84c057164dff6e18b86d378a13481bbf Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 7 Aug 2026 18:29:50 -0500 Subject: [PATCH 7/8] docs(sdk): document the transport-free consumption path dapi-grpc gets a crate-level feature table (including the new transport feature and the types-only build recipe), dash-platform-queries gets a README describing who the crate is for and what lives in it, and rs-sdk's README points transport-free embedders at the split crate. --- packages/dapi-grpc/src/lib.rs | 18 +++++++++ packages/dash-platform-queries/README.md | 49 ++++++++++++++++++++++++ packages/rs-sdk/README.md | 11 ++++++ 3 files changed, 78 insertions(+) create mode 100644 packages/dash-platform-queries/README.md diff --git a/packages/dapi-grpc/src/lib.rs b/packages/dapi-grpc/src/lib.rs index 59967bc5e24..6bf92618199 100644 --- a/packages/dapi-grpc/src/lib.rs +++ b/packages/dapi-grpc/src/lib.rs @@ -1,3 +1,21 @@ +//! Protobuf message types and generated gRPC stubs for DAPI. +//! +//! # Feature flags +//! +//! | feature | meaning | +//! |---|---| +//! | `core` / `platform` / `drive` | which proto surfaces are generated | +//! | `client` | generate client stubs (generic over the transport) | +//! | `transport` | tonic's native transport: `connect()` on generated clients, TLS roots. Pulls hyper/tokio. Default-on. | +//! | `server` | generate server stubs; implies `client`, `drive`, `transport` | +//! | `serde` / `mocks` | serde derives / dump-and-replay support | +//! +//! Types-only consumers (proof verification, embedders with their own +//! transport) build with `default-features = false, features = ["platform", +//! "client"]` and get message types plus transport-generic client stubs with +//! no networking stack in the dependency tree. wasm32 consumers must use +//! `default-features = false` — tonic's transport does not build there. + pub use prost::Message; #[cfg(feature = "core")] diff --git a/packages/dash-platform-queries/README.md b/packages/dash-platform-queries/README.md new file mode 100644 index 00000000000..723b9564cac --- /dev/null +++ b/packages/dash-platform-queries/README.md @@ -0,0 +1,49 @@ +# dash-platform-queries + +Transport-free query core of the Dash Platform SDK. + +This crate carries the pieces of `dash-sdk` that build queries, encode them +onto the wire format, and decode/verify proved responses — with **no +transport dependency**: no `rs-dapi-client`, no tokio runtime, no tonic +transport stack. `dash-sdk` depends on it and re-exports everything at the +historical paths, so SDK users need no changes. + +## Who this is for + +Embedders that bring their own transport and trust context and only need the +verification/query layer: + +- **Dash Core's platform GUI** — fetches over its own gRPC-Web transport, + serves quorum keys from its locally synced LLMQ state via a + [`ContextProvider`], and verifies every response proof with + [`drive-proof-verifier`]. +- Block explorers, Electrum-style servers, hardware-wallet tooling — anything + that talks to DAPI its own way but must not trust responses. + +If you want networking, retries, and a managed connection pool, use +`dash-sdk` — it consumes this crate internally. + +## What's here + +- [`documents::DocumentQuery`] — rich document query builder, wire + encoding for both request versions, and decoding **from** the wire request + (`DocumentQuery::try_from_request`) using the same proto conversions the + server (`drive-abci`) uses, so client and server cannot drift. +- `documents::verify_documents_response` — request-driven proof verification + for document queries, delegating to `drive-proof-verifier`'s `FromProof`. +- Aggregate proof helpers (count/sum/average/ranked) shared with `dash-sdk`. +- Pure DPNS builders — `build_dpns_preorder_and_domain_documents`, label + normalization/validation — and pure DashPay contact-request document + assembly (`dashpay::build_contact_request_document`); crypto material is + supplied by the caller, keys never enter this crate. +- `transition::validation` and document-transition helpers + (`ensure_entropy_matches_document_id`, `prepare_document_for_transition`). + +## Feature flags + +- `mocks` — serde support for the types used in dump/replay test vectors + (forwarded by `dash-sdk`'s `mocks`). + +The dependency tree is checked in CI to stay free of the transport stack +(`hyper`, `rustls`, `tower`); see the "Check transport-free feature cuts" +step in `.github/workflows/tests-rs-workspace.yml`. diff --git a/packages/rs-sdk/README.md b/packages/rs-sdk/README.md index 9a33a75b7b2..cd007cc0e40 100644 --- a/packages/rs-sdk/README.md +++ b/packages/rs-sdk/README.md @@ -42,6 +42,17 @@ connection to Platform. You can see examples of mocking in [mock_fetch.rs](tests/fetch/mock_fetch.rs) and [mock_fetch_many.rs](tests/fetch/mock_fetch_many.rs). +## Transport-free consumption + +The query-building, wire-encoding, and proof-verification layers of this SDK +live in the [`dash-platform-queries`](../dash-platform-queries) crate, which +this crate depends on and re-exports at the historical paths. Embedders that +bring their own transport and trust context (Dash Core's platform GUI, block +explorers) can depend on `dash-platform-queries` + `drive-proof-verifier` +directly and get typed, proof-verified results with no tokio runtime and no +tonic transport stack in their dependency tree. See that crate's README for +details. + ## Examples You can find quick start example in `examples/` folder. Examples must be configured by setting constants. From db332fe05469361e95e48ff34d1b39803ace305e Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 7 Aug 2026 18:44:35 -0500 Subject: [PATCH 8/8] fix(sdk): align DPNS builder validation with consensus, harden embedder seams Review follow-ups on the transport-free series: - The DPNS document builder validated labels with is_valid_username, whose consecutive-hyphen rejection is stricter than the DPNS contract's schema pattern - consensus accepts names like ab--cd. Split the check: new is_consensus_valid_label matches the contract pattern exactly and gates the builder (so dash-sdk's register_dpns_name no longer refuses consensus-valid labels), while is_valid_username keeps the stricter policy for its existing FFI/wasm gates and now documents the difference. - verify_documents_response rejects aggregate projections (COUNT/SUM/AVG) up front with a pointer to the aggregate proof helpers, instead of surfacing an opaque low-level proof error; try_from_request documents that it mirrors the server's wire-shape decode, not validate_and_route business rules. - The CI transport-leak guard also asserts wasm-sdk's wasm32 tree stays free of the native transport stack. - The proof-vector corpus gains a README with an explicit coverage matrix: the four documents-family cases pin query shape and clean decode failure but stop before the BLS check (placeholder payloads in the fixture state); identity, contested, and quorum-sig families run the full pipeline. This corrects the corpus commit's broader claim. --- .github/workflows/tests-rs-workspace.yml | 4 ++ .../src/documents/document_query.rs | 22 +++++++ .../src/dpns_usernames.rs | 62 +++++++++++++++---- .../tests/vectors/README.md | 31 ++++++++++ 4 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 packages/rs-drive-proof-verifier/tests/vectors/README.md diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index b137171d89b..9194a30c243 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -208,6 +208,10 @@ jobs: echo "::error::$banned leaked into drive-proof-verifier's dependency tree" exit 1 fi + if cargo tree -p wasm-sdk --target wasm32-unknown-unknown -e normal -i "$banned" 2>/dev/null | grep -q .; then + echo "::error::$banned leaked into wasm-sdk's wasm32 dependency tree" + exit 1 + fi done - name: Detect immutable structure changes diff --git a/packages/dash-platform-queries/src/documents/document_query.rs b/packages/dash-platform-queries/src/documents/document_query.rs index 86a9e4c1b46..e0ea6f34e78 100644 --- a/packages/dash-platform-queries/src/documents/document_query.rs +++ b/packages/dash-platform-queries/src/documents/document_query.rs @@ -400,6 +400,15 @@ impl DocumentQuery { /// `contract` must be the data contract the request targets — the /// request's `data_contract_id` is checked against `contract.id()` /// and the named document type must exist on it. + /// + /// Scope caveat: this mirrors the server's *wire-shape* decoding + /// (shared clause decoders), not its full `validate_and_route` + /// business rules — e.g. SUM/AVG requiring a non-empty field, + /// GROUP BY being illegal with SELECT DOCUMENTS, or HAVING being + /// unimplemented are enforced server-side only. A request violating + /// those decodes here but can never yield a provable response from + /// a real server, so this only matters for fabricated + /// request/response pairs. pub fn try_from_request( request: GetDocumentsRequest, contract: Arc, @@ -653,6 +662,19 @@ pub fn verify_documents_response( error: format!("failed to decode GetDocumentsRequest into a DocumentQuery: {e}"), } })?; + // This entry point verifies plain document fetches only. An aggregate + // projection (COUNT/SUM/AVG) is proved with a different proof shape; + // handing it to the Documents verifier would surface as an opaque + // low-level proof error, so reject it up front instead. + if query.select != drive::query::SelectProjection::documents() { + return Err(drive_proof_verifier::Error::RequestError { + error: format!( + "verify_documents_response only verifies plain document fetches; the request \ + carries a {:?} projection — use the aggregate proof helpers instead", + query.select.function + ), + }); + } >::maybe_from_proof_with_metadata( query, response, diff --git a/packages/dash-platform-queries/src/dpns_usernames.rs b/packages/dash-platform-queries/src/dpns_usernames.rs index 4116b7255a3..68c80a39ac4 100644 --- a/packages/dash-platform-queries/src/dpns_usernames.rs +++ b/packages/dash-platform-queries/src/dpns_usernames.rs @@ -33,7 +33,7 @@ fn hash_double(data: Vec) -> [u8; 32] { /// `salt`, whose double-SHA256 over `salt ‖ ".dash"` /// becomes the preorder's `saltedDomainHash`. /// -/// The `label` must satisfy [`is_valid_username`]; the raw label is stored +/// The `label` must satisfy [`is_consensus_valid_label`]; the raw label is stored /// in the domain document's `label` property while its /// [homograph-safe](convert_to_homograph_safe_chars) form is stored in /// `normalizedLabel`. @@ -46,10 +46,10 @@ pub fn build_dpns_preorder_and_domain_documents( entropy: [u8; 32], salt: [u8; 32], ) -> Result<(Document, Document), Error> { - if !is_valid_username(label) { + if !is_consensus_valid_label(label) { return Err(Error::InvalidInput(format!( "Invalid DPNS label \"{label}\": must be 3-63 characters, alphanumeric and hyphens \ - only, starting and ending with an alphanumeric character, without consecutive hyphens" + only, starting and ending with an alphanumeric character" ))); } @@ -161,15 +161,34 @@ pub fn convert_to_homograph_safe_chars(input: &str) -> String { .collect() } -/// Check if a username is valid according to DPNS rules -/// -/// A username is valid if: -/// - It's between 3 and 63 characters long -/// - It starts and ends with alphanumeric characters (a-zA-Z0-9) -/// - It contains only alphanumeric characters and hyphens -/// - It doesn't have consecutive hyphens (enforced by the pattern) +/// Check whether a label satisfies the DPNS contract's `label` schema +/// pattern — exactly what consensus enforces, nothing stricter. /// /// Pattern: `^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$` +/// (3-63 characters, alphanumeric and hyphens, alphanumeric at both ends; +/// consecutive hyphens ARE allowed by consensus). +pub fn is_consensus_valid_label(label: &str) -> bool { + if label.len() < 3 || label.len() > 63 { + return false; + } + let chars: Vec = label.chars().collect(); + if !chars[0].is_ascii_alphanumeric() || !chars[chars.len() - 1].is_ascii_alphanumeric() { + return false; + } + chars[1..chars.len() - 1] + .iter() + .all(|&ch| ch.is_ascii_alphanumeric() || ch == '-') +} + +/// Check if a username is valid according to this crate's recommended +/// client-side policy: the consensus pattern plus a stricter rejection of +/// consecutive hyphens. +/// +/// This is deliberately narrower than [`is_consensus_valid_label`] — a name +/// like `ab--cd` is consensus-valid but rejected here, matching the +/// pre-existing policy of the mobile SDK FFI and wasm-sdk gates. Callers +/// that must accept every consensus-valid label should use +/// [`is_consensus_valid_label`] instead. /// /// # Arguments /// @@ -367,7 +386,7 @@ mod tests { let contract = dpns_contract(); let identity_id = Identifier::from([2u8; 32]); - for bad in ["", "ab", "-alice", "alice-", "alice--bob", "alice_bob"] { + for bad in ["", "ab", "-alice", "alice-", "alice_bob"] { let result = build_dpns_preorder_and_domain_documents( &contract, identity_id, @@ -382,6 +401,27 @@ mod tests { } } + /// Consecutive hyphens are consensus-valid (the DPNS contract pattern + /// `^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$` allows them), so the + /// builder must accept them even though the stricter client-side + /// [`is_valid_username`] policy rejects them. + #[test] + fn build_dpns_documents_accepts_consensus_valid_double_hyphen() { + let contract = dpns_contract(); + let identity_id = Identifier::from([2u8; 32]); + + assert!(is_consensus_valid_label("alice--bob")); + assert!(!is_valid_username("alice--bob")); + build_dpns_preorder_and_domain_documents( + &contract, + identity_id, + "alice--bob", + [3u8; 32], + [4u8; 32], + ) + .expect("consensus-valid label with consecutive hyphens must build"); + } + #[test] fn test_convert_to_homograph_safe_chars() { assert_eq!(convert_to_homograph_safe_chars("alice"), "a11ce"); diff --git a/packages/rs-drive-proof-verifier/tests/vectors/README.md b/packages/rs-drive-proof-verifier/tests/vectors/README.md new file mode 100644 index 00000000000..5e1f8c8049b --- /dev/null +++ b/packages/rs-drive-proof-verifier/tests/vectors/README.md @@ -0,0 +1,31 @@ +# Proof-vector regression corpus + +Fixture cases generated from a real Drive state (platform v4.0.0 fixtures, +protocol version 12, grovedb 5.0.0), replayed through the crate's public +`FromProof` entry points. The same fixtures are replayed byte-exact by Dash +Core's platform GUI implementation, so drift between what Drive proves and +what any client verifies fails loudly here. + +Each case directory contains `manifest.json` (request parameters, block +metadata, expected outcome, pinned root hash) plus `proof.hex`, +`signature.hex`, and `quorum_pubkey.hex`. Loaders live in +`../common/mod.rs`; the suite is gated behind the `mocks` feature. + +## Coverage matrix — what each family actually exercises + +| family | grovedb proof replay | tenderdash BLS check | notes | +|---|---|---|---| +| identity (4 cases) | ✅ | ✅ | full pipeline through `FromProof` | +| contested vote state (3) | ✅ | ✅ | incl. `Ok(None)` proof-of-absence | +| quorum-sig (4) | ✅ | ✅ | 1 positive + 3 negatives (tampered sig, wrong key, wrong block-id hash) | +| documents / DPNS / DashPay (4) | ✅ | ❌ (not reached) | fixture state stores placeholder payloads at document positions, so `FromProof` fails at document decode *before* the signature check; these cases pin the `DriveDocumentQuery` shape (root hash + serialized payloads byte-for-byte via `verify_proof_keep_serialized`) and the clean-`Err` decode failure | +| identity-balance corrupted proof (1) | ✅ (rejects) | ❌ (not reached) | negative: bit-flipped proof fails as a GroveDB error | + +The `quorum-sig-valid` case shares its proof bytes with `identity-balance` +(the corpus has 15 distinct fixtures across 16 cases): all drive fixtures +commit to the same root hash, which is exactly the app hash the quorum +signature signs — that is what lets the positive cases run a genuine BLS +verification with real fixture key material. + +Regenerate only deliberately (fixture-generation lives with the Dash Core +platform GUI's vector tooling); a regeneration should be its own commit.