Repository navigation
perf(platform)!: delete a consumed document from what its lookup read (PV15) - #5360
QuantumExplorer wants to merge 36 commits into
Conversation
…a keywords From protocol version 14 the DPNS `domain` create is checked by the contract's own keywords instead of most of its data trigger: - `normalizedLabel` and `normalizedParentDomainName` are generated from `label` and `parentDomainName` (`generatedFrom` homographSafeASCII); - `preorderSalt` reveals the writer's own preorder, made in an earlier block, and deletes it (`refersTo` with a sha256d `findBy`, `where` `$ownerId`, `minimumAgeBlocks: 1`, `consume`); the preorder's storage is refunded to the writer; - the `recordsIdentityIsOwner` rule holds a new name's `records.identity` to its owner until the name first changes hands; - `domain` sets `canBeDeleted: false`; `preorder` requires `$createdAtBlockHeight`. Positions, transient list and encoding are v2's. `create_domain_data_trigger` 2 keeps only the parent domain checks, and data trigger bindings 2 drop the `domain` Replace and Delete rejects. `transition_to_version_14` re-stores DPNS. A preorder recording no `$createdAtBlockHeight` (made before the upgrade) counts as old enough for `minimumAgeBlocks`. The legacy JS SDK hashes the preorder as the platform does: normalized label, ".", parent as sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A document recording no `$createdAtBlockHeight` is known only to come from a block before the protocol upgrade that re-stored its system contract, so it now meets `minimumAgeBlocks: 1` (the DPNS reveal's) and no larger minimum. Another contract revealing DPNS preorders with a larger minimum refuses a pre-upgrade preorder; a test covers it. Also: a trigger-refused DPNS reveal keeps its preorder for a retry (test), and the check_tx contract update fee pin follows the DPNS v3 preorder type the test copies in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riscoll-44c981 # Conflicts: # packages/rs-platform-version/src/version/v14.rs
`User.DASH` hashes `user.DASH`, the parent as sent, while the domain carries `parentDomainName: "DASH"` and `normalizedParentDomainName: "dash"`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riscoll-44c981 # Conflicts: # packages/rs-platform-version/src/version/v14.rs
Short `description`s and no `$comment`s in DPNS v3; the rules they restated are in the schema keywords. The check_tx contract update fee pin follows the smaller preorder type it copies in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…owner A copy of a pending preorder no longer blocks it, and the copier can no longer reveal the name with the copy once the owner's reveal shows the salt: - `preorder` is unique on (`$ownerId`, `saltedDomainHash`), replacing the unique index on the hash alone; - the domain's `preorderSalt` finds it by `$ownerId` and the sha256d of the writer's id, the salt, the normalized label, "." and the parent. The parser (meta-schema v3, parser generation 3) admits `"$ownerId"` as a findBy function param (`LookupKeyParam::Writer`), a `$ownerId` findBy key on a transferable type beside a function, and that key for `consume`. At protocol version 14 every existing preorder is deleted and the type is rebuilt as a fresh v3 chain lays it out (`Drive::apply_contract_rebuilding_document_types`), so the minimumAgeBlocks allowance for preorders recording no creation height is reverted. rs-sdk and js-dash-sdk compute the preorder hash from the contract's own declaration, so either side of the upgrade hashes as the platform does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riscoll-44c981 # Conflicts: # packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs # packages/rs-platform-version/src/version/v14.rs
A registration took the preorder's hash form from the DPNS contract the client already held: the context provider's for the SDK's protocol version (13 on a fresh SDK), or js-dash-sdk's app cache. Either could still be DPNS v2 after the network moved to v3, so the preorder committed to the old hash and the domain create could never find it (40120), after the preorder was paid. rs-sdk now refreshes the protocol version (proven) and fetches DPNS from state before hashing, fetching again when the response raised the version the contract was parsed under. A preorderSalt reference without a findBy function is an error instead of a silent fall back to the old hash. js-dash-sdk fetches the contract with a new `skipCache` option, which also replaces the cached contract both documents are created against. Tests: the SDK hash against the real DPNS v2 and v3 contracts, a missing hashed property, and a mock SDK seeded at 13 with a provider holding v2; js specs for a cache warmed with v2 and for `skipCache`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arsed version Comparing the SDK's protocol version before and after the fetch missed a node behind the network: after the refresh learned protocol 14, a node at the block before activation could still prove DPNS v2 at 13, the ratchet ignores the lower version, and the registration would hash the old way. The registration now reads each DPNS contract response with its metadata and takes the first one proved at the version the SDK parsed it under, at most 3 times. A response proved at a newer version was parsed under an older one (decoding DPNS v3 under 13 drops the salt's reference) and has raised the SDK's version, so the next is parsed under it; one proved at an older version is skipped. Otherwise the registration fails before paying. The document building moves into `dpns_registration_documents`, so an offline test drives it against a context provider holding v2 and checks the preorder's owner-bound hash and the domain's salt. Scripted tests cover a response parsed under an older version (rebuilt from serialized bytes as a proof decode does), a node behind the network, and every node behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`apply_contract_rebuilding_document_types` stored the new contract through `update_contract_add_operations`, which only builds operations, so unlike an update it never put the rewritten contract in the block cache. Until the protocol change events refresh every system contract at their end, a read in the block could fall back to a global cache entry a committed-state read had filled with the old contract. The rebuild now fetches what it stored and seeds it with `insert_rewritten`, as `update_contract` does. Test: DPNS v2 warm in the global cache, the preorder type rebuilt, and a read in the block's transaction gets v3. The book's same-block transfer case says the rule still applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anwhile The version a registration compared a DPNS contract response with was read before the fetch awaited the network. SDK clones share that version, so another request could raise it from 13 to 14 while the fetch waited; a response from a node still at 13 proving DPNS v2 then matched the stale 13 and was taken, and the preorder hashed the old way. A response is now taken only when the SDK's version is unchanged across the fetch as well as equal to the version the node proved it at. The version only rises, so one unchanged across the fetch is the version the response was parsed under. Regression: the first fetch raises the shared version to the latest while returning DPNS v2 proved at 13, which is skipped, and the next response, v3, is taken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riscoll-44c981 # Conflicts: # packages/rs-platform-version/src/version/v14.rs
Now that `identifier` and `bytes` + `size` are in v5.0-dev (#5355), the DPNS v3 schema uses them: `preorderSalt` and `saltedDomainHash` are `"type": "bytes", "size": 32`, `records.identity` is `"type": "identifier"`. The parsed types are unchanged; the stored contract keeps the schema as written, so the check_tx contract-update fee pin moves (27003089990 to 27003077990). Protocol 13 cannot decode the shorthands, so the SDK test of a response parsed under an older version now uses DPNS v2 as the contract read under 13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clients: - js-dash-sdk refuses a records.identity other than the registering identity before paying for the preorder when the contract declares recordsIdentityIsOwner; the legacy (pre-v3) hash of a top-level name is of the salt and the label as sent, as trigger v1 hashes it; the "no DPNS app" spec now reaches the code it names. - rs-sdk `salted_domain_hash` reads the salt and label from the domain's properties instead of separate parameters. Drive and drive-abci: - the preorder rebuild refuses a type with trees outside its own subtree (indexOnly, contested index, moderator deletions, ttl), deletes without the backward-reference scan Drive never needs, and drops the always-None storage flags parameter. - trigger v2 queries the parent under the top level directly (a parent domain name holds no dot) and gains in-module tests. Parser and docs: - the consume check moves into `writer_owns_found_document`, so the parse_reference_target match is no longer reflowed. - `LOOKUP_KEY_WRITER` gives way to the existing `OWNER_ID`; the unused `preorder::properties::SALTED_DOMAIN_HASH` and `hash_double` import go. - meta-schema v3, reference_lookup.rs, wasm-dpp2 and the book describe the findBy `$ownerId` entry (40120 for another identity's commitment), the per-owner preorder index and the legacy top-level hash. Tests: - the strategy tests hash preorders with the shared `dpns_salted_domain_hash` (now outside cfg(test)) instead of the rule under test, and lose the stale "disable document triggers" comments; dead `dpns < 3` arms, the pasted height-1 comments and the derivable flag go; the protocol 13 test is renamed `should_...`; preorders are read by id; the award test uses `fast_forward_to_block`; the upgrade layout reference is a bare genesis state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`DocumentPropertyReferenceTarget::lookup_hash_key` returns the computed key of the `findBy` function a target declares, the chain `as_any_document_reference` -> `lookup` -> `hash_key` that rs-sdk's DPNS preorder hash and document reference validation 0 each spelled out. Reference validation 0 is a shipped generation edited in place: the accessor returns exactly what the inline chain returned, so nothing changes at any protocol version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A create that consumes a commitment (a `refersTo` lookup with a computed key declaring `consume`, protocol version 14) read the commitment twice: the lookup in reference validation found it, and the delete read it again by id to know what to remove and refund. The second read was billed. The lookup now keeps the stored element's storage flags beside the document (`query_documents_with_flags`, the same path query and cost), `ConsumedDocument` carries both, and the delete runs from them through `Drive::force_delete_read_document_for_contract_operations`: the operations of `force_delete_document_for_contract_operations` less its read, dispatched on the same method version. A contested create's consume uses the new `ForceDeleteReadDocument` operation, the plain create's `AddDocumentAndDeleteConsumed`. A DPNS v3 domain create's processing fee drops from 2,525,540 to 2,514,960 credits; storage fee and the refund to the writer are unchanged. A Drive test checks the read-document delete's operations equal the force delete's after its read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
|
✅ Final review complete — no blockers (commit f338eb1) · triage: critical |
…ties The register spec mocks `crypto.randomBytes` to zeros for the whole suite, so two `generateRandomIdentifier()` calls gave the same identity and the refusal never fired. The spec now uses two fixed, different ids. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s stay deletable documents.md stated without exception that a findBy `"$ownerId"` source needs a referring type that cannot be transferred or traded, for a property's findBy and for creatorRefersTo; beside a function the lookup is judged on the create alone and the source is allowed, as DPNS v3 declares. The deletion chapter's onlyWhenConsumed preorder example now says DPNS v3's own preorder keeps `canBeDeleted: true`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three Drive tests used the latest DPNS fixture for writes or proofs at
protocol version 13. DPNS v3 is now written with the `bytes` and
`identifier` type shorthands, which the protocol 13 parser refuses
("unsupported property type: bytes"). A chain at 13 stores DPNS v2, which
every later version reads too, so these tests now load it: the two
contested-poll tests (`..._poll_started_before_protocol_version_14`) and
`verify_batch_document_proof_before_protocol_version_14_carries_no_balance`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Verified the supplied findings against head cec6d0a and the PR's base-to-head diff. No blocking defects were confirmed; the overlapping cloning findings combine into one performance suggestion, and the new deletion guards lack direct negative-path coverage. This was static verification only: no builds or tests were run, and the supplied CI snapshot shows Kotlin checks queued without completed Rust validation.
🟡 2 suggestion(s)
1 finding(s) not shown inline (the lines are not part of this PR's diff)
🟡 Suggestion: Move consumed document payloads instead of deep-cloning them
packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs:58
ConsumedDocument now owns a full Document and its storage flags, so this to_vec() recursively clones each document's property map and values. The reference-validation block also clones the owned lookup result into ConsumedDocument before dropping the original. Both copies are avoidable: this converter owns mut self, and the subsequent owned document conversion discards the original consumed_documents field. The PR therefore introduces copying proportional to the consumed document bodies on every consuming create, including contracts with larger commitments than DPNS preorders. After completing the borrowed reference checks, move the lookup tuple into ConsumedDocument; add a take_consumed_documents accessor using std::mem::take and use it here, matching the existing take_* accessors.
source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: general, architecture-layering, platform-versioning, rust-quality, security-auditor)
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: platform-versioning); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 6: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The cross-layer change to lookup state, create conversion, and batch deletion is intricate and directly changes consensus-visible fee accounting and storage-refund inputs through force_delete_read_document_for_contract_operations_v0 and DocumentOperationType conversion. - 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(lane failed),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 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— 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-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs`:
- [SUGGESTION] packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs:58: Move consumed document payloads instead of deep-cloning them
ConsumedDocument now owns a full Document and its storage flags, so this to_vec() recursively clones each document's property map and values. The reference-validation block also clones the owned lookup result into ConsumedDocument before dropping the original. Both copies are avoidable: this converter owns mut self, and the subsequent owned document conversion discards the original consumed_documents field. The PR therefore introduces copying proportional to the consumed document bodies on every consuming create, including contracts with larger commitments than DPNS preorders. After completing the borrowed reference checks, move the lookup tuple into ConsumedDocument; add a take_consumed_documents accessor using std::mem::take and use it here, matching the existing take_* accessors.
In `packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs`:
- [SUGGESTION] packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs:222-236: Add negative-path tests for the new read-document deletion guards
The new helper independently duplicates the indexOnly and history-keeping guards from the existing force-delete path. The added operation-equivalence test uses a normal document type, so it does not verify either rejection; the existing history-deletion test exercises the old deletion path rather than this helper. Contract registration currently excludes these types from consumption, so this is a defensive API coverage gap, not a demonstrated execution defect. Add direct assert_matches! cases for the new entry point with an indexOnly type and a history-keeping type, asserting CorruptedCodeExecution and InvalidDeletionOfDocumentThatKeepsHistory respectively, to pin the guard parity promised by this PR.
| if document_type.index_only() { | ||
| return Err(Error::Drive(DriveError::CorruptedCodeExecution( | ||
| "indexOnly documents cannot be deleted by id: there is no primary-storage \ | ||
| row; use delete_index_only_document_for_contract_operations with the \ | ||
| document's values", | ||
| ))); | ||
| } | ||
|
|
||
| if document_type.documents_keep_history() { | ||
| return Err(Error::Drive( | ||
| DriveError::InvalidDeletionOfDocumentThatKeepsHistory( | ||
| "this document type keeps history and therefore can not be deleted", | ||
| ), | ||
| )); | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: Add negative-path tests for the new read-document deletion guards
The new helper independently duplicates the indexOnly and history-keeping guards from the existing force-delete path. The added operation-equivalence test uses a normal document type, so it does not verify either rejection; the existing history-deletion test exercises the old deletion path rather than this helper. Contract registration currently excludes these types from consumption, so this is a defensive API coverage gap, not a demonstrated execution defect. Add direct assert_matches! cases for the new entry point with an indexOnly type and a history-keeping type, asserting CorruptedCodeExecution and InvalidDeletionOfDocumentThatKeepsHistory respectively, to pin the guard parity promised by this PR.
source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality)
…ion the response reports
A fresh SDK starts at its network's floor (protocol version 13 on
mainnet, testnet and regtest) and learns the network's version only
from a verified response. DPNS v3 is written with the `bytes` and
`identifier` type shorthands, which the protocol version 13 parser
refuses, so on a network at 14 the first proof holding DPNS failed
("unsupported property type: bytes") before the SDK could learn 14,
every time.
When parsing under the SDK's version fails on an unpinned SDK below the
newest version it knows, `parse_proof_at_the_reported_version` parses
under that newest version, which verifies the proof and its quorum
signature over a state id that signs the response's protocol version,
and, when the reported version is another known one, parses again under
it. A pinned SDK, or one at the newest version, reports the first
failure. The fetch traits require cloneable responses (all are) for the
retry.
Tests: a stand-in proof readable only from protocol version 14 parses on
an SDK seeded at 13 and fails on one pinned at 13.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts with #5313, which verifies a response again after fetching a missing quorum key, in the same clone-and-retry spot as the protocol version fallback: - fetch.rs, fetch_many.rs: both sides require a cloneable response; the doc names both retries. - sdk.rs: the Dapi arm takes #5313's `verify_fetching_quorum_key`, whose two verifications now go through `parse_proof_at_the_reported_version` (the version passed in, so both start from the same one). A failure of the quorum signature or its key is reported as it is under either version (`fails_after_reading_the_proof`): the signature is checked once the proof was read, so a missing key still reaches the fetch and no extra quorum lookup is made. Test: `should_report_a_missing_quorum_key_found_under_the_newest_version`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… verification `parse_proof_at_the_reported_version` borrows the request and response and copies them once per parse, so `verify_fetching_quorum_key` keeps one copy for both of its verifications instead of copying it again before the first. The probe is now scripted by its request (the first version that decodes it, the version its response reports, the versions whose signature or key fails) and records every version it is parsed under. New tests: - a reported version between the SDK's and the newest is parsed again under it, and its failure there is reported; - a signature failure under the newest version is reported over the first decoding error, and one under the SDK's version is not retried; - through `verify_fetching_quorum_key`, a fresh SDK reads the proof and learns the reported version, and learns none when the signature fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h the version fallback The probe now asks the provider for its quorum's key instead of failing outright, so a key the provider fetches makes the second verification succeed. A new test drives `verify_fetching_quorum_key` with an SDK two versions back and a provider holding no key: one fetch, the parse sequence current, newest, current, newest, reported, and the SDK learns the reported version. A regression of the second verification to a direct parse at the SDK's version would fail it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riscoll-44c981 # Conflicts: # book/src/contract-keywords/refers-to-lookup.md # book/src/data-model/documents.md # packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json # packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs # packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs
…riscoll-44c981 # Conflicts: # book/src/contract-keywords/refers-to-lookup.md # book/src/data-model/documents.md # packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json # packages/rs-dpp/src/data_contract/document_type/property/reference_lookup.rs
…ut a salt A DPNS v3 preorder now holds `ownerNameHash`, the sha256d of the owner's id, the normalized label, "." and the normalized parent, unique per owner (`ownerAndNameHash`), and `nameHash`, the same without the owner, indexed with its creation height (`nameHashAndHeight`). It carries no salt and lives an hour (`ttl` 3600, so it refunds nothing when consumed). The domain finds it through `creatorRefersTo` (findBy `$ownerId` and the `ownerNameHash` function, minimumAgeBlocks 1, consume). `preorderSalt` stays declared on the domain, transient and no longer required, so domains stored under v1 and v2 decode. `nameHash` is there for a first-committer refersTo keyword built in a separate PR: with it, a name whose older valid preorder belongs to someone else is refused, which stops a watcher who reads a pending name from the mempool from preordering and revealing it first under full blocks. No salted design can check that before the owner reveals. Without a salt an owner holds one pending preorder of a name: a second is refused as a duplicate (40105) while the first stands. `Sdk::register_dpns_name` reveals a pending one instead of preordering again. - Drive: `apply_contract_rebuilding_document_types` leaves a type already stored as declared (so the v14 upgrade stays idempotent), and rebuilds into a type with a ttl, which holds no document and so no expiry entry; a stored type with a ttl is still refused. - rs-sdk and js-dash-sdk build the preorder from the domain's creator reference (`nameHash` from its params without `$ownerId`) and send no salt from v3; v1 and v2 keep the salted hash. - Tests: dpp DPNS v3 shape and hash; drive rebuild (ttl new type, type left as stored, stored ttl refused); drive-abci dpns_v3 (one pending preorder per name, the contested retry reveals the same preorder, preorder and domain fees pinned: 62,554,803 credits a name at 14 against 76,862,280 at 13); the check_tx update pin gains the 0.01 Dash non-unique index; rs-sdk and js-dash-sdk preorder shapes. - Docs: v14 note 90, the book (commit and reveal, documents, deletion, transient). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st, and reuses a pending preorder - rs-sdk registers the DPNS contract it fetched and built the documents against with its context provider, so the preorder and domain confirmations decode under it. A known-contracts cache warmed with DPNS v2 would otherwise decode the v3 preorder's proof under v2's single property and refuse it, after the preorder was paid for. - The v3 preorder property names are spelled in the module rather than imported from the bundled DPNS contract, which the SDK builds only with that contract's feature (`cargo check -p dash-sdk --no-default-features` builds). - The pending preorder lookup is a shared query builder (`pending_preorder_query`). - js-dash-sdk looks for a pending v3 preorder by its owner and `ownerNameHash` before preordering, reveals it instead, and propagates a failed query; v1 and v2 keep the fresh salt. Tests: rs-sdk (the exact predicates, a pending preorder returned, none in a proven absence, a failed query propagated, no query for a salted preorder, the registration leaving a writable v2-seeded provider holding v3); js-dash-sdk (the query, the reuse, a failed query, no query under v2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its cache `WasmTrustedContext` forwarded `get_data_contract` to its inner `TrustedHttpContextProvider` but not `register_data_contract`, so the trait's no-op ran. A DPNS registration that registers the v3 contract it built against left a cache seeded with DPNS v2 (by `addKnownContract`, a contract fetch or a restored store) in place, and the preorder's confirmation decoded under v2 and failed after the preorder was paid for. The call is now forwarded. Test: a context seeded with DPNS v2 resolves DPNS to v3 once v3 is registered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - drive contract migration modules: #5350's `rekey_unsigned_integer_index_values` beside this branch's `apply_contract_rebuilding_document_types`. - v14 notes: #5350 took note 90 (unsigned integers keyed in value order), so the DPNS v3 note moves to 92, after the shorthands note 91; the upgrade's comment cites it as 92. #5369 gave the document reference targets `where_conditions`: the owner-keyed reveal test's expected `DeletableDocumentLookup` sets it empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#5378 wrote the book's examples with the type shorthands, among them the salted DPNS preorder and its reveal in refers-to-lookup, documents and deletion. This branch already writes them with the shorthands, for the unsalted DPNS v3 (`ownerNameHash`, `nameHash`, `creatorRefersTo`), so its versions are kept; documents.md no longer points at "the salt above". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retargets DPNS v3 to protocol version 15. The merge takes the base side of: - the SDK proof version fallback (sdk.rs, quorum_key.rs, fetch.rs, fetch_many.rs): #5361's verifier_version supersedes it; - the dpp parser and meta-schema: #5371 already reads the "$ownerId" param, and DPNS v3 now declares the where entry consume asks for, so the parser needs no relaxation; - the v14 note and the book's refersTo pages, rewritten for PV15 next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irst valid preorder DPNS v3 now activates at protocol version 15: - SYSTEM_DATA_CONTRACT_VERSIONS_V4 selects dpns 3 and DRIVE_ABCI_VALIDATION_VERSIONS_V11 selects create_domain_data_trigger 2, both only in PLATFORM_V15; the v14 tables are v5.0-dev's again. - The preorder rebuild runs in a new transition_to_version_15. - Data trigger bindings v2 are no longer edited: their domain Replace and Delete rejects are never reached once the schema refuses both. The domain's creatorRefersTo adopts chooseFirst (#5371): findBy the creator and nameHash, where $ownerId for consume, and a reveal is refused (40149) while another identity holds an older valid preorder of the name, one holding the hash of its own owner and the name at ownerNameHash. The preorder's unique index is now ($ownerId, nameHash). rs-sdk and js-dash-sdk derive nameHash from the findBy function and ownerNameHash from chooseFirst.validWhen, and look a pending preorder up by ($ownerId, nameHash). Tests move from 13/14 to 14/15, gain the front-running regression, and re-pin the name fees (PV14 76,851,000, PV15 62,560,093 credits) and the check_tx contract update fee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s versions A chain at protocol version 12 moving straight to 15 runs the upgrade rungs for 13, 14 and 15 in one block, each with version 15's tables. The rung for 13 loaded DPNS by that table, so it stored v3 with a plain update, which kept the old preorders and their index, and the rung for 15 then skipped its rebuild of a type already stored as declared. The rung for 13 now loads DPNS as protocol version 14 does (v2) when the target's table holds a later one. A target of 13 or 14 still loads it by its own table, so no ladder any chain can have run changes. Adds the 12 to 15 ladder regression: a stored legacy preorder is removed and the preorder layout equals a chain born at 15. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes its fee pins and its v14 notes: the single-read consume moves to protocol version 15 next, so the v14 notes stay as v5.0-dev ships them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om protocol version 15 The single-read consume becomes a protocol version 15 change, so protocol version 14 keeps the fees v5.0-dev ships: - DriveDocumentDeleteMethodVersions gains delete_consumed_document_operations, 0 in every existing table. DRIVE_DOCUMENT_METHOD_VERSIONS_V5 sets it to 1 and DRIVE_VERSION_V10 (PLATFORM_V15) selects that table. - At 0, fetch_document_through_lookup runs the plain query_documents it always ran, and force_delete_read_document_for_contract_operations deletes the consumed document by id, reading it again. At 1 the lookup keeps the storage flags (query_documents_with_flags) and the delete runs from them. The two queries differ in more than the flags (query_item_value errors on a missing intermediate tree, query does not), so the lookup itself is gated too, not only the delete. Fees at 15: a DPNS v3 name create 3,404,300 -> 3,393,640 processing, the chooseFirst reveal 3,574,720 -> 3,562,700; the latter is pinned at 14 too, unchanged. A Drive test checks the delete at 14 is the by-id force delete, and at 15 that delete less its read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
At the exact reviewed head, the version tables confine the lookup-and-delete optimization to protocol version 15 while preserving the historical query and by-id deletion paths; no blocking defect was confirmed. Both prior suggestions remain valid, and the new estimation branch also lacks direct equivalence coverage. Verification was static only: the supplied CI snapshot lists successful checks but contains no targeted Rust test or clippy results.
🟡 3 suggestion(s)
1 finding(s) not shown inline (the lines are not part of this PR's diff)
🟡 Suggestion: Move consumed document payloads instead of deep-cloning them
packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs:58
This PR expands ConsumedDocument from an identifier and type name to an owned Document and storage flags, making this existing to_vec() deep-clone every consumed document's property tree. The converter already owns mutable self, and the subsequent owned document conversion discards the original consumed-document vector, so the copy adds allocations and CPU work proportional to the consumed payload size without preserving anything needed later. Move the vector out with mem::take, directly or through a take-style accessor; the generated operations and protocol-version-specific fees remain unchanged.
let consumed_documents = match &mut self {
DocumentCreateTransitionAction::V0(action) => {
std::mem::take(&mut action.consumed_documents)
}
};
source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: general, rust-quality)
1 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: platform-versioning); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 6: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 12: 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) — The intricate cross-layer change in fetch_document_through_lookup and Drive::force_delete_read_document_for_contract_operations changes consensus-visible processing fees and deletion/refund operations, with protocol-version dispatch preserving historical behavior. - 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 14% 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— 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— 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-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs`:
- [SUGGESTION] packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs:58: Move consumed document payloads instead of deep-cloning them
This PR expands `ConsumedDocument` from an identifier and type name to an owned `Document` and storage flags, making this existing `to_vec()` deep-clone every consumed document's property tree. The converter already owns mutable `self`, and the subsequent owned document conversion discards the original consumed-document vector, so the copy adds allocations and CPU work proportional to the consumed payload size without preserving anything needed later. Move the vector out with `mem::take`, directly or through a take-style accessor; the generated operations and protocol-version-specific fees remain unchanged.
In `packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs`:
- [SUGGESTION] packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs:239-250: Cover the read-document deletion's estimation branch
Both new operation-equivalence tests pass `&mut None`, so neither exercises this estimation branch. Unlike stateful deletion, it ignores the supplied document and flags, adds estimated layer information, and passes `DocumentEstimatedAverageSize` downstream. Extend the equivalence fixture to run with separate `Some(HashMap::new())` estimation maps for the by-id and read-document entry points. At protocol version 14, assert identical estimated operations and layer information; at the latest version, assert equivalent downstream operations and layer information with only the redundant read-cost operation removed. This directly tests the estimation contract changed by the optimization.
- [SUGGESTION] packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs:222-236: Add negative-path tests for the new read-document deletion guards
(existing thread: https://github.com/dashpay/platform/pull/5360#discussion_r4231399014)
The new read-document implementation independently rejects indexOnly and history-keeping types, but both added equivalence tests use an ordinary `person` document. Existing by-id deletion tests would still pass if either guard disappeared from this implementation. Contract validation excludes these consumption targets, so this is a defensive coverage gap rather than a demonstrated production defect. Add focused tests through `force_delete_read_document_for_contract_operations` at `PlatformVersion::latest()`, asserting `CorruptedCodeExecution` and `InvalidDeletionOfDocumentThatKeepsHistory`. Exercise both stateful and estimation modes to pin these refusals before either branch runs.
| let document_info = if let Some(estimated_costs_only_with_layer_info) = | ||
| estimated_costs_only_with_layer_info | ||
| { | ||
| Self::add_estimation_costs_for_levels_up_to_contract_document_type_excluded( | ||
| contract, | ||
| estimated_costs_only_with_layer_info, | ||
| &platform_version.drive, | ||
| )?; | ||
| DocumentEstimatedAverageSize(document_type.estimated_size(platform_version)? as u32) | ||
| } else { | ||
| DocumentOwnedInfo((document, storage_flags.map(Cow::Owned))) | ||
| }; |
There was a problem hiding this comment.
🟡 Suggestion: Cover the read-document deletion's estimation branch
Both new operation-equivalence tests pass &mut None, so neither exercises this estimation branch. Unlike stateful deletion, it ignores the supplied document and flags, adds estimated layer information, and passes DocumentEstimatedAverageSize downstream. Extend the equivalence fixture to run with separate Some(HashMap::new()) estimation maps for the by-id and read-document entry points. At protocol version 14, assert identical estimated operations and layer information; at the latest version, assert equivalent downstream operations and layer information with only the redundant read-cost operation removed. This directly tests the estimation contract changed by the optimization.
source: gpt-6.1-sol (phase2-reviewer: rust-quality)
|
Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post |
|
Bots are done — your move: post |
Basic explanation
What this does: When a document "consumes" a commitment (as registering a DPNS name deletes the name's preorder), the platform was reading the commitment from storage twice: once to check it, and once more to delete it, and it billed both reads. From protocol version 15 the delete reuses what the first read found.
Value: A create that consumes a commitment costs one document read less in processing fees: a DPNS v3 name create 10,660 credits less, a
chooseFirstreveal 12,020 less. The same document is deleted and the same storage refund is paid. It applies to any contract whoserefersTodeclaresconsume, not only DPNS.Risks: Low. It is a consensus change at protocol version 15 only: protocol version 14 keeps both reads and the fees
v5.0-devships, which a test pins. The cheaper path deletes from the document and storage flags the lookup read, so a mismatch there would refund the wrong owner or remove the wrong entries; a Drive test checks its operations equal the force delete's after that read.Stacked on #5337 (DPNS v3, also protocol version 15), which is stacked on the sync PR #5404.
Retargeted from protocol version 14 to 15
This PR first edited protocol version 14 in place. With DPNS v3 moved to
v5.1-devand protocol version 15, protocol version 14 is the shipped 5.0 line, so the single read now has its own version:DriveDocumentDeleteMethodVersionsgainsdelete_consumed_document_operations, 0 in every existing table.DRIVE_DOCUMENT_METHOD_VERSIONS_V5sets it to 1, andDRIVE_VERSION_V10, which onlyPLATFORM_V15selects, uses that table.query_documents_with_flagsandquery_documentsrun the same path query, but through different GroveDB calls (queryvsquery_item_value, which differ in how they treat a missing intermediate tree), so at 0 the lookup keeps its plainquery_documentsand its cost, also for a reveal that finds nothing.v5.0-dev's again; protocol version 15 note 2 describes the change.Issue being fixed or feature implemented
A create that consumes a commitment (a
refersTolookup withconsume: true) read the commitment twice:force_delete_document_for_contract_operations, which reads the document again by id to know which index entries to remove and whom to refund (billed again).What was done?
fetch_document_through_lookupreturns the document with its stored element's storage flags: atdelete_consumed_document_operations1 throughquery_documents_with_flags, at 0 throughquery_documentswith no flags.ConsumedDocumentcarries the document and storage flags the lookup read, instead of only its id.Drive::force_delete_read_document_for_contract_operationsdispatches ondelete_consumed_document_operations: 0 isforce_delete_document_for_contract_operationsby the document's id, exactly what protocol version 14 runs; 1 is its operations less its read, with the same guards (refusesindexOnlyand history-keeping types).AddDocumentAndDeleteConsumeddeletes each consumed document through it. A contested create's consume uses the newForceDeleteReadDocumentoperation, which converts exactly asForceDeleteDocumentdid at 0;ForceDeleteDocumentkeeps serving moderator deletes and ttl expiry.Before and after
should_charge_a_name_at_protocol_versions_14_and_15), processingchooseFirstreveal, accepted (should_pin_the_fees_of_a_reveal_choosing_the_first_of_its_preorders), processingStorage fees and refunds are unchanged everywhere. The
chooseFirstreveal is pinned at both versions: at 14 it is the feev5.0-devships.In-place changes to shipped generations
fetch_document_through_lookup): every protocol version selects it. It branches on the new slot, which is 0 in every table before protocol version 15, and at 0 it runs exactly the query it ran (same call, same arguments, cost added the same way). The returned storage flags areNonethere and nothing reads them.ConsumedDocument; at 0 the delete reads only its id, as before.DocumentOperationType::AddDocumentAndDeleteConsumedand the contested create's consume go throughforce_delete_read_document_for_contract_operations, which at 0 callsforce_delete_document_for_contract_operationswith the same arguments the old code passed.How Has This Been Tested?
should_delete_a_read_document_as_a_force_delete_does_less_its_read(latest: the operations equal the force delete's after its read) and newshould_delete_a_consumed_document_by_its_id_at_protocol_version_14(the operations equal the by-id force delete's).should_pin_the_fees_of_a_reveal_choosing_the_first_of_its_preordersnow runs at 14 and 15 (the commit and reveal fixture takes a protocol version): 3,574,720 at 14, unchanged, and 3,562,700 at 15.cargo test -p drive-abci --lib -- dpns_v3_tests commit_reveal consume choose_first only_when_consumed refers_to dpns: 120 passed.cargo test -p drive --lib -- drive::document::delete util::batch::drive_op_batch: 52 passed.cargo clippy -p drive -p drive-abci -p platform-version --all-targetsandcargo fmt --all -- --check: clean.Breaking Changes
Consensus change at protocol version 15: a create that consumes a commitment is charged one document read less. Nothing changes at protocol version 14 or for clients.
Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code
PR Hygiene ·
f338eb1/self-reviewedsystem-contracts— you own itjs-wasm-sdk(packages/js-dash-sdk/src/SDK/Client/Platform/methods/contracts/get.spec.ts,packages/js-dash-sdk/src/SDK/Client/Platform/methods/contracts/get.ts,packages/js-dash-sdk/src/SDK/Client/Platform/methods/names/register.spec.tsand 3 more) — shumkovdpp— you own itrs-drive-abci— you own itrs-drive— you own itrust-sdk(packages/rs-sdk/src/platform/dpns_usernames/mod.rs,packages/rs-sdk/src/sdk.rs) — lklimek or shumkovWhen every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.