Skip to content

feat(sdk): add transport-free CXX bindings - #4416

Draft
PastaPastaPasta wants to merge 11 commits into
dashpay:refactor/document-query-decode-buildersfrom
PastaPastaPasta:feat/platform-cxx-bindings
Draft

feat(sdk): add transport-free CXX bindings#4416
PastaPastaPasta wants to merge 11 commits into
dashpay:refactor/document-query-decode-buildersfrom
PastaPastaPasta:feat/platform-cxx-bindings

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Dash Core's Platform GUI currently carries a private copy of its Rust/CXX bridge and pins individual Platform crates. This makes the embedding ABI, proof-verification logic, decoders, state-transition builders, and dependency closure owned by the consumer instead of Platform.

This stacked PR gives Platform ownership of that transport-free C++ embedding surface. It is based on #4389 and assumes #4388 and #4389 merge before this PR is retargeted to v4.2-dev.

What was done?

  • Added the dash-platform-cxx workspace package with the existing platform_ffi ABI for proof verification, DPP decoding, state-transition construction, quorum context, and callback-based wallet signing.
  • Added 31 Rust tests and the proof/decoder/signing vectors previously maintained with the Dash Core integration.
  • Added a standalone manifest and lockfile so downstream build systems can vendor only this package's dependency closure.
  • Added an install contract for the static archive and public/generated CXX headers.
  • Added a real C++ link-and-run smoke test and CI checks that reject native DAPI transport dependencies.
  • Integrated the package with workspace metadata, package filters, Docker package discovery, feature checks, and nightly CI.

After this lands, PastaPastaPasta/dash#67 will consume the installed archive and headers through Dash Core's depends system and remove its private Rust/CXX implementation.

How Has This Been Tested?

  • cargo test -p dash-platform-cxx --locked (31 tests)
  • cargo clippy -p dash-platform-cxx --all-targets --locked -- -D warnings
  • cargo check --manifest-path packages/rs-platform-cxx/standalone/Cargo.toml --locked
  • standalone debug C++ link-and-run smoke test
  • standalone release build and install-layout verification
  • workspace and standalone dependency-tree checks for rs-dapi-client, Hyper, Rustls, Tower, and Reqwest
  • cargo machete
  • cargo fmt --all -- --check
  • shellcheck packages/rs-platform-cxx/install.sh packages/rs-platform-cxx/test-cxx-link.sh
  • YAML parsing and git diff --check

Breaking Changes

None. The existing platform_ffi namespace and bridge ABI from the downstream integration are preserved.

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 added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

This pull request was created by Codex.

PastaPastaPasta and others added 10 commits August 18, 2026 09:24
…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.
…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 now binds the proof to the whole wire request,
  not just its SELECT projection. GroveDB and Tenderdash proofs authenticate
  the state and the resolved DriveDocumentQuery, and the DocumentQuery ->
  DriveDocumentQuery lowering drops group_by, having, offset and prove - so
  an untrusted transport could otherwise pair a request the real server
  would have refused (SELECT DOCUMENTS ... GROUP BY age) with a genuine
  proof for the narrower query it lowers to, and verification would accept
  it. Every dropped field is now rejected up front, mirroring rs-drive-abci's
  validate_and_route (non-empty HAVING for a non-aggregate SELECT, GROUP BY
  under SELECT DOCUMENTS) and reject_offset_off_the_ranked_path (OFFSET off
  the ranked surface); prove=false is rejected because an honest server
  answers such a request without a proof at all. The pre-existing aggregate-
  projection rejection (COUNT/SUM/AVG, which use a different proof shape)
  moves into the same gate. Five tests cover the rejections with a
  ContextProvider that panics if reached, pinning that they fire before any
  proof machinery runs.
- TryFrom<&DocumentQuery> for DriveDocumentQuery converts the limit with a
  checked u16::try_from instead of an `as` cast. Drive's limit is a u16 and
  the server refuses anything larger with InvalidLimit, so the cast turned a
  request for 65537 documents into a 1-document query - and a proof for that
  query then verified. This matches the offset conversion right below it,
  which already refused rather than truncated.
- try_from_request documents that it mirrors the server's wire-shape decode,
  not validate_and_route business rules, and points at the entry point that
  closes the gap.
- 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.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ions and limits

Two trust-boundary gaps let an untrusted transport pair a valid proof with a request the supplied platform version's server would have refused. First, the wire version oneof (V0/V1) was never checked against platform_version.drive_abci.query.document_query bounds, so a V1 request could be verified under a platform version whose server only serves V0. Both verify entry points now run the same check_version gate the server's query_documents dispatch runs, before any decoding, contract lookup, or proof machinery.

Second, the DocumentQuery -> DriveDocumentQuery lowering only guarded the u16 cast, so limits 101..=65535 - which the server refuses with InvalidLimit via DriveDocumentQuery::from_typed_clauses' default_query_limit cap (100) - reached a raw DriveDocumentQuery and could verify a proof no honest server would produce. The lowering now mirrors from_typed_clauses exactly: 0 stays the unset-default sentinel, 1..=100 passes, anything above is refused with the server's own QuerySyntaxError::InvalidLimit.

Tests reuse the panicking ContextProvider to pin that both rejections happen before any proof machinery runs, and cover the provider-resolved entry point rejecting before its contract lookup.
The COUNT/SUM/AVG proof verifiers each duplicated a u16::try_from-only limit conversion, so server-invalid limits in 101..=65535 - which drive's aggregate dispatchers refuse with InvalidLimit against max_query_limit before producing proof bytes - still reached the proof primitives. Same vulnerability class as the documents-path limit fix: an untrusted transport could pair such a request with a genuine proof for a different, server-permitted query.

Centralize the semantics in a shared aggregate_limit module: a single cap check (DEFAULT_QUERY_LIMIT, the compile-time twin of the config default the SDK cannot see per-operator) runs at the top of each verify helper before any proof or provider machinery, and the two 0-sentinel translations (distinct walk: 0 -> DEFAULT_QUERY_LIMIT; carrier walk: 0 -> None, unbounded outer walk) replace the six hand-rolled copies, mirroring the server dispatchers exactly.

The new test drives the COUNT verify path through FromProof with the panicking ContextProvider, pinning that over-cap limits are rejected before proof machinery runs.
…fault

The aggregate dispatchers server-side compare against drive_config.max_query_limit, so the verifier's compile-time mirror is DEFAULT_MAX_QUERY_LIMIT, not DEFAULT_QUERY_LIMIT (default_query_limit's twin, which remains correct for the documents path's from_typed_clauses comparison). Both are 100 today with no compile-time link; a const assert pins the distinct-walk fallback within the cap so a future divergence becomes a build error instead of a silent parity break.
The transport-free preorder/domain builder takes the salt as an argument, so the front-running protection the preorder commitment provides now rests on the embedder: a fresh CSPRNG 32-byte salt per registration attempt, and salt/label/domain-document secrecy until the preorder create transition is confirmed. Spell out both obligations - and that the networked SDK's register_dpns_name (StdRng::from_entropy, submit-and-wait before broadcasting the domain document) is the reference behavior - on the builder's docs.
The extraction from drive-abci widened where_operator_from_proto, value_from_proto, where_clause_from_proto, order_clause_from_proto and having_clause_from_proto to pub, but the cross-crate consumers (drive-abci's v1 conversions, the SDK decode path) only use DecodeError, the plural request-level decoders and select_from_proto. Narrow the singular helpers to pub(crate) so the shared decode surface stays as small as its actual contract.
Drive the over-cap rejection through DocumentSum and DocumentAverage as well as DocumentCount, so removing the shared cap gate from any one verify helper fails the test, and add unit tests pinning the cap boundaries (0/1/100 accepted, 101 rejected) and the 0-sentinel translations (distinct walk -> DEFAULT_QUERY_LIMIT, carrier walk -> None).
@coderabbitai

coderabbitai Bot commented Aug 18, 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: b3b0f9a3-7d97-499a-9081-a32b9fd9ad7b

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