refactor(sdk): shared wire-request decode and pure DPNS/DashPay document builders - #4389
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
|
⛔ Blockers found — Opus deferred (commit 5435934) |
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The shared decoders and extracted builders largely preserve the existing behavior, but the new request-driven document verifier does not bind verification to every semantically relevant request field. A malicious transport can therefore substitute a valid proof for a different query, so this trust-boundary issue must be fixed before merging.
Source: reviewer backend model gpt-5.6-sol; final verifier backend model gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).
Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 1 blocking
🤖 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/dash-platform-queries/src/documents/document_query.rs`:
- [BLOCKING] packages/dash-platform-queries/src/documents/document_query.rs:665-678: Reject request fields that are discarded before proof verification
Validating only the `select` projection does not ensure that the proof corresponds to the wire request. The subsequent `TryFrom<&DocumentQuery> for DriveDocumentQuery` conversion discards `group_by` and `having`, even though the server rejects both for `SELECT DOCUMENTS`; consequently, an untrusted transport can pair a request such as `SELECT DOCUMENTS GROUP BY age` with a valid proof for the corresponding plain document query and this function will accept it. The conversion also narrows `request.limit` with `as u16` at lines 1127-1129, so a wire limit of 65537 becomes 1 even though the server rejects limits above `u16::MAX`. Plain-document `offset` and a false `prove` flag are additional request shapes that cannot produce this proved response from the real server but are not rejected here. GroveDB and Tenderdash proofs authenticate the state and resolved Drive query, not the discarded request envelope. Before delegating, reject every field incompatible with a proved plain-document request (`group_by`, `having`, `offset`, and `prove == false`) and use a checked `u16::try_from` conversion for the limit so no request information is silently changed.
| // This entry point verifies plain document fetches only. An aggregate | ||
| // projection (COUNT/SUM/AVG) is proved with a different proof shape; | ||
| // handing it to the Documents verifier would surface as an opaque | ||
| // low-level proof error, so reject it up front instead. | ||
| if query.select != drive::query::SelectProjection::documents() { | ||
| return Err(drive_proof_verifier::Error::RequestError { | ||
| error: format!( | ||
| "verify_documents_response only verifies plain document fetches; the request \ | ||
| carries a {:?} projection — use the aggregate proof helpers instead", | ||
| query.select.function | ||
| ), | ||
| }); | ||
| } | ||
| <Documents as FromProof<DocumentQuery>>::maybe_from_proof_with_metadata( |
There was a problem hiding this comment.
🔴 Blocking: Reject request fields that are discarded before proof verification
Validating only the select projection does not ensure that the proof corresponds to the wire request. The subsequent TryFrom<&DocumentQuery> for DriveDocumentQuery conversion discards group_by and having, even though the server rejects both for SELECT DOCUMENTS; consequently, an untrusted transport can pair a request such as SELECT DOCUMENTS GROUP BY age with a valid proof for the corresponding plain document query and this function will accept it. The conversion also narrows request.limit with as u16 at lines 1127-1129, so a wire limit of 65537 becomes 1 even though the server rejects limits above u16::MAX. Plain-document offset and a false prove flag are additional request shapes that cannot produce this proved response from the real server but are not rejected here. GroveDB and Tenderdash proofs authenticate the state and resolved Drive query, not the discarded request envelope. Before delegating, reject every field incompatible with a proved plain-document request (group_by, having, offset, and prove == false) and use a checked u16::try_from conversion for the limit so no request information is silently changed.
source: ['codex']
There was a problem hiding this comment.
Verified against rs-drive-abci and fixed — this was a real hole.
Server rules confirmed in packages/rs-drive-abci/src/query/document_query/v1/mod.rs:
validate_and_route,SelectFunction::Documentsarm: non-emptygroup_by→InvalidArgument.validate_and_route, ahead of the per-function gates: non-emptyhavingwith a non-aggregate SELECT →not_yet_implemented("HAVING clause").reject_offset_off_the_ranked_path:offset.is_some()on any non-Rankeddecision →not_yet_implemented("OFFSET pagination …"). A documents fetch never routes toRanked.query_documents_typed(v0/mod.rs, shared by both wire versions):limit > u16::MAX→QuerySyntaxError::InvalidLimit.
So all four are shapes an honest server refuses, and all four are dropped by the DocumentQuery → DriveDocumentQuery lowering. prove == false isn't a server rejection but has the same effect: the server answers without a proof, so a proved response cannot belong to that request.
Changes:
- New
reject_request_the_server_would_not_have_proved(&query, prove)gate inverify_documents_response, run before delegating toFromProof. It rejectsprove == false, non-documents projections (the pre-existing check, moved in), non-emptyhaving, non-emptygroup_by, and anyoffset.proveis read off the wire request before decode, sinceDocumentQueryhas no such field. TryFrom<&DocumentQuery> for DriveDocumentQuerynow usesu16::try_fromfor the limit. You were right that this one is objectively wrong independent of the security argument — 65537 wrapped to 1. It now matches theoffsetconversion immediately below it, which already refused rather than truncated.- Five tests in
tests/document_query_wire_roundtrip.rs(verify_binds_the_whole_request::*) pluslimit_above_u16_max_is_rejected_not_truncated. The tests pass aContextProviderthat panics on every method, so they also pin that the rejection fires before any proof machinery runs.
try_from_request itself is unchanged — it still decodes these fields faithfully, which is what makes it a true inverse of encoding. Its "scope caveat" doc now points at the entry point that closes the gap.
No existing test expectations changed.
🤖 Posted autonomously by Claude on behalf of pasta.
There was a problem hiding this comment.
Resolved in this update — Reject request fields that are discarded before proof verification no longer present.
Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.
…-builders base Move all seven dashpay/platform git dependencies from the old feat/transport-free-embedder-core pin (e8e1961fe54f) to rev 2a6dbe39065104981b7f9bb4fbee598aab869fe4, the head of refactor/document-query-decode-builders (PR dashpay/platform#4389) whose content is the rebased equivalent on the current v4.2-dev base. No FFI-visible API drift: the crate builds unchanged and all 31 rust/platform tests pass against the new revision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fb66886 to
097eadb
Compare
2a6dbe3 to
4f1c1bd
Compare
097eadb to
84841a8
Compare
4f1c1bd to
be3375f
Compare
84841a8 to
9970e9f
Compare
be3375f to
5596a4c
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The shared proto conversions and extracted document builders generally preserve existing behavior and add useful deterministic coverage. However, the request-driven verifier still reduces the wire request to a narrower Drive query without validating every field that the server uses for routing, allowing a fabricated response to verify against a request the server would reject.
Source: reviewer backend model gpt-5.6-sol; final verifier backend model gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).
Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 1 blocking
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
🤖 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/dash-platform-queries/src/documents/document_query.rs`:
- [BLOCKING] packages/dash-platform-queries/src/documents/document_query.rs:681-694: Reject request fields that are discarded before proof verification
(existing thread: https://github.com/dashpay/platform/pull/4389#discussion_r3771773859)
Checking only that `select` is the documents projection does not bind verification to the complete wire request. The subsequent `TryFrom<&DocumentQuery> for DriveDocumentQuery` conversion discards `group_by` and `having`, although the server rejects both when used with `SELECT DOCUMENTS`. It also narrows a nonzero `u32` limit with `as u16`, so a request for 65537 documents is verified as a limit-1 query even though the server rejects limits above `u16::MAX`. A plain-document `offset` is lowered into a Drive query despite being rejected by the server's routing layer, and `prove == false` is ignored even though an honest server cannot return this proved response for such a request. Consequently, an untrusted transport can pair one of these server-invalid requests with a valid proof for the reduced Drive query and this function accepts it. Before delegating, reject nonempty `group_by` or `having`, any plain-document `offset`, and `prove == false`, and convert the limit with `u16::try_from` so no request information is silently changed.
…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>
9970e9f to
7087bd2
Compare
5596a4c to
5435934
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The prior request-envelope binding issue is fixed: discarded fields are now rejected before proof verification, and limit narrowing is checked. Two server-parity gaps remain in the new request-driven verifier: it accepts wire versions disabled by the supplied PlatformVersion and explicit document limits above the server's canonical cap, allowing proofs to authenticate request/response pairings that an honest server could not produce.
Source: reviewer backend model gpt-5.6-sol; final verifier backend model gpt-5.6-sol.
Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).
Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet: not run (deferred by blocker gate)
🔴 2 blocking
🤖 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/dash-platform-queries/src/documents/document_query.rs`:
- [BLOCKING] packages/dash-platform-queries/src/documents/document_query.rs:761-767: Reject document wire versions unavailable at the target platform version
The verifier dispatches on the request oneof without checking whether that wire version is enabled by `platform_version.drive_abci.query.document_query`. The server performs this check in `rs-drive-abci/src/query/document_query/mod.rs` before decoding or executing the request. For example, PlatformVersions 1–11 have bounds `min_version = 0, max_version = 0`, so their servers reject every V1 request with `UnsupportedQueryVersion`; this verifier instead decodes the V1 request and can verify a genuine V0 proof for the equivalent lowered `DriveDocumentQuery`. An untrusted transport can therefore attach a valid proof to a request that an honest server at the supplied PlatformVersion could not have answered. Derive the request feature version (`V0 = 0`, `V1 = 1`) and reject it unless the supplied version bounds accept it before decoding or delegating to `FromProof`.
- [BLOCKING] packages/dash-platform-queries/src/documents/document_query.rs:1234-1245: Enforce the server's document limit cap during verification
The checked `u16` conversion prevents wrapping, but it still accepts explicit limits from 101 through 65535. The server passes the converted value to `DriveDocumentQuery::from_typed_clauses`, which rejects any value above `DriveConfig::default().default_query_limit` (`DEFAULT_QUERY_LIMIT`, currently 100). The verifier bypasses that constructor and creates a raw `DriveDocumentQuery` with values such as `limit = Some(101)`. If the matching range contains fewer documents than either limit, a genuine proof for a server-valid query can also satisfy the larger path query, so verification accepts a fabricated request/response pairing that an honest server would reject. Validate explicit plain-document limits against the same canonical limit used by proof verification, rather than only checking whether they fit in `u16`.
| let prove = match &request.version { | ||
| Some(V0(v0)) => v0.prove, | ||
| Some(V1(v1)) => v1.prove, | ||
| // Missing version is reported by the decode below. | ||
| None => true, | ||
| }; | ||
| let query = DocumentQuery::try_from_request(request, contract).map_err(|e| { |
There was a problem hiding this comment.
🔴 Blocking: Reject document wire versions unavailable at the target platform version
The verifier dispatches on the request oneof without checking whether that wire version is enabled by platform_version.drive_abci.query.document_query. The server performs this check in rs-drive-abci/src/query/document_query/mod.rs before decoding or executing the request. For example, PlatformVersions 1–11 have bounds min_version = 0, max_version = 0, so their servers reject every V1 request with UnsupportedQueryVersion; this verifier instead decodes the V1 request and can verify a genuine V0 proof for the equivalent lowered DriveDocumentQuery. An untrusted transport can therefore attach a valid proof to a request that an honest server at the supplied PlatformVersion could not have answered. Derive the request feature version (V0 = 0, V1 = 1) and reject it unless the supplied version bounds accept it before decoding or delegating to FromProof.
source: ['codex']
| let limit = if request.limit != 0 { | ||
| Some(request.limit as u16) | ||
| Some(u16::try_from(request.limit).map_err(|_| { | ||
| Error::Config(format!( | ||
| "limit {} does not fit a documents query's u16 limit (max {}); \ | ||
| the server rejects such limits with InvalidLimit", | ||
| request.limit, | ||
| u16::MAX | ||
| )) | ||
| })?) | ||
| } else { | ||
| None | ||
| }; |
There was a problem hiding this comment.
🔴 Blocking: Enforce the server's document limit cap during verification
The checked u16 conversion prevents wrapping, but it still accepts explicit limits from 101 through 65535. The server passes the converted value to DriveDocumentQuery::from_typed_clauses, which rejects any value above DriveConfig::default().default_query_limit (DEFAULT_QUERY_LIMIT, currently 100). The verifier bypasses that constructor and creates a raw DriveDocumentQuery with values such as limit = Some(101). If the matching range contains fewer documents than either limit, a genuine proof for a server-valid query can also satisfy the larger path query, so verification accepts a fabricated request/response pairing that an honest server would reject. Validate explicit plain-document limits against the same canonical limit used by proof verification, rather than only checking whether they fit in u16.
source: ['codex']
Issue being fixed or feature implemented
Third slice of the
feat/transport-free-embedder-coreseries (#4335; after #4344, #4345, and #4388): gives transport-free embedders the remaining pieces they need to construct and verify Platform interactions without reimplementing SDK logic — the drift-prone code Dash Core's Platform GUI (PastaPastaPasta/dash#67, dashpay/dash#7512) currently hand-builds in C++.Stacked on #4388 (base branch
refactor/dash-platform-queries); will be retargeted tov4.2-devwhen that merges. Only the last four commits are new.What was done?
DocumentQuery::try_from_requestdecodes a wire-formatGetDocumentsRequestback into a richDocumentQuery— the inverse of request encoding — by liftingdrive-abci's server-side proto conversions into shared client code, so the bytes the server decodes and the client verifies go through the same code.drive-abcinow consumes the shared conversions (deduplicated, −382 lines in itsconversions.rs). Round-trip coverage intests/document_query_wire_roundtrip.rs.documents::verify_documents_responsedelegating todrive-proof-verifier'sFromProof, keyed on the exact request bytes sent. It binds the proof to the whole request, not just the part that survives lowering: GroveDB/Tenderdash proofs authenticate the state and the resolvedDriveDocumentQuery, and theDocumentQuery→DriveDocumentQuerylowering dropsgroup_by,having,offsetandprove— so each of those is rejected up front, mirroringrs-drive-abci'svalidate_and_routeandreject_offset_off_the_ranked_path. Without that, an untrusted transport could 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. The limit conversion is likewise checked (u16::try_from) rather than anascast, which silently turned a request for 65537 documents into a 1-document query.build_dpns_preorder_and_domain_documents(salted-domain-hash preorder/domain pair) anddashpay::build_contact_request_document, extracted from rs-sdk's networked flows. Crypto material is supplied by the caller — ECDH/key custody stays out of this crate.is_consensus_valid_label(matches the DPNS contract regex; gates the builders) fromis_valid_username(stricter client-side policy, e.g. consecutive-hyphen rejection) so builders cannot reject labels the contract accepts.rs-sdkre-exports everything at its old paths; no consumer changes imports.How Has This Been Tested?
cargo test -p dash-platform-queries(41 unit + 15 integration tests, including wire round-trip, request-binding rejections, and builder/validation coverage);cargo checkfordash-sdkanddrive-abci;cargo fmt --check; clippy clean.Breaking Changes
None beyond those already declared by the base PR #4388. Moved items remain importable at their previous
dash_sdkpaths;drive-abci's request decoding behavior is unchanged (same conversions, now shared).One behavior change worth calling out:
TryFrom<&DocumentQuery> for DriveDocumentQuerynow returns an error for a limit aboveu16::MAXinstead of wrapping it. The server rejects such limits outright (QuerySyntaxError::InvalidLimit), so the previousascast could only ever produce a query the caller did not ask for. No existing test depended on the truncation.