feat(platform)!: delta-based data contract update transition for protocol version 15 - #4730
QuantumExplorer wants to merge 1 commit into
Conversation
…ocol version 15 A V0 data contract update re-sends the whole contract even for a one-keyword change. Protocol version 15 adds DataContractUpdateTransitionV1, which carries only the delta: new and updated document schemas and shared definitions, new groups and tokens, added and removed keywords, an optional config and a tri-state description change, keyed by the contract id, the owner and the new version. Validation merges the delta onto the stored contract (DataContract::apply_update, new contract method version slot) and then holds the merged contract to exactly the checks a full-contract update gets: the generation-1 update rules, the identities new groups and tokens name, external token costs and reference declarations. The delta shape itself adds three checks with their own consensus errors: the submitter must own the contract, updated entries must exist and new entries must not (DataContractUpdateEntryNotFoundError 40011, DataContractUpdateEntryAlreadyExistsError 40010), and the sections of one transition must not overlap (DataContractUpdateOverlappingEntriesError 10277). A missing contract is a consensus error with a nonce bump, never an execution error. Drive gains a V1 update action that carries the delta's registration cost, so a delta pays only for what it adds where a full-contract update pays for the whole contract again. Proving fetches the stored contract to learn whether it keeps history; verification checks every field of the delta against the proven contract. Protocol version 15 is introduced here as the first consensus change on the 4.3 line: v15.rs, DRIVE_ABCI_VALIDATION_VERSIONS_V11 (contract update basic structure 2, state 2, transform_into_action 1) and STATE_TRANSITION_SERIALIZATION_VERSIONS_V4 (the V1 update joins the wire and becomes the client default). The v10 validation table and the V3 serialization table stay byte-identical for protocol version 14 replay, and a delta reaching a pre-15 validator is an UnsupportedVersionError consensus error. The update accessor now returns the embedded contract as an Option, with a data_contract_id accessor beside it. new_from_data_contract keeps building V0; new_from_contract_update and from_contract_update take the stored and the updated contract and pick the form the platform version defaults to (V1 from protocol version 15). platform-wallet's contract update uses the delta form; wasm-dpp and wasm-dpp2 expose the new variant. Reimplements the update half of PR #3021 from scratch. The create transition V1 from that attempt is deliberately left for a follow-up. Co-Authored-By: Claude Fable 5.1 <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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 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 |
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-09-14T00:32:01.373Z |
|
⛔ Final review complete — 3 blocking finding(s) (commit a9ce612) · triage: critical |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## v4.3-dev #4730 +/- ##
============================================
- Coverage 84.10% 77.91% -6.19%
============================================
Files 2797 2806 +9
Lines 379933 403167 +23234
============================================
- Hits 319541 314129 -5412
- Misses 60392 89038 +28646
🚀 New features to boost your workflow:
|
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
The delta transition is integrated across the versioned protocol, execution, storage, and client layers, but the new delta representation has correctness gaps. In particular, keyword reordering is silently lost during delta extraction, and execution-proof verification authenticates only the fields mentioned by the delta rather than the complete materialized contract, allowing false-positive verification. The WASM declarations and wallet FFI also do not expose the full V1 delta surface.
🔴 3 blocking | 🟡 2 suggestion(s)
2 finding(s) not shown inline (the lines are not part of this PR's diff)
🟡 Suggestion: Expose the V1 delta shape in the WASM TypeScript declarations
packages/wasm-dpp2/src/data_contract/transitions/update.rs:19-41
The Rust transition crossing this boundary is now the DataContractUpdateTransition enum, which can contain either the V0 full-contract form or the V1 delta form. The generic conversion methods can consequently return V1 values, but DataContractUpdateTransitionObject and DataContractUpdateTransitionJSON still require the V0-only dataContract field and omit the V1 fields such as dataContractId, ownerId, version, delta maps, keyword changes, config, and description. Typed consumers cannot construct valid V1 objects for fromObject/fromJSON, and the declared return type does not describe V1 values returned by those methods. Update the declarations to represent the V0/V1 union and the corresponding JSON/object field types.
source: muse-spark-1.3-contributor (phase1-reviewer: general, ffi-engineer, rust-quality, security-auditor); gpt-6-astra (phase2-reviewer: general, ffi-engineer, rust-quality, security-auditor)
🟡 Suggestion: Provide an FFI representation for clearing the contract description
packages/rs-platform-wallet-ffi/src/data_contract.rs:90-102
V1 uses DescriptionUpdate as a tri-state value: Keep, Clear, or Set. The wallet FFI API exposes description as an optional C string, and its current conversion treats NULL or an empty string as absence/preserve rather than as an explicit clear operation. As a result, C and Swift callers using this API can retain or replace a description but cannot construct the supported Clear delta. Add an explicit clear flag or another unambiguous sentinel while preserving the existing distinction between omitted description and an empty description.
source: muse-spark-1.3-contributor (phase1-reviewer: general, ffi-engineer, rust-quality, security-auditor)
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: ffi-engineer); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 8: 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 change directly modifies consensus validation in packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/state/v2/mod.rs and basic_structure/v2/mod.rs, introducing protocol-v15 delta materialization, ownership and entry checks, nonce handling, and delta-based fee calculation across versioned execution paths. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— ffi-engineer (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 11% left, 5h 100% left),glm-5.3-flash(zai below 15% reserve: 5h 100% left, weekly 13% left) - Fresh verifier:
gpt-6-astra— final-verifier; agentastra-verifier - Phase 2 reviewers:
gpt-6-astra— general (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— ffi-engineer (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-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/mod.rs`:
- [BLOCKING] packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/mod.rs:272-283: Reject keyword reordering instead of silently dropping it
The delta extractor computes keyword changes only as set differences. If the old contract contains `["one", "two"]` and the new contract contains `["two", "one"]`, both delta lists are empty, so applying the resulting transition retains the old order rather than producing the supplied contract. The same loss of ordering occurs when removals and additions alter the relative order: removals preserve the remaining old order and additions are appended. Keyword order is part of the stored contract representation and existing contract comparison treats the vectors positionally, so this API can successfully produce a transition whose applied contract differs from the caller's contract. Reject reorder-only changes as not expressible, or extend the delta format to encode ordering explicitly.
In `packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs`:
- [BLOCKING] packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs:121-134: Verify the complete materialized contract for V1 update proofs
V0 update proof verification compares the complete proven contract with the contract embedded in the signed transition. The V1 path instead calls `first_mismatch`, which checks only fields named by the delta and intentionally ignores untouched fields. The proof supplies only the resulting contract, and for a non-history contract does not supply the pre-update contract. Therefore a proven contract with the same ID, owner, version, and carried delta values can contain arbitrary changes to untouched configuration, descriptions, document types, definitions, groups, tokens, or keyword ordering and still pass verification. This permits a light client to report that a particular signed delta executed when the authenticated state was produced by a different update. The proof must authenticate the pre-state or otherwise bind and compare the complete materialized result against the signed delta.
In `packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/first_mismatch.rs`:
- [BLOCKING] packages/rs-dpp/src/state_transition/state_transitions/contract/data_contract_update_transition/v1/first_mismatch.rs:44-63: Reject contradictory delta sections during proof verification
The V1 proof verifier checks that entries in `new_document_schemas` and `updated_document_schemas` match the proven contract, but it does not reject the same document type appearing in both sections. The same omission exists for `new_schema_defs` versus `updated_schema_defs`, and duplicate additions/removals are not checked here either. Basic-structure validation rejects these malformed transitions on the normal platform path, but `first_mismatch` is independently used by proof verification and does not run that validation. A malformed signed transition can therefore be rejected by the platform while an authenticated proof of the existing contract still passes `first_mismatch`, causing the client to report successful execution. Proof verification must validate delta shape and reject overlapping or contradictory sections before checking the resulting contract.
In `packages/wasm-dpp2/src/data_contract/transitions/update.rs`:
- [SUGGESTION] packages/wasm-dpp2/src/data_contract/transitions/update.rs:19-41: Expose the V1 delta shape in the WASM TypeScript declarations
The Rust transition crossing this boundary is now the `DataContractUpdateTransition` enum, which can contain either the V0 full-contract form or the V1 delta form. The generic conversion methods can consequently return V1 values, but `DataContractUpdateTransitionObject` and `DataContractUpdateTransitionJSON` still require the V0-only `dataContract` field and omit the V1 fields such as `dataContractId`, `ownerId`, `version`, delta maps, keyword changes, config, and description. Typed consumers cannot construct valid V1 objects for `fromObject`/`fromJSON`, and the declared return type does not describe V1 values returned by those methods. Update the declarations to represent the V0/V1 union and the corresponding JSON/object field types.
In `packages/rs-platform-wallet-ffi/src/data_contract.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/data_contract.rs:90-102: Provide an FFI representation for clearing the contract description
V1 uses `DescriptionUpdate` as a tri-state value: `Keep`, `Clear`, or `Set`. The wallet FFI API exposes description as an optional C string, and its current conversion treats NULL or an empty string as absence/preserve rather than as an explicit clear operation. As a result, C and Swift callers using this API can retain or replace a description but cannot construct the supported `Clear` delta. Add an explicit clear flag or another unambiguous sentinel while preserving the existing distinction between omitted description and an empty description.
| let remove_keywords = old_contract | ||
| .keywords() | ||
| .iter() | ||
| .filter(|keyword| !new_contract.keywords().contains(*keyword)) | ||
| .cloned() | ||
| .collect(); | ||
| let add_keywords = new_contract | ||
| .keywords() | ||
| .iter() | ||
| .filter(|keyword| !old_contract.keywords().contains(*keyword)) | ||
| .cloned() | ||
| .collect(); |
There was a problem hiding this comment.
🔴 Blocking: Reject keyword reordering instead of silently dropping it
The delta extractor computes keyword changes only as set differences. If the old contract contains ["one", "two"] and the new contract contains ["two", "one"], both delta lists are empty, so applying the resulting transition retains the old order rather than producing the supplied contract. The same loss of ordering occurs when removals and additions alter the relative order: removals preserve the remaining old order and additions are appended. Keyword order is part of the stored contract representation and existing contract comparison treats the vectors positionally, so this API can successfully produce a transition whose applied contract differs from the caller's contract. Reject reorder-only changes as not expressible, or extend the delta format to encode ordering explicitly.
source: gpt-6-astra (phase2-reviewer: general, ffi-engineer, rust-quality, security-auditor)
| let mismatch = match data_contract_update { | ||
| DataContractUpdateTransition::V0(v0) => { | ||
| let contract_for_serialization: DataContractInSerializationFormat = | ||
| contract | ||
| .clone() | ||
| .try_into_platform_versioned(platform_version)?; | ||
| contract_for_serialization | ||
| .first_mismatch(&v0.data_contract) | ||
| .map(|mismatch| mismatch.to_string()) | ||
| } | ||
| DataContractUpdateTransition::V1(v1) => v1.first_mismatch(&contract), | ||
| }; | ||
| if let Some(mismatch) = mismatch { | ||
| return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after update with id {}: {}", contract_id, mismatch)))); |
There was a problem hiding this comment.
🔴 Blocking: Verify the complete materialized contract for V1 update proofs
V0 update proof verification compares the complete proven contract with the contract embedded in the signed transition. The V1 path instead calls first_mismatch, which checks only fields named by the delta and intentionally ignores untouched fields. The proof supplies only the resulting contract, and for a non-history contract does not supply the pre-update contract. Therefore a proven contract with the same ID, owner, version, and carried delta values can contain arbitrary changes to untouched configuration, descriptions, document types, definitions, groups, tokens, or keyword ordering and still pass verification. This permits a light client to report that a particular signed delta executed when the authenticated state was produced by a different update. The proof must authenticate the pre-state or otherwise bind and compare the complete materialized result against the signed delta.
source: muse-spark-1.3-contributor (phase1-reviewer: general, ffi-engineer, rust-quality, security-auditor); gpt-6-astra (phase2-reviewer: general)
| let document_schemas = updated_contract.document_schemas(); | ||
| for (name, schema) in self | ||
| .new_document_schemas | ||
| .iter() | ||
| .chain(self.updated_document_schemas.iter()) | ||
| { | ||
| if document_schemas.get(name) != Some(&schema) { | ||
| return Some(format!("document type '{name}' differs")); | ||
| } | ||
| } | ||
|
|
||
| let schema_defs = updated_contract.schema_defs(); | ||
| for (name, definition) in self | ||
| .new_schema_defs | ||
| .iter() | ||
| .chain(self.updated_schema_defs.iter()) | ||
| { | ||
| if schema_defs.and_then(|definitions| definitions.get(name)) != Some(definition) { | ||
| return Some(format!("schema definition '{name}' differs")); | ||
| } |
There was a problem hiding this comment.
🔴 Blocking: Reject contradictory delta sections during proof verification
The V1 proof verifier checks that entries in new_document_schemas and updated_document_schemas match the proven contract, but it does not reject the same document type appearing in both sections. The same omission exists for new_schema_defs versus updated_schema_defs, and duplicate additions/removals are not checked here either. Basic-structure validation rejects these malformed transitions on the normal platform path, but first_mismatch is independently used by proof verification and does not run that validation. A malformed signed transition can therefore be rejected by the platform while an authenticated proof of the existing contract still passes first_mismatch, causing the client to report successful execution. Proof verification must validate delta shape and reject overlapping or contradictory sections before checking the resulting contract.
source: gpt-6-astra (phase2-reviewer: general, ffi-engineer, rust-quality, security-auditor)
Issue being fixed or feature implemented
A V0
DataContractUpdateTransitionembeds the whole contract, so a one-keyword change re-sends (and re-validates, and re-prices) every document type. PR #3021 set out to fix this with a delta-based V1 update transition, but it targeted protocol version 12, is 1800+ commits stale, and carried a review blocker; a rebase was not practical. This PR reimplements the update half of that idea from scratch on the 4.3 line, as the first consensus change of protocol version 15.What was done?
Protocol version 15 (
v15.rs,DRIVE_ABCI_VALIDATION_VERSIONS_V11,STATE_TRANSITION_SERIALIZATION_VERSIONS_V4). Everything else matches v14; the v10 validation table and the V3 serialization table stay byte-identical for v14 replay.DataContractUpdateTransitionV1(rs-dpp) carries only the delta, keyed bydata_contract_id,owner_idand the newversion:config: Option<DataContractConfig>updated_schema_defs/new_schema_defs,updated_document_schemas/new_document_schemasnew_groups,new_tokens(positions continue the stored ones)add_keywords/remove_keywordsdescription: DescriptionUpdate { Keep | Clear | Set(String) }(a tri-state enum rather thanOption<Option<String>>, so JSON round-trips)Materialize, then reuse.
DataContract::apply_update(newcontract_versions.methods.apply_updateslot) merges the delta onto the stored contract and rebuilds aDataContractInSerializationFormatV1. The result then goes through exactly what a V0 update goes through: the generation-1validate_updaterules, the identity checks for new groups and tokens, external token costs andrefersToreference validation. Only delta-shape checks are new:DataContractUpdatePermissionError)DataContractUpdateEntryNotFoundError40011,DataContractUpdateEntryAlreadyExistsError40010)DataContractUpdateOverlappingEntriesError10277)drive-abci gets generation 2 of contract update
basic_structureandstateandtransform_into_action1: fetch the stored contract (fee-charged), turn a missing contract into a consensus error with a nonce bump (never an execution error), apply the delta, then run the generation-1 checks on the merged contract. A V1 transition that reaches a generation-0/1 validator (a pre-v15 node checking a newer transition) is anUnsupportedVersionErrorconsensus error, not an execution error, so a mis-versioned transition cannot stall a proposal.rs-drive adds a V1 update action transformer (
try_from_borrowed_v1_transition) that still producesDataContractUpdateTransitionActionV0; the action carries the delta's registration cost, so a delta pays only for what it adds (new/updated document types and their indexes, new tokens, added keywords, no base fee) where a V0 update pays for the whole contract again. Proving fetches the stored contract to learnkeeps_history; verification checks every delta field against the proven contract (first_mismatch).API changes (see Breaking Changes):
data_contract()returnsOption,set_data_contractreturnsResult, newdata_contract_id().new_from_data_contractalways builds V0; newnew_from_contract_update(old, new, ...)/from_contract_updatepick V0 or V1 from the platform version's default (V1 from v15). platform-wallet'supdate_data_contract_with_signeruses the delta form; wasm-dpp / wasm-dpp2 expose the new accessors.Not included: the create transition V1 from #3021 (flat fields, id derived from owner and nonce). It was dead code there (never selected by any version table), saves about 40 bytes per registration, and touches every create validator and SDK registration path. It belongs in its own PR.
Note: #4706 also introduces
v15.rson this base. Whichever merges second rebases; the conflict is the v15 doc comment and the two slots each PR changes.How Has This Been Tested?
cargo check -p platform-version -p dpp -p drive -p drive-abci --all-targets: clean.cargo test -p platform-version: 20 passed (the version array and the v13/v14 gating tests).cargo test -p dpp --lib data_contract_update: 24 passed. Covers V1 serialization round-trips (bincode, JSON, platform value),from_contract_updatedelta extraction,first_mismatch,registration_cost, and the latest-version default being the delta form.cargo test -p drive-abci --lib -- data_contract_update execution_event: 61 passed. Includes the generation-2 state tests (a delta adding a document type, keywords and a description; a missing contract as a paid consensus error with a nonce bump; an owner that does not own the contract; a new document type that already exists; a delta held to the full-contract update rules;delta_update_reaching_a_pre_v15_validator_is_an_unsupported_version_error, where a V1 handed to the v14 basic-structure validator and to the V0 Drive transformer yieldsUnsupportedVersionErrorrather than an execution error; a missing contract in the transformer is not an execution error) and the check_tx tests (data_contract_update_check_tx_latest_protocol_version_deltapins the delta's processing fee against the V0 fee for the same change).cargo test -p drive-abci --lib data_contract_update::basic_structure: 4 passed, new in this PR: a non-overlapping delta passes; a document type, a schema definition, or a keyword named in two conflicting sections is rejected withDataContractUpdateOverlappingEntriesError(10277).cargo test -p drive --lib data_contract_update: 22 passed (V0 and V1 action transformers, prove / verify of a delta against the stored contract).cargo clippy -p platform-version -p dpp -p drive -p drive-abci --all-targets --all-features -- -D warnings: clean.cargo fmt --all -- --check: clean.Breaking Changes
DataContractUpdateTransitionAccessorsV0::data_contract()returnsOption<&DataContractInSerializationFormat>(V1 embeds no contract);set_data_contractreturnsResult.new_from_data_contractalways builds V0; usenew_from_contract_update/from_contract_updatefor the delta form.Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code