Repository navigation
feat(sdk): add dash-platform-cxx, a thin CXX shell over dash-sdk for C++ embedders - #4633
PastaPastaPasta wants to merge 5 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
0631c07 to
5279114
Compare
d855a77 to
b283a5f
Compare
…crate dashpay/platform#4633 rebuilds the Platform CXX bindings as a thin bridge over dash-sdk: the SDK owns DAPI transport, retries and proof verification, and Core supplies endpoints, quorum keys, its ChainLock height and wallet signatures. The crate is an ordinary workspace member now, so the package vendors from the workspace root (the lockfile made vendorable by dashpay/platform#4631), builds with -p dash-platform-cxx, and installs the header tree the crate's build.rs stages plus the static archive; the nested standalone manifest and install.sh are gone with the old design. mbedtls leaves depends: the SDK carries its own TLS stack (rustls with the system trust store), so Core no longer links a TLS library for Platform. The vendoring config gains the workspace's git sources. Validated on aarch64-apple-darwin: make -C depends PLATFORM_GUI=1 platform_cxx vendors 840 crates (150 MB archive) and builds the crate offline in 3 minutes; the staged prefix carries include/dash/platform/{ffi.h,signer.h}, include/rust/cxx.h and lib/libdash_platform_cxx.a. The knob-off package set is unchanged.
…ble-platform-gui The Qt-free client library dash-qt drives for DashPay: per-network parameters and system contract ids, the PlatformClient interface, DPP decoding and state-transition adapters, and the wallet record formats. Where the earlier revision (dashpay#7626) carried its own gRPC-Web/TLS transport, hand-written protobuf and CBOR encoders, per-endpoint retry and freshness tracking, and handed request/response byte pairs to a transport-free verifier, this one is a thin consumer of the Dash Platform SDK through dash-platform-cxx (dashpay/platform#4633). The SDK owns query construction, DAPI transport, retries with address banning, proof verification (GroveDB replay plus the Tenderdash quorum signature against the keys this node pushes from its LLMQ store), protocol-version tracking and the chain-id and ChainLock freshness checks; the node supplies evonode endpoints from its deterministic masternode list, the Platform quorum keys, its best ChainLock height and wallet signatures through a digest callback, so private keys never leave the wallet. The PlatformClient interface the GUI programs against is unchanged apart from gaining an sdk() accessor; the production implementation keeps its single worker thread and callback marshalling and forwards each query to the SDK handle. Absence stays proven, never inferred: an empty result only reaches a callback after the SDK verified a proof of it. The DPP decoders and state-transition builders take the SDK handle so they build under the protocol version the SDK has seen the network run, ratcheted up from the per-network floor in params.cpp. The C++ transport, protobuf, CBOR, retry and freshness code and their unit tests are gone with the design; the DPP byte-exactness suite and the wallet key tests stay, and the fuzz harness keeps the decoder targets (proof verification is fuzzed upstream). Validated on aarch64-apple-darwin against a depends prefix carrying the SDK-backed archive: configure detects the bindings, libdash_platform.a and test_dash build, platform_dpp_tests and platformkeys_tests pass.
5279114 to
47b05e3
Compare
b283a5f to
d72b4d5
Compare
…crate dashpay/platform#4633 rebuilds the Platform CXX bindings as a thin bridge over dash-sdk: the SDK owns DAPI transport, retries and proof verification, and Core supplies endpoints, quorum keys, its ChainLock height and wallet signatures. The crate is an ordinary workspace member now, so the package vendors from the workspace root (the lockfile made vendorable by dashpay/platform#4631), builds with -p dash-platform-cxx, and installs the header tree the crate's build.rs stages plus the static archive; the nested standalone manifest and install.sh are gone with the old design. mbedtls leaves depends: the SDK carries its own TLS stack (rustls with the system trust store), so Core no longer links a TLS library for Platform. The vendoring config gains the workspace's git sources. Validated on aarch64-apple-darwin: make -C depends PLATFORM_GUI=1 platform_cxx vendors 840 crates (150 MB archive) and builds the crate offline in 3 minutes; the staged prefix carries include/dash/platform/{ffi.h,signer.h}, include/rust/cxx.h and lib/libdash_platform_cxx.a. The knob-off package set is unchanged.
…ble-platform-gui The Qt-free client library dash-qt drives for DashPay: per-network parameters and system contract ids, the PlatformClient interface, DPP decoding and state-transition adapters, and the wallet record formats. Where the earlier revision (dashpay#7626) carried its own gRPC-Web/TLS transport, hand-written protobuf and CBOR encoders, per-endpoint retry and freshness tracking, and handed request/response byte pairs to a transport-free verifier, this one is a thin consumer of the Dash Platform SDK through dash-platform-cxx (dashpay/platform#4633). The SDK owns query construction, DAPI transport, retries with address banning, proof verification (GroveDB replay plus the Tenderdash quorum signature against the keys this node pushes from its LLMQ store), protocol-version tracking and the chain-id and ChainLock freshness checks; the node supplies evonode endpoints from its deterministic masternode list, the Platform quorum keys, its best ChainLock height and wallet signatures through a digest callback, so private keys never leave the wallet. The PlatformClient interface the GUI programs against is unchanged apart from gaining an sdk() accessor; the production implementation keeps its single worker thread and callback marshalling and forwards each query to the SDK handle. Absence stays proven, never inferred: an empty result only reaches a callback after the SDK verified a proof of it. The DPP decoders and state-transition builders take the SDK handle so they build under the protocol version the SDK has seen the network run, ratcheted up from the per-network floor in params.cpp. The C++ transport, protobuf, CBOR, retry and freshness code and their unit tests are gone with the design; the DPP byte-exactness suite and the wallet key tests stay, and the fuzz harness keeps the decoder targets (proof verification is fuzzed upstream). Validated on aarch64-apple-darwin against a depends prefix carrying the SDK-backed archive: configure detects the bindings, libdash_platform.a and test_dash build, platform_dpp_tests and platformkeys_tests pass.
…crate dashpay/platform#4633 rebuilds the Platform CXX bindings as a thin bridge over dash-sdk: the SDK owns DAPI transport, retries and proof verification, and Core supplies endpoints, quorum keys, its ChainLock height and wallet signatures. The crate is an ordinary workspace member now, so the package vendors from the workspace root (the lockfile made vendorable by dashpay/platform#4631), builds with -p dash-platform-cxx, and installs the header tree the crate's build.rs stages plus the static archive; the nested standalone manifest and install.sh are gone with the old design. mbedtls leaves depends: the SDK carries its own TLS stack (rustls with the system trust store), so Core no longer links a TLS library for Platform. The vendoring config gains the workspace's git sources. Validated on aarch64-apple-darwin: make -C depends PLATFORM_GUI=1 platform_cxx vendors 840 crates (150 MB archive) and builds the crate offline in 3 minutes; the staged prefix carries include/dash/platform/{ffi.h,signer.h}, include/rust/cxx.h and lib/libdash_platform_cxx.a. The knob-off package set is unchanged.
…ble-platform-gui The Qt-free client library dash-qt drives for DashPay: per-network parameters and system contract ids, the PlatformClient interface, DPP decoding and state-transition adapters, and the wallet record formats. Where the earlier revision (dashpay#7626) carried its own gRPC-Web/TLS transport, hand-written protobuf and CBOR encoders, per-endpoint retry and freshness tracking, and handed request/response byte pairs to a transport-free verifier, this one is a thin consumer of the Dash Platform SDK through dash-platform-cxx (dashpay/platform#4633). The SDK owns query construction, DAPI transport, retries with address banning, proof verification (GroveDB replay plus the Tenderdash quorum signature against the keys this node pushes from its LLMQ store), protocol-version tracking and the chain-id and ChainLock freshness checks; the node supplies evonode endpoints from its deterministic masternode list, the Platform quorum keys, its best ChainLock height and wallet signatures through a digest callback, so private keys never leave the wallet. The PlatformClient interface the GUI programs against is unchanged apart from gaining an sdk() accessor; the production implementation keeps its single worker thread and callback marshalling and forwards each query to the SDK handle. Absence stays proven, never inferred: an empty result only reaches a callback after the SDK verified a proof of it. The DPP decoders and state-transition builders take the SDK handle so they build under the protocol version the SDK has seen the network run, ratcheted up from the per-network floor in params.cpp. The C++ transport, protobuf, CBOR, retry and freshness code and their unit tests are gone with the design; the DPP byte-exactness suite and the wallet key tests stay, and the fuzz harness keeps the decoder targets (proof verification is fuzzed upstream). Validated on aarch64-apple-darwin against a depends prefix carrying the SDK-backed archive: configure detects the bindings, libdash_platform.a and test_dash build, platform_dpp_tests and platformkeys_tests pass.
…crate dashpay/platform#4633 rebuilds the Platform CXX bindings as a thin bridge over dash-sdk: the SDK owns DAPI transport, retries and proof verification, and Core supplies endpoints, quorum keys, its ChainLock height and wallet signatures. The crate is an ordinary workspace member now, so the package vendors from the workspace root (the lockfile made vendorable by dashpay/platform#4631), builds with -p dash-platform-cxx, and installs the header tree the crate's build.rs stages plus the static archive; the nested standalone manifest and install.sh are gone with the old design. mbedtls leaves depends: the SDK carries its own TLS stack (rustls with the system trust store), so Core no longer links a TLS library for Platform. The vendoring config gains the workspace's git sources. Validated on aarch64-apple-darwin: make -C depends PLATFORM_GUI=1 platform_cxx vendors 840 crates (150 MB archive) and builds the crate offline in 3 minutes; the staged prefix carries include/dash/platform/{ffi.h,signer.h}, include/rust/cxx.h and lib/libdash_platform_cxx.a. The knob-off package set is unchanged.
…ble-platform-gui The Qt-free client library dash-qt drives for DashPay: per-network parameters and system contract ids, the PlatformClient interface, DPP decoding and state-transition adapters, and the wallet record formats. Where the earlier revision (dashpay#7626) carried its own gRPC-Web/TLS transport, hand-written protobuf and CBOR encoders, per-endpoint retry and freshness tracking, and handed request/response byte pairs to a transport-free verifier, this one is a thin consumer of the Dash Platform SDK through dash-platform-cxx (dashpay/platform#4633). The SDK owns query construction, DAPI transport, retries with address banning, proof verification (GroveDB replay plus the Tenderdash quorum signature against the keys this node pushes from its LLMQ store), protocol-version tracking and the chain-id and ChainLock freshness checks; the node supplies evonode endpoints from its deterministic masternode list, the Platform quorum keys, its best ChainLock height and wallet signatures through a digest callback, so private keys never leave the wallet. The PlatformClient interface the GUI programs against is unchanged apart from gaining an sdk() accessor; the production implementation keeps its single worker thread and callback marshalling and forwards each query to the SDK handle. Absence stays proven, never inferred: an empty result only reaches a callback after the SDK verified a proof of it. The DPP decoders and state-transition builders take the SDK handle so they build under the protocol version the SDK has seen the network run, ratcheted up from the per-network floor in params.cpp. The C++ transport, protobuf, CBOR, retry and freshness code and their unit tests are gone with the design; the DPP byte-exactness suite and the wallet key tests stay, and the fuzz harness keeps the decoder targets (proof verification is fuzzed upstream). Validated on aarch64-apple-darwin against a depends prefix carrying the SDK-backed archive: configure detects the bindings, libdash_platform.a and test_dash build, platform_dpp_tests and platformkeys_tests pass.
cfb93ac to
edb833b
Compare
|
🕓 Review not started yet because this PR is a draft.
Commit fb52ec0. Normal review starts when eligible; priority review starts as soon as a slot is available. |
25fd33c to
b40f44c
Compare
b40f44c to
09f4850
Compare
09f4850 to
49f76ed
Compare
49f76ed to
2a0a1cc
Compare
2d13776 to
0f8fc29
Compare
2a0a1cc to
0c57897
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
The CXX shell has four blocking correctness defects: PV13 pagination can silently omit contact requests, contested vote state is truncated at 100 contenders without continuation, rejected foreign-chain responses can poison the SDK's protocol-version state, and document builders omit the state-dependent contest funding required for existing contests. Several additional API, input-validation, and test-quality issues should also be addressed before relying on the new interface.
🔴 4 blocking | 🟡 6 suggestion(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: ffi-engineer); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: platform-versioning); reviewer 5: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 6: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 10: gpt-6-astra (agent: phase2-reviewer, role: platform-versioning); reviewer 11: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 12: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)
- Triage:
criticalbygpt-6-astra(effort low) — This large, intricate new FFI layer directly introduces signature and key-handling logic in packages/rs-platform-cxx/src/signer.rs, including wallet signing callbacks and AssetLockSigner::sign_ecdsa compact-signature parsing and public-key recovery, alongside signed state-transition assembly in builders.rs. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— architecture-layering (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— ffi-engineer (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— platform-versioning (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— security-auditor (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 13% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6-astra— final-verifier; agentastra-verifier - Phase 2 reviewers:
gpt-6-astra— general (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-platform-cxx/src/ops.rs`:
- [BLOCKING] packages/rs-platform-cxx/src/ops.rs:644-670: Guard contact-request pagination against PV13 timestamp-boundary skips
This query orders by `$createdAt` and uses the generic document-ID cursor, but protocol versions below 14 can return a proven empty continuation when a page boundary splits contact requests sharing the same `$createdAt`. The first page can therefore contain 100 requests and report `has_more`, while the next call returns zero items and `has_more = false`, silently omitting requests. Requests created in the same Platform block can share a timestamp, and the behavior affects both incoming and outgoing queries. Use a safe continuation strategy or return `Unavailable` for unsupported PV13 continuations, as `names_of_identity` already does, and add same-timestamp paging coverage.
- [BLOCKING] packages/rs-platform-cxx/src/ops.rs:724-739: Expose continuation for contested states exceeding 100 contenders
The request always uses `limit: Some(CONTESTED_VOTE_COUNT)` with `CONTESTED_VOTE_COUNT = 100` and `start_at: None`. `VerifiedContested` exposes neither a cursor nor an incomplete-result indicator. Contests can legally contain more than 100 contenders; the current protocol limits permit up to 1,000. The API therefore returns a successful, proved first page as though it were the complete contested state, so an embedder cannot reliably find a later contender or display all tallies. Add pagination/completeness information while preserving the one-request-per-call contract.
- [BLOCKING] packages/rs-platform-cxx/src/ops.rs:169-227: Apply chain acceptance before the SDK mutates verification state
`fetch` calls `client.run`, which performs proof verification inside the SDK, before calling the shell's `accept` function. Proof verification invokes `Sdk::verify_response_metadata`, which ratchets the shared SDK protocol version before `accept` checks the signed chain ID. A correctly signed response from a foreign chain can therefore advance the SDK to a newer protocol version, be rejected as `ChainIdMismatch`, and leave a later valid legacy response unable to parse under the mutated version. The shell's own watermark remains unchanged, but the shared SDK state is still poisoned. Add an SDK acceptance hook that checks the embedder's chain policy after proof/signature verification and before `verify_response_metadata` commits the version, or otherwise prevent rejected metadata from mutating SDK state.
In `packages/rs-platform-cxx/src/builders.rs`:
- [BLOCKING] packages/rs-platform-cxx/src/builders.rs:213-228: Preserve state-dependent contest-fund preparation in the document builder
The create path passes `None` for `StateTransitionCreationOptions`, so it bypasses `rs-sdk`'s `with_contest_fund_to_join` preparation. That preparation reads the existing contender count and increases the prefund when joining an existing contest. At the first funding-doubling threshold, the transition built here carries only the base fund while Drive requires the larger amount and rejects it with `DocumentContestNotPaidForError`. The CXX API exposes no contest maximum or creation-options parameter that lets callers supply the required amount. Share the SDK's canonical preparation/build path or expose an explicit, state-aware maximum and test a domain joining an existing contest.
- [SUGGESTION] packages/rs-platform-cxx/src/builders.rs:256-314: Keep protocol-owned document and DIP-15 encoding logic in lower layers
The CXX shell duplicates protocol semantics that already belong to lower layers: DPNS preorder hashing and document properties, DashPay profile create/replace assembly, and the DIP-15 ASK28/account-reference bit layout in `helpers.rs:71-83`. The implementations match today, but future system-contract or DIP-15 corrections can update the SDK or `platform-encryption` while leaving C++ callers on stale behavior. Add pure, parameterized builders and MAC-based helpers to the owning crates, then have this adapter perform only FFI conversion and signing integration.
- [SUGGESTION] packages/rs-platform-cxx/src/builders.rs:455-466: Reject contradictory recipient IDs before building a contact request
`ContactRequestInput` carries `to_user_id`, but the builder never validates or uses it. The SDK derives the document's `toUserId` from `recipient.id()` instead. If the two FFI inputs disagree, the builder successfully signs a request addressed to a different identity than the caller supplied, making contradictory embedder state silently actionable. Validate equality before invoking the SDK or remove the redundant field, and add a mismatch test that confirms no signer callback is made.
- [SUGGESTION] packages/rs-platform-cxx/src/builders.rs:418-423: Check the profile revision increment before building a replacement
`existing.revision + 1` is computed from embedder-supplied data without overflow checking. At `u64::MAX`, debug builds panic and release builds wrap to zero, producing an invalid replacement revision rather than rejecting the input before signing.
In `packages/rs-platform-cxx/tests/cxx_smoke.cc`:
- [SUGGESTION] packages/rs-platform-cxx/tests/cxx_smoke.cc:159-169: Value-initialize the C++ identity key before passing it to Rust
The generated C++ `ContractBounds` contains a `std::array<uint8_t, 32> contract_id` without a default member initializer. `platform_ffi::IdentityKey key;` therefore leaves that array indeterminate; assigning only `bounds.kind` does not initialize it. Rust unconditionally copies `bounds.contract_id` before matching `NoBounds`, so this smoke test reads uninitialized bytes across the CXX boundary even though the field is unused for this variant.
In `packages/rs-platform-cxx/src/helpers.rs`:
- [SUGGESTION] packages/rs-platform-cxx/src/helpers.rs:71-84: Reject or explicitly constrain DIP-15 versions above four bits
The public helper accepts `version: u32` but stores it in the top four bits using `(version << 28)`. Values above `0x0f` silently lose their high bits; for example, version 16 produces version bits zero and cannot round-trip through the inverse helper. Validate the range at the FFI boundary or change the API to return an error instead of silently truncating caller input.
In `packages/rs-platform-cxx/src/lib.rs`:
- [SUGGESTION] packages/rs-platform-cxx/src/lib.rs:873-885: Keep the network-free contact builder independent of endpoint readiness
Unlike the other builders, `build_contact_request` calls `self.0.sdk()?` before invoking the SDK's local contact-document assembly. With no endpoints pushed, this returns `no evonode endpoints` even when a verified protocol version and all contact inputs are available. The API documents builders as requiring no network request, and the other builders work after verification without an endpoint set. Provide an endpoint-independent SDK/context handle for this local operation or otherwise make the readiness precondition consistent across builders.
0f8fc29 to
324dbc9
Compare
c5752ab to
35eac29
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Preliminary review — Phase 1 blocker gate
The CXX shell preserves the intended trust and version-gating boundaries, but several previously identified correctness issues remain. In particular, contested vote results are silently truncated, contact-request pagination can skip results on PV13, and foreign-chain proof verification can mutate SDK protocol-version state before the shell rejects the response.
Validated blockers were found by the Phase-1 review and confirmed by a fresh verifier. Phase 2 is deferred until a fresh same-head revalidation clears the blocker gate.
🔴 4 blocking | 🟡 6 suggestion(s)
10 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: ffi-engineer); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: platform-versioning); reviewer 5: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 6: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-gate-verifier, role: verifier)
- Triage:
criticalbygpt-6-astra(effort low) — This large, intricate addition directly implements signature and key-handling boundaries in packages/rs-platform-cxx/src/signer.rs (including AssetLockSigner::sign_ecdsa signature parsing and public-key recovery) and quorum trust-anchor enforcement in src/provider.rs, meeting both the complexity and critical-surface criteria. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— architecture-layering (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— ffi-engineer (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— platform-versioning (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— security-auditor (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 13% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6-astra— verifier; agentastra-gate-verifier - Phase 2 reviewers: not run (deferred by blocker gate)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-platform-cxx/src/lib.rs`:
- [SUGGESTION] packages/rs-platform-cxx/src/lib.rs:873-885: Keep the network-free contact builder independent of endpoint readiness
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701915)
The contact builder only assembles and signs a request using the local context provider; it does not perform network I/O. However, this entry point requires `self.0.sdk()`, which returns an error until endpoints have been installed and also fails after endpoints are cleared. This unnecessarily couples offline transition construction to transport readiness. Retain a provider-backed SDK or otherwise separate builder SDK acquisition from the live network SDK.
In `packages/rs-platform-cxx/src/builders.rs`:
- [SUGGESTION] packages/rs-platform-cxx/src/builders.rs:418-423: Check the profile revision increment before building a replacement
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701907)
The replacement revision is derived from embedder-supplied data with plain `existing.revision + 1`. At `u64::MAX` this panics in debug builds or wraps in release builds, rather than returning a controlled error before signing. Use checked arithmetic at the boundary.
- [SUGGESTION] packages/rs-platform-cxx/src/builders.rs:455-468: Reject contradictory recipient IDs before building a contact request
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701877)
The bridge accepts both a structured recipient identity and `input.to_user_id`, but the builder never compares them. The SDK request is constructed from the structured recipient, so a caller can provide a different `to_user_id` and receive a successfully built request addressed to a different identity than the input claims. Validate the two IDs before deriving the shared secret and signing.
- [SUGGESTION] packages/rs-platform-cxx/src/builders.rs:256-314: Keep protocol-owned document and DIP-15 encoding logic in lower layers
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701869)
The shell duplicates protocol-owned DPNS commitment hashing and preorder/domain document construction that already has a canonical SDK/DPP path. The PR describes this crate as a thin shell, but these copies can drift when the SDK or system-contract behavior changes, causing transitions to be rejected only after signing. Move the canonical constructors or encodings into the owning layer and keep this crate focused on FFI translation.
- [BLOCKING] packages/rs-platform-cxx/src/builders.rs:213-228: Preserve state-dependent contest-fund preparation in the document builder
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701856)
The SDK's normal document-create path calls `with_contest_fund_to_join`, which reads the current contest state and supplies the maximum fund required to join, including the increased amount for crowded contests. This CXX builder calls the lower-level DPP constructor directly with `None` for `StateTransitionCreationOptions` and performs no equivalent state read. DPP only applies a contest fund when one is supplied; it does not perform the SDK's live contest lookup here. Consequently, a contested domain can be built without the required join fund and be rejected after signing and nonce/fee processing. The builder must obtain and pass the state-dependent contest fund, or expose a state-aware SDK helper for this path.
In `packages/rs-platform-cxx/src/helpers.rs`:
- [SUGGESTION] packages/rs-platform-cxx/src/helpers.rs:71-84: Reject or explicitly constrain DIP-15 versions above four bits
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701903)
The account-reference format reserves only the top four bits for the rotation version, but this function accepts an unrestricted `u32` and shifts it into those bits. Values above 15 silently alias or corrupt the encoded reference. Reject versions greater than 15, or explicitly mask them if wrapping is the intended contract and document that behavior.
In `packages/rs-platform-cxx/tests/cxx_smoke.cc`:
- [SUGGESTION] packages/rs-platform-cxx/tests/cxx_smoke.cc:159-169: Value-initialize the C++ identity key before passing it to Rust
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701882)
The smoke test default-declares `platform_ffi::IdentityKey key;` and initializes only selected fields. The bounds structure includes an array field that is not explicitly initialized, so this test can pass indeterminate bytes across the CXX boundary. Use value-initialization to make the bridge input deterministic.
In `packages/rs-platform-cxx/src/ops.rs`:
- [BLOCKING] packages/rs-platform-cxx/src/ops.rs:169-227: Apply chain acceptance before the SDK mutates verification state
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701850)
The shell checks `metadata.chain_id` only after `client.run(op)` has completed SDK proof verification. `Client::platform_version` documents that the SDK ratchets its internal protocol version during verification, before this chain-id check. A validly signed response from another chain can therefore mutate the shared SDK's version before being rejected by the shell. That state is later used by SDK operations such as contact-request creation, so the foreign-chain response can influence subsequent local behavior. Move chain acceptance ahead of the ratchet or isolate and roll back the SDK state when the chain check fails.
- [BLOCKING] packages/rs-platform-cxx/src/ops.rs:724-755: Expose continuation for contested states exceeding 100 contenders
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701841)
The contested vote query sets `limit: Some(CONTESTED_VOTE_COUNT)` with a limit of 100 and no `start_at`, offset, cursor, or `has_more` result. Contests can contain more than 100 contenders, so the FFI returns an apparently successful but incomplete tally. Either page through all contenders internally or expose a continuation mechanism and make truncation explicit.
- [BLOCKING] packages/rs-platform-cxx/src/ops.rs:644-677: Guard contact-request pagination against PV13 timestamp-boundary skips
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701828)
Contact requests are ordered by `$createdAt` but continuation uses only a document-id `StartAfter` cursor while the optional filter uses `$createdAt > since_ms`. The neighboring `names_of_identity` implementation explicitly rejects PV13 continuation because this class of index/cursor behavior can return an empty page while entries remain. Contact-request pagination has no equivalent guard, overlap, or compound timestamp-plus-ID cursor, so a multi-page PV13 inbox can silently skip requests at the page boundary. Apply the same PV13 guard or implement a boundary-safe cursor.
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The exact head preserves the SDK proof-verification boundary and does not modify shipped consensus behavior. Eight client/API and FFI-fixture issues remain; under the supplied severity policy, these are suggestions rather than consensus blockers. This verification was static only: no builds or tests were run, and the supplied CI snapshot contains no Rust/CXX validation result.
🟡 8 suggestion(s)
8 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This large new integration directly implements signature handling and asset-lock public-key recovery in packages/rs-platform-cxx/src/signer.rs and quorum-key trust and proof freshness checks in provider.rs, meeting both the intricacy and critical-surface criteria. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 15% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-platform-cxx/src/builders.rs`:
- [SUGGESTION] packages/rs-platform-cxx/src/builders.rs:217-228: Preserve state-dependent contest-fund preparation in the document builder
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701856)
The final None leaves creation options at their default, so DPP attaches only the base contested-document fund. At PV14, required_vote_resolution_fund_to_join requires twice that amount when 250 contenders already exist, with further increases every 50 contenders. Document-create state_v2 rejects an insufficient stated maximum during paid validation. The regular SDK create path prepares this through with_contest_fund_to_join, but build_dpns_domain exposes no funding override, so callers cannot construct a valid join for these populated contests through the bridge. Preserve network-free construction by accepting an explicit contest-fund maximum and forwarding it through StateTransitionCreationOptions.contest_fund, with state-dependent preparation delegated to the SDK. Add a populated-contest regression; comparisons against another DPP construction using None reproduce the omission.
- [SUGGESTION] packages/rs-platform-cxx/src/builders.rs:455-466: Reject contradictory recipient IDs before building a contact request
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701877)
ContactRequestInput exposes to_user_id, but this conversion never reads or validates it. The SDK constructs toUserId from the separately supplied recipient identity, so contradictory bridge inputs can successfully sign a request addressed to a different identity from the one named in input.to_user_id. The public-key check validates the encryption key selected from that recipient identity; it does not establish agreement between the two destination IDs. Reject a mismatch before minting or signing, or remove the redundant field so the bridge has one authoritative destination. Add a negative case asserting that the signer is not called.
- [SUGGESTION] packages/rs-platform-cxx/src/builders.rs:418-424: Check the profile revision increment before building a replacement
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701907)
The bridge revision is an unrestricted u64, and the preceding validation rejects only values below INITIAL_REVISION. An input of u64::MAX reaches existing.revision + 1: overflow-checked builds panic, while ordinary release builds wrap to zero. The replacement constructor checks that a revision exists but does not reject zero before signing, so release mode can return a signed invalid replacement. Use checked_add, or construct the document at the existing revision and delegate to DPP's checked increment_revision method. Return a descriptive local error and test that the maximum revision is refused before the signer callback.
In `packages/rs-platform-cxx/src/ops.rs`:
- [SUGGESTION] packages/rs-platform-cxx/src/ops.rs:723-727: Expose continuation for contested states exceeding 100 contenders
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701841)
This query always requests the first 100 contenders. The bridge accepts no continuation, and VerifiedContested exposes neither page metadata nor a completeness indicator. Valid contests can exceed this limit; PV14 permits 1,000 contenders. A contest containing 101 contenders therefore returns Ok while omitting an identity and its tally, and the caller cannot retrieve that omitted entry through this API. The proof authenticates the bounded query, not a complete contender list. Expose the existing query's start_at capability and page metadata, preserving one SDK request per bridge call, or return an explicit incomplete outcome when completeness cannot be established. Cover a contest exceeding 100 contenders in the replay suite.
- [SUGGESTION] packages/rs-platform-cxx/src/ops.rs:642-664: Guard contact-request pagination against PV13 timestamp-boundary skips
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701828)
StartAfter is unsafe here at PV13 when a page boundary splits requests sharing $createdAt. With a positive since_ms, the frozen v0 lowering passes the exclusive cursor to WhereClause::to_path_query, which starts strictly after the cursor's timestamp key; the equality-only route also excludes that terminal timestamp key. Remaining document IDs at that timestamp are skipped. Both prover and verifier use this lowering, so the next page can verify successfully and return Ok with has_more=false despite omitted contacts. PV14's cursor-padding path is version-gated and does not protect PV13. Add a fail-closed legacy continuation guard, analogous to names_of_identity, or a demonstrably complete legacy paging strategy. Test a page boundary splitting equal timestamps at PV13 and PV14 without changing shipped Drive v0.
In `packages/rs-platform-cxx/tests/cxx_smoke.cc`:
- [SUGGESTION] packages/rs-platform-cxx/tests/cxx_smoke.cc:157: Value-initialize the C++ identity key before passing it to Rust
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701882)
The pinned CXX 1.0.198 generator supplies default member initializers for primitive scalar fields, but not std::array members. IdentityKey key therefore leaves bounds.contract_id indeterminate. Assigning NoBounds does not initialize that array, and builders::contract_bounds reads it into an Identifier before matching the bounds kind. The devnet builder path consequently reads uninitialized input across the FFI boundary. Value-initialize the key. Apply the same initialization discipline to the devnet ContactRequestInput at line 204: recipient_pubkey is never assigned, yet build_contact_request reads it through PublicKey::from_slice before producing the expected error. Rust Default and catch_unwind do not make these C++ inputs initialized.
In `packages/rs-platform-cxx/src/lib.rs`:
- [SUGGESTION] packages/rs-platform-cxx/src/lib.rs:876-881: Keep the network-free contact builder independent of endpoint readiness
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701915)
This local builder requires self.0.sdk(), which fails when the endpoint set is empty. After an accepted read establishes the protocol version, set_endpoints([]) deliberately drops that SDK while preserving the verified version. Other builders remain usable, but contact construction fails with no evonode endpoints even though both identities, encryption inputs, nonce and signing callback are supplied locally and the contract comes from the local provider. Separate local minting capability from transport readiness rather than retaining the connection-owning SDK solely for this builder. Add coverage for accepting a read, clearing endpoints, and then constructing a contact request.
In `packages/rs-platform-cxx/src/helpers.rs`:
- [SUGGESTION] packages/rs-platform-cxx/src/helpers.rs:78-84: Reject or explicitly constrain DIP-15 versions above four bits
(existing thread: https://github.com/dashpay/platform/pull/4633#discussion_r4129701903)
The bridge accepts a u32 version but encodes only its low four bits through version << 28. Version 16 therefore encodes identically to version 0, and the advertised inverse cannot recover the supplied version. This matches the existing platform-encryption formula, so it is not an encoding divergence, but the new API does not explicitly state a supported input range or rollover policy. Document and test a 0..=15 precondition, explicitly define modulo-16 behavior, or make the operation fallible for larger versions. This avoids callers treating an unrestricted rotation counter as a lossless input.
packages/rs-platform-cxx rebuilt on v4.2-dev with dash-sdk (default-features off: dpns-contract, dashpay-contract, core_key_wallet) and platform-encryption as its only Platform dependencies; the direct dpp/drive/platform-version/context-provider/dash-platform-queries dependencies, the #4632 builders and the decode/st/queries modules are gone. Bridge (namespace platform_ffi): Config{network, tenderdash_chain_id, platform_llmq_type, proxy}; nine-kind Status; one Verified* struct per read with typed ProvenAbsent (also under an unsupported protocol version, which meta then shows; the default value could not tell absence apart); one page per bridge call with a StartAfter cursor, has_more computed against the query's own limit (at PV13 Drive answers a names_of_identity continuation with an empty page); typed BroadcastResult; builders returning Built{bytes, hash, object_id}; pure DPNS/DIP-15 helpers. Every entry, including new_platform_client and Drop, runs under catch_unwind, and the crate refuses panic=abort at compile time as well as in build.rs. provider.rs is the push-model ContextProvider: LLMQ-type gate, quorum-hash normalization from Core's internal uint256 order, refusal of every proof until a local ChainLock height is pushed, a 288-block ChainLock-lag floor with no ceiling, and the compiled-in DPNS/DashPay contracts cached per protocol version with negative entries. Provider errors map to Status by variant, also when raised inside proof verification. client.rs builds the SDK lazily from the https endpoint set the embedder pushes (any other scheme is refused: the address list would dial http in the clear; the host must be an IP address, or an onion name when a proxy is set, so nothing is resolved locally). Config.proxy{kind, address, isolate} is the embedder's SOCKS5 proxy (none, a numeric TCP address or a Unix socket; isolate = fresh random credentials per connection, one Tor circuit each), fixed for the client's lifetime and passed to every SDK it builds through SdkBuilder::with_proxy, with a 15 s connect budget instead of 5 s; a proxy the shell cannot use fails new_platform_client, so there is never a direct fallback. A proxy failure is Unavailable (the SDK neither retries nor bans for it), not Rejected. The DAPI client keeps a pooled channel, and so an open connection, per evonode it has talked to, and removing an address from the list does not touch the pool, which is private to the client: an empty set therefore drops the SDK (no Platform socket stays open while the embedder has disabled networking), a set that removes endpoints rebuilds the SDK over the same, updated AddressList (retained entries keep their ban state), and a set that only adds updates the list in place. The provider, the height watermark and the verified protocol version belong to the client and survive every rebuild; a rebuilt SDK is seeded at the verified version. Proved reads are not dispatched before a ChainLock anchor is pushed or after shutdown. The SDK watermark is off; ops.rs checks the chain id, keeps a tolerance-3 Platform-height watermark keyed after it, records the protocol version of every accepted response, and signals UnsupportedProtocolVersion while still returning the value. Builders and contested_vote_fund_credits take that verified version (transition rules and the contested prefund changed across versions), so they fail before the first accepted read, except on a devnet whose floor is the latest version, and once a version this build does not know was seen. Document queries load the compiled-in contracts at this build's latest version, since a network SDK seeded at the PV13 floor could not decode a DashPay v2 profile. A node's definitive (non-retryable) gRPC refusal of a read or a broadcast, including tonic's answer to a response above the 4 MiB decoding bound, is Rejected; no answer is Unavailable. search_names refuses an empty or over-long prefix before dispatch, which Drive would refuse anyway. runtime.rs owns the tokio runtime and aborts the in-flight task on shutdown. signer.rs: BytesSigner hands the full signable preimage to WalletSigner::SignForKey; AssetLockSigner is the single digest path and recovers the public key from the compact signature (compressed-key header only, as Core funds P2PKH of the compressed key), so PublicKeyForKey is gone. The adapters take a SignerCallbacks trait whose Send + Sync impls sit on the extern type under the any-thread contract signer.h states. builders.rs: identity create through dpp try_from_identity_with_signers, DPNS preorder/domain and the profile create assembled inline until rs-sdk exports them, each create given the document id put_to_platform derives from its entropy and nonce at the verified version (dpp derives it itself only from PV14; up to PV13 it sends the document's id as is, and a zero id is refused by Drive with InvalidDocumentTransitionIdError), a property the network's contract does not have yet refused before signing, contact requests through Sdk::create_contact_request with client-side ECDH under the verified version. build_profile replaces the Profile a get_profile read at its revision + 1 and carries the fields the embedder does not edit (avatar, DashPay v2 payment addresses) into the replacement, since a replace transition carries the whole document and would otherwise erase what another wallet set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d PV14 Every test that depends on the protocol version runs at PV13 (what testnet and mainnet run, the SDK's floor for them) and at this build's latest, parametrized with test_case::test_matrix; the Drive fixture is generated at each version, signed by one test quorum. tests/replay.rs replays the proved reads and the broadcast through the same client code an embedder runs, against dash-sdk's mock transport fed with proofs generated from a real Drive state (tests/common/mod.rs): GroveDB proofs from the state and a Tenderdash StateId/CanonicalVote signed by a test BLS quorum, so the SDK runs the full proof replay and quorum-signature check and only the socket is mocked. It covers the design's freshness matrix (proofless, tampered proof, tampered signature, unknown quorum, quorum hash in Core order, wrong LLMQ type, foreign chain id with no shell state moved while the SDK's ratchet does, no anchor, ChainLock lag 288/289 and one ahead, height watermark H/H-2/H-3 accepted and H-4/H-5 rejected, stale signed time recording no version, PV15 signalled with the value and closing the builders, a proven absence under PV15 still ProvenAbsent), identity by id and by key hash, nonce masking and the proven-absent nonce of an unused contract, resolve/search/names-of-identity, search prefix guards and a definitive gRPC refusal read as Rejected while an outage stays Unavailable, 101-name pages with has_more against the query's limit and a cursor continuation for both paged name reads (a names_of_identity continuation is an empty page at PV13, pinned), a profile with a DashPay v2 payment address where the contract has one and the profile at the network floor, contact requests, contested tallies and absence, every broadcast Status kind, and the lifecycle. tests/builders.rs checks every builder byte for byte against dpp's in-process private-key constructions (identity create against try_from_identity_with_signer_and_private_key for instant and chain proofs, DPNS preorder/domain and profile create against put_to_platform's create path including its id derivation, the profile replace against its replacement path, the contact request against Sdk::create_contact_request and decryption with the same secret), every document's id recomputed as Drive's advanced-structure check does and its data validated against the system contract at that version, the profile replace carrying avatar and payment-address fields it does not edit, refusing another identity's profile and refusing at PV13 a payment address DashPay v1 lacks, the contested prefund, signer and bad-input refusals, public-key recovery for every compressed header, the no-reactor invariant, and test_data/state_transition_first_byte.json (Batch = 2, IdentityCreate = 3), checked against the built transitions and rewritten only under UPDATE_TEST_VECTORS=1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The README states the trust model (pushed quorum keys and ChainLock anchor, chain-id check, shell watermark, protocol-version signal, proven absence, refusal versus outage, panic containment), summarizes the bridge API by group, describes the SOCKS5 proxy passthrough (fixed Config.proxy, no direct fallback, per-connection isolation, proxy failures Unavailable without banning, the 15 s proxied connect budget), and fixes the threading and signing contract (blocking reads on the embedder's worker, builders on the calling thread, SignForKey over the signable preimage with the first-byte variant index, SignAssetLockSighash as the one digest path). CI runs the crate's tests with nextest, links tests/cxx_smoke.cc from the staged headers and archive, and asserts the embedder's trust closure: no trusted-context-provider, reqwest, openssl-sys or native-tls in the dependency tree and no seed-list or unproved entry points in the sources. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shell compared each verified response's signed chain_id with a configured Config.tenderdash_chain_id. That check adds nothing: chain_id is part of the message the Platform quorum signs, so a relabelled id fails verification, and the signing quorum must be one the embedder pushed from its own Core chain, so a response from another network does not verify at all. The stale-node case after a testnet reset is covered by the SDK's signed-time window and the 288-block ChainLock lag floor. This matches the reasoning that closed #4963. Removed from the bridge: Config.tenderdash_chain_id, Meta.chain_id and StatusKind::ChainIdMismatch; UnsupportedProtocolVersion and Internal are renumbered to 6 and 7. Client::tenderdash_chain_id() and the empty-id refusal in Client::new are gone. accept() now runs the height watermark, then records the verified protocol version, then raises the unsupported-version signal. The replay suite replaces the two foreign-chain-id cases with one that relabels the chain id after signing (Rejected, no shell state moved) and one that accepts a proof any pushed Platform quorum signed for another chain id. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- get_contact_requests: below protocol version 14 Drive continues the (field, $createdAt) index after the cursor's creation time and skips the requests sharing it; a continuation there is Unavailable instead of a silently short list (as names_of_identity already does). The replay suite pages across a shared $createdAt at PV13 and PV14 and shows the since_ms re-read that recovers every request. - get_contested_vote_state takes a start_after cursor and returns a Page, so contests above 100 contenders are reachable; the first page carries the tallies, an empty continuation is the end, not an absence. - build_dpns_domain takes the contender count and states the fund to join that contest (PV14 doubles it past 250 contenders), as put_to_platform does; contest_fund_to_join exposes the amount. - build_contact_request refuses a to_user_id that is not the recipient before signing and no longer needs endpoints (local_sdk). - build_profile refuses a replace at revision u64::MAX instead of wrapping. - dip15_account_reference_from_mac refuses a version above 15 or an account index above 28 bits instead of truncating. - cxx_smoke.cc value-initializes every bridge struct. C++ API changes: build_dpns_domain(.., salt, contenders, key, signer), get_contested_vote_state(label, start_after), VerifiedContested.page, contest_fund_to_join(contenders), and dip15_account_reference_from_mac now throws rust::Error on out-of-range input. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2bb7671 to
6109d6e
Compare
100556a to
fb52ec0
Compare
Issue being fixed or feature implemented
Dash Core's DashPay GUI (dash-qt, tracked in dashpay/dash#7512) needs Dash Platform from C++, on Core's own terms. Quorum keys come from its LLMQ store, evonode endpoints from its masternode list, and signatures from its wallet. No key and no trust decision leaves the node.
rs-sdk-ffiis shaped for mobile wallets that fetch quorum keys from a trusted HTTP service and keep private keys in-process, so it does not fit.Earlier revisions of this PR were stacked on #4632 and used document builders added to
dash-platform-queries. Following review, that stack is gone. This PR is a thincxxshell overdash-sdkwith no protocol logic of its own. The SDK builds the queries, owns the transport, retries and address banning, verifies the proofs and tracks the protocol version. Core supplies only what it alone knows. The PR does not depend on #4632.Stacked on #5160 (
feat(sdk): route DAPI connections through a SOCKS5 proxy, branchfeat/dapi-client-socks5-proxy), and this PR's base is that branch. Dash Core users route their traffic through-proxy(usually Tor), and DashPay must follow the same settings instead of refusing to run under a proxy. That PR adds SOCKS5 support tors-dapi-clientandSdkBuilder::with_proxy, and fixes a panic on IPv6 endpoints. This PR passes the embedder's proxy through. The diff here is only this PR's own commits. Once #5160 merges, this PR is retargeted tov5.0-dev. Core pins a commit of this branch, so it does not wait for either merge. Core currently pins02b1749cb6; the review fixes in100556a767change a few C++ signatures (see below), so the next re-pin needs matching Core changes.What was done?
New workspace member
packages/rs-platform-cxx(dash-platform-cxx). Its only Platform dependencies aredash-sdk(default features off;dpns-contract,dashpay-contract,core_key_wallet) andplatform-encryption.provider.rs). The embedder pushes the active Platform quorum keys, hashes in Core's byte order, and its best ChainLock height. A proof naming any LLMQ type other than the network's Platform type is refused before a key is looked up. No proved read is dispatched until a ChainLock height has been pushed. A proof whose core-chain-locked height trails that anchor by more than 288 blocks is refused as stale. There is no upper bound, since a node one ChainLock ahead is honest.ops.rs). After the SDK has verified the quorum signature and its signed-time window, the shell applies its own monotonic Platform-height watermark (tolerance 3 blocks) and then records the verified protocol version, so a stale response moves nothing the shell decides on. The SDK's own watermark is off. Both the watermark and the verified version survive SDK rebuilds.chain_idwith a configured value, for the reasons that closed feat(sdk): reject response metadata from an unexpected chain id #4963. The id is part of the message the quorum signs, so a relabelled id fails verification. The signing quorum must be a Platform quorum the embedder pushed from its own Core chain, so a response from another network does not verify. A stale node after a testnet reset is caught by the SDK's signed-time window and the 288-block ChainLock lag floor.Ok,ProvenAbsent,Rejected,Unavailable,UnsupportedProtocolVersion, …) plus the verified metadata. The reads cover identity by id and by key hash, identity-contract nonce, DPNS resolve, search and names-of-identity (paged), DashPay profile, contact requests, and contested vote state. Broadcasts are classified the same way, including consensus error codes.include/dash/platform/signer.h).SignForKey(key_id, signable)receives the full signable preimage, so the wallet hashes and checks what it signs and answers with a 65-byte compact recoverable signature.SignAssetLockSighashis the one digest path, and the public key is recovered from its signature. Private keys never cross the FFI. The signer must be callable from any thread, although builders only call it on the calling thread.try_from_identity_with_signers. DPNS preorder/domain and profile create/replace are assembled over dpp's batch transitions. Contact requests are minted bySdk::create_contact_requestwith the embedder's ECDH secret. Builders use the protocol version a verified read has shown, and refuse to run before one exists or after an unknown version has been seen.put_to_platformwould derive, from the entropy alone at PV13 and from the entropy and nonce from PV14. A property the network's contract version does not have yet, such as DashPay v2 payment addresses before PV14, is refused before signing.Configcarriesproxy: Proxy{kind, address, isolate}: kind 0 is none, 1 is a numeric TCPip:port, and 2 is a Unix socket path.isolatesends fresh random SOCKS5 credentials on every connection, so Tor builds a separate circuit for each, matching Core's-proxyrandomize.SdkBuilder::with_proxy, with a 15 s connect budget instead of 5 s.new_platform_clientfail, so there is never a direct fallback.set_endpointsaccepts only IP literals, or.onionnames when a proxy is set, so no endpoint is ever resolved locally and an onion name never reaches the system resolver.Unavailable, notRejected. The SDK neither retries it nor bans the evonode.catch_unwind, and the crate refusespanic = "abort".build.rsstagesdash/platform/{ffi.h,signer.h}andrust/cxx.hundertarget/<profile>/include/.tests/cxx_smoke.ccfrom the staged headers and archive. It also asserts the trust closure: nors-sdk-trusted-context-provider,reqwest,openssl-sysornative-tlsin the tree, and no seed-list or unproved entry points in the sources. The README documents the trust model, API and threading contract.How Has This Been Tested?
tests/replay.rs(70 cases). Proofs are generated from a real Drive state and signed by a test BLS quorum, then replayed throughdash-sdk's mock transport, so only the socket is mocked. The cases cover the freshness matrix (tampered proof or signature, unknown quorum, wrong LLMQ type, chain id relabelled after signing, a chain id signed by a pushed quorum accepted, no anchor, ChainLock lag 288/289, watermark H-3 accepted and H-4 rejected, unsupported protocol version), proven absence, paging, every read, and every broadcast status.tests/builders.rs(21 cases). Every builder is compared byte for byte with dpp's in-process private-key constructions and the SDK'sput_to_platform/create_contact_requestpaths. The suite recomputes each document id the way Drive does, validates the data against the system contract, and checks signer refusals and the no-reactor invariant.Unavailablewith the evonode not banned.100556a767(review fixes):cargo test -p dash-platform-cxx137 passed (40 unit, 23 builders, 74 replay);cargo clippy -p dash-platform-cxx -- -D warningsclean, with and without--all-targets;cargo fmt --all --checkclean;scripts/cxx-smoke.shlinks and runs on macOS. The fixes:get_contact_requestscontinuation below PV14 isUnavailable. PV13 Drive skips the requests that share the cursor's$createdAt; the replay suite reproduces it, 102 of 104, and shows thesince_msre-read that recovers them.get_contested_vote_state(label, start_after)pages contenders, andVerifiedContestedgainspage.build_dpns_domain(.., salt, contenders, key, signer)states the fund to join a contest of that size (PV14 doubles it past 250 contenders).contest_fund_to_join(contenders)exposes the amount.build_contact_requestrefuses ato_user_idother than the recipient and no longer needs endpoints.build_profilerefuses revisionu64::MAX.dip15_account_reference_from_macthrows on a version above 15 or an index above 28 bits.02b1749cb6(chain-id check removed):cargo test -p dash-platform-cxx128 passed (37 unit, 21 builders, 70 replay);cargo clippy -p dash-platform-cxx --all-targets -- -D warningsclean with and without--features mocks;cargo fmt --checkclean;scripts/cxx-smoke.shlinks and runs on macOS.cargo test -p dash-platform-cxx --features mocks: 128 passed (37 unit, 21 builders, 70 replay), on top of the proxy branch rebased ontov5.0-dev(thenv4.2-dev) at99bc968a5c. The rebase only needed the builders suite to pass the newDocumentSystemValuesargument tovalidate_document_properties. Also clean:cargo clippy -p dash-platform-cxx --all-targets -- -D warningswith and without--features mocks,cargo fmt --checkandcargo machete.scripts/cxx-smoke.shlinks and runs from C++ on macOS, including construction with a proxy and the refusal of a proxy host name.b40f44c12e. The final round on 2026-09-28 usedfd356b5c9f, this branch before the rebase onto the currentv4.2-dev. Its crate sources match this head apart from the one-line test fix. It also covered interop with the DashPay iOS app. Results:code=0. The name resolves on Platform to the new identity, which confirms the PV13 document-id fix. A later round registered two more names on encrypted wallets under a single passphrase prompt.Breaking Changes
None. This adds a new crate, and nothing existing changes apart from workspace membership, the Rust CI package filters and the workflow. (The base PR's
AppliedRequestSettings.proxyfield is described there.)Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
Consumer. The dash-qt stack tracked in dashpay/dash#7512 is the only consumer:
02b1749cb6;Core pins this crate by
dashpay/platformcommit. A Core-side bundle script (contrib/devtools/platform-bundle.sh) produces a reproducible, sha256-pinned vendored crate bundle for that commit, anddependsbuilds the static library from it. The bundle gains one crate,tokio-socks(MIT), from the base PR. No otherrs-sdkchange is required.🤖 Generated with Claude Code