Skip to content

feat(sdk): transport-free verification and query core for embedders - #4335

Draft
PastaPastaPasta wants to merge 8 commits into
v4.2-devfrom
feat/transport-free-embedder-core
Draft

feat(sdk): transport-free verification and query core for embedders#4335
PastaPastaPasta wants to merge 8 commits into
v4.2-devfrom
feat/transport-free-embedder-core

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 7, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Embedders that bring their own transport and trust context — Dash Core's platform GUI (PastaPastaPasta/dash#67), block explorers, Electrum-style servers, hardware-wallet tooling — currently cannot consume Platform's verification layer without dragging in the full networked SDK stack, and end up hand-rolling query construction and proof verification against Drive internals that can silently drift.

Concretely, before this PR:

  • dapi-grpc unconditionally built tonic's native transport on non-wasm targets, so even drive-proof-verifier (which never opens a connection) pulled hyper/tokio/rustls/ring/tower into its tree — measured at Dash Core: the transport stack alone is +123 crates of vendored, supply-chain-reviewed source per release.
  • The query-building/wire-encoding layer lived inside dash-sdk, inseparable from rs-dapi-client (a hard, non-optional dependency), so "use just the verification layer" was not expressible.
  • The proto→query decoding for document queries existed only server-side (drive-abci), so a verifying client had to reconstruct DriveDocumentQuery shapes by hand from Drive internals — ~1.5k lines of drift-prone reimplementation per embedder.
  • The DPNS/DashPay document content rules (salted domain hash, property maps, DIP-15 sizing) lived only inside networked SDK flows.

Discussed with QuantumExplorer (Slack, 2026-08-07): the agreed direction was "make an SDK cut small enough to match / split rs-sdk; refactor instead of duplicating." This PR is that refactor. It follows the same pattern tenderdash-proto already uses (client/server feature split) and the precedent of wasm-drive-verify (an in-tree transport-free drive/verify consumer).

What was done?

Eight commits, each independently reviewable and revertible (happy to split into separate PRs if preferred):

Update: commit 1 is split out as standalone PR #4344 (with its CI slice folded in), and commit 5 as PR #4345 (strengthened: +3 metadata-tamper negatives proving the signature covers the StateId fields). As slices land, this branch rebases and drops them.

  1. feat(sdk): add transport feature to dapi-grpc — default-on transport feature carrying tonic's channel/TLS features; build_transport driven by the feature (never on wasm32). Types-only consumers build with default-features = false, features = ["platform", "client"]. drive-proof-verifier's standalone tree drops 407 → 339 crates; hyper, h2, rustls, ring, tower disappear. Zero changes to drive-proof-verifier itself.
  2. refactor(sdk): extract transport-free query core into dash-platform-queries — new crate carrying DocumentQuery + wire encoding, aggregate proof helpers, DPNS free functions, transition validation; dash-sdk depends on it and re-exports everything at historical paths. Pure moves, no logic changes (one disclosed API cleanup: the documented-dead QuerySettings.request_settings field is removed; DocumentQuery::new_with_data_contract_id moves to an extension trait).
  3. refactor(sdk): decode document queries from the wire request in shared client code — the drive-abci v1 proto→clause decoders move to the shared crate (error strings byte-identical; drive-abci keeps a thin mapping shim), enabling DocumentQuery::try_from_request and the embedder entry points verify_documents_response / verify_documents_response_with_provider_contract: request-driven, typed, proof-verified document queries where client and server decode the same bytes with the same code.
  4. refactor(sdk): extract pure DPNS and DashPay document buildersbuild_dpns_preorder_and_domain_documents and build_contact_request_document as pure functions (crypto material supplied by the caller; keys never enter the crate); register_dpns_name/create_contact_request now call them.
  5. test(sdk): add proof-vector regression corpus for drive-proof-verifier — the crate's first integration tests: 16 cases from a real Drive state driving the public FromProof entry points, including genuine tenderdash BLS verification with real fixture key material, and negative cases pinning clean failures. See tests/vectors/README.md for the exact per-family coverage matrix (the four documents-family cases stop at document decode, before the BLS step — the commit message's blanket claim is corrected by the README and commit 8). The same fixtures are replayed byte-exact by Dash Core's implementation, making this a cross-implementation anchor.
  6. ci: cover the transport-free feature cuts — PR-time standalone-graph checks + a tree guard failing if hyper/rustls/tower leaks into drive-proof-verifier (native) or wasm-sdk (wasm32); both verification crates added to the nightly per-feature matrix and check-features.
  7. docs(sdk): document the transport-free consumption path — dapi-grpc feature table, dash-platform-queries README, rs-sdk README pointer.
  8. fix(sdk): align DPNS builder validation with consensus, harden embedder seams — post-review fixes: the DPNS builder validates against the exact contract schema pattern (consensus accepts consecutive hyphens; the stricter is_valid_username remains as documented client-side policy for the existing FFI/wasm gates), verify_documents_response fail-fasts on aggregate projections, wasm-target leak guard, corpus coverage README.

Known wording nits in already-made commit messages (kept rather than rewriting history mid-review): commit 2 says "no rs-sdk consumer changes imports" — two in-repo test files needed a trait import; commit 5's "full pipeline" claim is scoped down by the coverage matrix in tests/vectors/README.md.

Why not "just use the SDK"?

For a phone with no node, the SDK owning transport, trust bootstrapping (TrustedHttpContextProviderquorums.<network>.networks.dash.org), and key custody is the right design. A validating node inverts all three: it has the masternode list and LLMQ quorum keys from consensus (a trusted HTTP endpoint would be a trust downgrade), its keys live in its own wallet, and its endpoint selection is consensus-derived. It needs the layer underneath the SDK — which already existed inside these crates; this PR exposes it rather than forking it.

How Has This Been Tested?

  • Full workspace: cargo fmt --check --all, cargo clippy --workspace --all-targets --all-features --locked -- --no-deps -D warnings, cargo machete, wallet-closure check — all clean.
  • cargo test -p dash-platform-queries (36 + 9 cases), cargo test -p drive-proof-verifier --features mocks (261 existing + 16 new vector cases), cargo test -p dash-sdk --no-default-features --features mocks,offline-testing,... (164 + offline vector suite), drive-abci document_query unit tests unchanged (76 cases).
  • Transport-free cuts verified standalone: types-only dapi-grpc, drive-proof-verifier, dash-platform-queries all build --locked; cargo tree confirms no hyper/rustls/tower; wasm-sdk builds for wasm32-unknown-unknown.
  • Downstream consumers compile: rs-sdk-ffi, platform-wallet, rs-unified-sdk-ffi, wasm-sdk.
  • End-to-end consumer proof: Dash Core's platform-gui-rust branch (feat(qt): back Dash Platform GUI internals with real grovedb/drive/dpp crates PastaPastaPasta/dash#67) is being reworked to pin this branch and drive its DashPay GUI through verify_documents_response/FromProof — byte-exact against the same fixture corpus.

Breaking Changes

None for dash-sdk consumers via crates: everything moved is re-exported at its historical path. Two narrow source-level changes inside the workspace, both disclosed in commit 2's message: QuerySettings.request_settings (documented as unused) is removed, and DocumentQuery::new_with_data_contract_id requires importing the DocumentQuerySdk extension trait. dapi-grpc's transport feature is default-on, so existing native consumers are unchanged; wasm consumers already use default-features = false.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

🤖 Generated with Claude Code

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"] }
…ueries

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<GetDocumentsRequest> 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<T> 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.
…d 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).
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.
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.
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.
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.
…er 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.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0214e8c8-ae44-4409-939b-7eb3fa7fe87f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant