Skip to content

feat(dpp): add documents_countable to DocumentTypeV2 for O(1) total document counts#3457

Open
QuantumExplorer wants to merge 1 commit intov3.1-devfrom
feat/documents-countable
Open

feat(dpp): add documents_countable to DocumentTypeV2 for O(1) total document counts#3457
QuantumExplorer wants to merge 1 commit intov3.1-devfrom
feat/documents-countable

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented Apr 8, 2026

Summary

Adds two new fields to DocumentTypeV2 for O(1) document counting:

  • documents_countable: bool — primary key tree uses CountTree, enabling O(1) total document count queries
  • blast_syncable: bool — primary key tree uses ProvableCountTree for BLAST sync support. Implies documents_countable = true.

Changes

  • DocumentTypeV2: New struct with both fields
  • DocumentType enum: Added V2 variant with full match arm coverage
  • Accessor traits: DocumentTypeV2Getters / DocumentTypeV2Setters
  • Schema parsing: try_from_schema v2 parses both "documentsCountable" and "blastSyncable" (protocol version 12+ required)
  • Drive: CountTree for countable, ProvableCountTree for blast_syncable
  • Platform version: CONTRACT_VERSIONS_V4 with try_from_schema: 2, used in v12

Schema example

{
  "person": {
    "type": "object",
    "documentsCountable": true,
    "properties": { ... }
  }
}

Or with BLAST sync:

{
  "person": {
    "type": "object",
    "blastSyncable": true,
    "properties": { ... }
  }
}

NOT in this PR

Count query extensions — follow-up to PR #3435.

Test plan

  • cargo check -p dpp -p drive passes
  • cargo fmt --all clean
  • CI

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added DocumentType V2 with two optional flags: documentsCountable (O(1) total counts) and rangeCountable (provable range counts; implies documentsCountable). Drive now selects primary-key storage trees based on these flags.
  • Bug Fixes / Validation

    • Updates reject changes to documentsCountable or rangeCountable to preserve compatibility.
  • Platform

    • Platform versioning updated to include the new contract versions enabling V2.
  • Tests

    • Unit tests cover V2 defaults, conversions, getters/setters, and flag invariants.

@github-actions github-actions bot added this to the v3.1.0 milestone Apr 8, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Add DocumentTypeV2 and two optional top-level schema booleans (documentsCountable, rangeCountable); parse and preserve them in DocumentType APIs, enforce immutability on updates, compute primary-key TreeType from these flags, and select appropriate Drive low-level tree creation (provable-count, count, or normal). Platform contract/versioning updated to expose the v2 parser.

Changes

Cohort / File(s) Summary
Schema Definition
packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
Added top-level boolean properties documentsCountable and rangeCountable (range implies documentsCountable).
DocumentType V2 Core
packages/rs-dpp/src/data_contract/document_type/v2/mod.rs, packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs, packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
New DocumentTypeV2 struct with fields including documents_countable and range_countable, accessors/setters, conversions from V0/V1 (default false), random constructors, and unit tests covering semantics and invariants.
Accessor Traits & Dispatch
packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs, packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
Added DocumentTypeV2Getters/DocumentTypeV2Setters; extended dispatch so DocumentType::V2 delegates getters/setters and token-cost APIs to v2, and V0/V1 return false/no-ops for new flags.
Public Enum Variants & Constants
packages/rs-dpp/src/data_contract/document_type/mod.rs
Exported v2 module; added V2 variants to DocumentType, DocumentTypeRef, DocumentTypeMutRef; added DOCUMENTS_COUNTABLE and RANGE_COUNTABLE constants.
Schema Parsing (try_from_schema v2)
packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs, .../try_from_schema/mod.rs
Added v2 parser reading optional booleans (defaults false); `documents_countable = documentsCountable
Versioning & Platform
packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs, .../mod.rs, packages/rs-platform-version/src/version/v12.rs
Added CONTRACT_VERSIONS_V4 (sets try_from_schema: 2) and updated PLATFORM_V12 to use v4 contract versions.
Drive: Insert/Update Contract Integration
packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs, packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs
Primary-key tree initialization now branches: provable-count tree if range_countable(), count tree if documents_countable(), else legacy batch_insert_empty_tree; adapted type_path ownership for low-level ops.
Drive Document Ops & Estimation
packages/rs-drive/src/drive/document/..., packages/rs-drive/src/drive/document/primary_key_tree_type.rs
Replaced hardcoded NormalTree with computed primary_key_tree_type (based on document-type flags and platform feature version) across insert/delete/query/estimation flows; added DocumentTypePrimaryKeyTreeType trait and tests.
Low-level Drive Operation
packages/rs-drive/src/fees/op.rs
Added LowLevelDriveOperation::for_known_path_key_empty_provable_count_tree(...) to create/insert an empty provable-count tree element.
Validation / Update Rules & Tests
packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs, packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs
Document-type update validation now rejects changes to documents_countable or range_countable; tests extended to cover DocumentType::V2.
Drive Versioning (method versions)
packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/...
Added primary_key_tree_type: FeatureVersion to Drive document method versions and initialized it in v1/v2 constants.
Misc Integrations
packages/rs-dpp/src/..., packages/rs-drive/src/...
Multiple import/dispatch updates to include V2 getters/setters and handle DocumentType::V2 across modules (versioned methods, conversions, try_from_schema dispatch).

Sequence Diagram(s)

sequenceDiagram
    participant Schema as Schema (JSON)
    participant Parser as try_from_schema_v2
    participant DocType as DocumentTypeV2
    participant Drive as Drive (Insert/Update)
    participant Storage as LowLevelTreeOps

    Schema->>Parser: provide schema (may include documentsCountable, rangeCountable)
    Parser->>Parser: read flags, compute documents_countable = documentsCountable || rangeCountable
    Parser->>DocType: construct DocumentTypeV2 with flags
    DocType->>Drive: Drive queries range_countable()/documents_countable()
    alt range_countable == true
        Drive->>Storage: for_known_path_key_empty_provable_count_tree (ProvableCountTree)
    else documents_countable == true
        Drive->>Storage: for_known_path_key_empty_count_tree (CountTree)
    else
        Drive->>Storage: batch_insert_empty_tree (NormalTree fallback)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nudged two flags into the schema ground,
count and range where trees are newly found.
Parsers tuck them in, DocTypes hold them tight,
Drive chooses which tree to plant by night,
A rabbit hops — protocols grow right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main feature: adding documents_countable to DocumentTypeV2 to enable O(1) total document counts, directly reflecting the primary objective of this changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/documents-countable

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented Apr 8, 2026

✅ Review complete (commit 51d1808)

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 8, 2026

Codecov Report

❌ Patch coverage is 85.57920% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.13%. Comparing base (46e037e) to head (51d1808).
⚠️ Report is 5 commits behind head on v3.1-dev.

Files with missing lines Patch % Lines
...ct/document_type/methods/validate_update/v0/mod.rs 18.18% 18 Missing ⚠️
...rc/drive/contract/update/update_contract/v0/mod.rs 38.09% 13 Missing ⚠️
...rc/drive/contract/insert/insert_contract/v0/mod.rs 42.85% 12 Missing ⚠️
packages/rs-drive/src/fees/op.rs 0.00% 12 Missing ⚠️
...-drive/src/drive/document/primary_key_tree_type.rs 91.22% 5 Missing ⚠️
...document_type/class_methods/try_from_schema/mod.rs 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           v3.1-dev    #3457    +/-   ##
==========================================
  Coverage     84.12%   84.13%            
==========================================
  Files          2601     2604     +3     
  Lines        271457   271856   +399     
==========================================
+ Hits         228366   228717   +351     
- Misses        43091    43139    +48     
Components Coverage Δ
dpp 79.44% <93.58%> (+0.07%) ⬆️
drive 84.17% <66.92%> (-0.03%) ⬇️
drive-abci 87.39% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.10% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 55.66% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs (1)

10-39: Random V2 generators never exercise documents_countable.

Line 16 and Line 32 both build through DocumentTypeV1, and From<DocumentTypeV1> for DocumentTypeV2 initializes documents_countable to false. Any fuzz/property tests using these helpers therefore miss the new CountTree path entirely. Please thread the flag through RandomDocumentTypeParameters or randomize it when the platform version supports the feature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs`
around lines 10 - 39, The current helpers random_document_type and
invalid_random_document_type call DocumentTypeV1 generators and convert to
DocumentTypeV2, but the From<DocumentTypeV1> for DocumentTypeV2 sets
documents_countable = false so the CountTree path is never exercised; update
either RandomDocumentTypeParameters to include a documents_countable boolean and
pass it through into DocumentTypeV1 -> DocumentTypeV2 construction, or (simpler)
when platform_version supports CountTree, use the provided rng inside
random_document_type and invalid_random_document_type to set
DocumentTypeV2.documents_countable randomly before returning; locate the two
functions (random_document_type, invalid_random_document_type) and the
From<DocumentTypeV1> for DocumentTypeV2 conversion to implement threading or
randomized assignment conditioned on platform_version.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs`:
- Around line 301-307: The match in DocumentTypeRef::schema_owned (and similarly
in DocumentTypeMutRef::schema_owned) contains duplicated DocumentTypeRef::V2
pattern arms causing unreachable code; remove the redundant V2 arm so each
variant (V0, V1, V2) appears exactly once in the match, keeping the correct call
(v2.clone().schema_owned()) for the single V2 arm and run cargo clippy to verify
the warning is gone.

In
`@packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs`:
- Around line 51-61: In the v1 parser in try_from_schema
(packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs),
add logic to detect the DOCUMENTS_COUNTABLE field during parsing and, when
full_validation is enabled and platform_version.protocol_version < 12 (i.e.,
v1/protocols 2–3), return the same
ProtocolError::ConsensusError(BasicError::UnsupportedFeatureError(...)) as the
v2 branch does; specifically, mirror the check that sets documents_countable and
returns UnsupportedFeatureError for "documentsCountable" so the v1 path no
longer silently ignores DOCUMENTS_COUNTABLE but fails explicitly under older
protocol versions.

---

Nitpick comments:
In `@packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs`:
- Around line 10-39: The current helpers random_document_type and
invalid_random_document_type call DocumentTypeV1 generators and convert to
DocumentTypeV2, but the From<DocumentTypeV1> for DocumentTypeV2 sets
documents_countable = false so the CountTree path is never exercised; update
either RandomDocumentTypeParameters to include a documents_countable boolean and
pass it through into DocumentTypeV1 -> DocumentTypeV2 construction, or (simpler)
when platform_version supports CountTree, use the provided rng inside
random_document_type and invalid_random_document_type to set
DocumentTypeV2.documents_countable randomly before returning; locate the two
functions (random_document_type, invalid_random_document_type) and the
From<DocumentTypeV1> for DocumentTypeV2 conversion to implement threading or
randomized assignment conditioned on platform_version.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23a22067-a368-4eb1-b3a4-17e2a65e5136

📥 Commits

Reviewing files that changed from the base of the PR and between 46e037e and 9b5ef81.

📒 Files selected for processing (15)
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/v12.rs

Comment thread packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
@QuantumExplorer QuantumExplorer force-pushed the feat/documents-countable branch 2 times, most recently from d3b946a to 1e92143 Compare April 8, 2026 09:55
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs (1)

8-8: Tighten inline comment scope to include both v2 flags.

Line 8 and Line 36 comments mention only documentsCountable, but the v2 parser path also covers blastSyncable. Updating the comment would prevent future confusion.

✏️ Suggested comment tweak
-// Introduced in protocol version 12, adds documents_countable feature via try_from_schema v2
+// Introduced in protocol version 12, enables v2 schema parsing for documentsCountable/blastSyncable
...
-            try_from_schema: 2, // changed: supports documentsCountable
+            try_from_schema: 2, // changed: supports documentsCountable and blastSyncable

Also applies to: 36-36

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs`
at line 8, Update the inline comment(s) that currently mention only
`documentsCountable` to also include `blastSyncable`, clarifying that both
feature flags are introduced in protocol version 12 and are set via the
`try_from_schema v2` parser path; locate the comments near the `try_from_schema`
v2 handling (references: `documentsCountable`, `blastSyncable`, `try_from_schema
v2`) in v4.rs and adjust the text on both occurrences so the scope clearly
covers both flags.
packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs (2)

64-79: Consider using a named constant for the protocol version threshold.

The literal 12 is used here and in v1/mod.rs (line 325) for the same feature gate. While PROTOCOL_VERSION_12 is defined in packages/rs-platform-version/src/version/v12.rs, it's not used here. Using a constant would improve maintainability and clarity.

💡 Example improvement
+use crate::version::PROTOCOL_VERSION_12; // or appropriate import path

 #[cfg(feature = "validation")]
-if full_validation && needs_v2 && platform_version.protocol_version < 12 {
+if full_validation && needs_v2 && platform_version.protocol_version < PROTOCOL_VERSION_12 {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs`
around lines 64 - 79, The code uses the literal 12 as a protocol threshold when
gating features (in the if checking full_validation && needs_v2 &&
platform_version.protocol_version < 12); replace the magic number with the named
constant PROTOCOL_VERSION_12 (import it from the platform-version crate/module
where it is defined) and use that constant in the comparison
(platform_version.protocol_version < PROTOCOL_VERSION_12); also make the same
replacement in the corresponding v1 check to keep behavior consistent and add
the necessary use/import for PROTOCOL_VERSION_12 at the top of the module.

96-100: Redundant assignment: documents_countable || blast_syncable is already enforced by the getter.

At line 98, documents_countable is set to documents_countable || blast_syncable. However, the DocumentTypeV2Getters::documents_countable() getter (in v2/accessors.rs lines 202-204) already returns self.documents_countable || self.blast_syncable. This means the field value doesn't need to encode the implication—the getter handles it.

Consider simplifying to just set the parsed values directly:

♻️ Proposed simplification
         if needs_v2 {
             let mut v2: DocumentTypeV2 = v1.into();
-            v2.documents_countable = documents_countable || blast_syncable;
+            v2.documents_countable = documents_countable;
             v2.blast_syncable = blast_syncable;
             Ok(v2.into())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs`
around lines 96 - 100, The code redundantly sets v2.documents_countable =
documents_countable || blast_syncable inside the try_from path even though
DocumentTypeV2Getters::documents_countable() already returns
self.documents_countable || self.blast_syncable; change the assignment to store
the parsed boolean directly (v2.documents_countable = documents_countable) and
leave v2.blast_syncable = blast_syncable as-is so the getter enforces the
implication, updating the conversion in the DocumentTypeV2 construction path
that uses the try_from logic where DocumentTypeV2 is produced.
packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs (1)

1-40: Indentation should use 4 spaces per coding guidelines.

The file uses 2-space indentation, but Rust files in this repository should use 4-space indentation. Also, random_document_type is missing a doc comment while invalid_random_document_type has one—consider adding a brief doc comment for consistency.

As per coding guidelines: "Use 4-space indent for Rust files".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs`
around lines 1 - 40, The file uses 2-space indentation and is missing a doc
comment for random_document_type; update the indentation to 4 spaces across the
entire file and add a brief doc comment above impl
DocumentTypeV2::random_document_type matching the style of
invalid_random_document_type (e.g., "Creates a random valid document type" or
similar). Ensure you adjust the indentation for the impl block and both methods
(random_document_type and invalid_random_document_type) so they use 4-space
indents and run rustfmt after changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs`:
- Around line 64-79: The code uses the literal 12 as a protocol threshold when
gating features (in the if checking full_validation && needs_v2 &&
platform_version.protocol_version < 12); replace the magic number with the named
constant PROTOCOL_VERSION_12 (import it from the platform-version crate/module
where it is defined) and use that constant in the comparison
(platform_version.protocol_version < PROTOCOL_VERSION_12); also make the same
replacement in the corresponding v1 check to keep behavior consistent and add
the necessary use/import for PROTOCOL_VERSION_12 at the top of the module.
- Around line 96-100: The code redundantly sets v2.documents_countable =
documents_countable || blast_syncable inside the try_from path even though
DocumentTypeV2Getters::documents_countable() already returns
self.documents_countable || self.blast_syncable; change the assignment to store
the parsed boolean directly (v2.documents_countable = documents_countable) and
leave v2.blast_syncable = blast_syncable as-is so the getter enforces the
implication, updating the conversion in the DocumentTypeV2 construction path
that uses the try_from logic where DocumentTypeV2 is produced.

In `@packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs`:
- Around line 1-40: The file uses 2-space indentation and is missing a doc
comment for random_document_type; update the indentation to 4 spaces across the
entire file and add a brief doc comment above impl
DocumentTypeV2::random_document_type matching the style of
invalid_random_document_type (e.g., "Creates a random valid document type" or
similar). Ensure you adjust the indentation for the impl block and both methods
(random_document_type and invalid_random_document_type) so they use 4-space
indents and run rustfmt after changes.

In
`@packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs`:
- Line 8: Update the inline comment(s) that currently mention only
`documentsCountable` to also include `blastSyncable`, clarifying that both
feature flags are introduced in protocol version 12 and are set via the
`try_from_schema v2` parser path; locate the comments near the `try_from_schema`
v2 handling (references: `documentsCountable`, `blastSyncable`, `try_from_schema
v2`) in v4.rs and adjust the text on both occurrences so the scope clearly
covers both flags.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5570ee7c-2eae-487c-9586-dd234a5251f6

📥 Commits

Reviewing files that changed from the base of the PR and between 9b5ef81 and d3b946a.

📒 Files selected for processing (15)
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/v12.rs
✅ Files skipped from review due to trivial changes (3)
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/v12.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs

@QuantumExplorer QuantumExplorer force-pushed the feat/documents-countable branch from 1e92143 to be7f38d Compare April 8, 2026 10:13
Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self Reviewed

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs (1)

871-905: Add direct coverage for the new V2-specific accessor dispatch.

There are no focused assertions here for documents_countable() / blast_syncable() on the owned, ref, and mut-ref variants. A small DocumentType::V2 test would lock in these new match arms and the V0/V1 fallback behavior.

Also applies to: 907-938

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs` around
lines 871 - 905, Add focused unit tests that exercise the V2-specific match arms
on DocumentType by constructing DocumentType::V2 (owned), a &DocumentType::V2
(ref) and a &mut DocumentType::V2 (mut-ref) and asserting the getters
DocumentTypeV2Getters::documents_countable and ::blast_syncable return the
expected values; also test setters
DocumentTypeV2Setters::set_documents_countable and ::set_blast_syncable mutate
the V2 state when called on the mut-ref and that calling the getters/setters on
V0/V1 remains a no-op/returns false as currently implemented; target the
DocumentType, DocumentTypeV2Getters, DocumentTypeV2Setters, documents_countable,
blast_syncable, set_documents_countable and set_blast_syncable symbols in your
new tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs`:
- Around line 211-220: The setter set_documents_countable currently allows
documents_countable to be set to false while blast_syncable may remain true;
update impl DocumentTypeV2Setters for DocumentTypeV2 so
set_documents_countable(&mut self, countable: bool) sets
self.documents_countable = countable and if countable == false also clear
self.blast_syncable to false to preserve the blast_syncable =>
documents_countable invariant (mirror the normalization done in
set_blast_syncable).

In `@packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs`:
- Around line 288-319: Existing code allows toggling documents_countable and
blast_syncable on contract updates which corrupts on-disk trees; modify
validate_config to detect any change to DocumentType::documents_countable or
DocumentType::blast_syncable for existing document types and reject the update.
Specifically, in validate_config compare the old and new document type maps (by
name/identifier) and if an existing type's documents_countable or blast_syncable
differs, return a validation error (match the style used for
documents_mutable/documents_keep_history checks). Ensure the same symbols are
referenced: validate_config, DocumentType (or document type map),
documents_countable, blast_syncable, and use the same error type/format used
elsewhere so callers (including update_contract logic) will fail fast.

---

Nitpick comments:
In `@packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs`:
- Around line 871-905: Add focused unit tests that exercise the V2-specific
match arms on DocumentType by constructing DocumentType::V2 (owned), a
&DocumentType::V2 (ref) and a &mut DocumentType::V2 (mut-ref) and asserting the
getters DocumentTypeV2Getters::documents_countable and ::blast_syncable return
the expected values; also test setters
DocumentTypeV2Setters::set_documents_countable and ::set_blast_syncable mutate
the V2 state when called on the mut-ref and that calling the getters/setters on
V0/V1 remains a no-op/returns false as currently implemented; target the
DocumentType, DocumentTypeV2Getters, DocumentTypeV2Setters, documents_countable,
blast_syncable, set_documents_countable and set_blast_syncable symbols in your
new tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc904f91-e9e2-4e3d-b430-2cf570fb26de

📥 Commits

Reviewing files that changed from the base of the PR and between d3b946a and be7f38d.

📒 Files selected for processing (15)
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/v12.rs
✅ Files skipped from review due to trivial changes (5)
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-platform-version/src/version/v12.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs

Comment thread packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
@QuantumExplorer QuantumExplorer force-pushed the feat/documents-countable branch from be7f38d to 1e21664 Compare April 8, 2026 10:33
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rs-dpp/src/data_contract/document_type/v2/mod.rs`:
- Around line 71-75: The two capability flags documents_countable and
blast_syncable currently have crate-level mutable visibility which allows
bypassing DocumentTypeV2Setters and breaking the invariant that blast_syncable
implies documents_countable; make both fields private (remove pub(in
crate::data_contract)) and enforce the invariant in the setter methods on
DocumentTypeV2 (DocumentTypeV2Setters): when setting blast_syncable = true also
set documents_countable = true, and when setting documents_countable = false
prevent or clear blast_syncable (or return an error) to avoid inconsistency;
update any internal callers to use the provided getters/setters instead of
writing the fields directly.

In `@packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs`:
- Around line 288-319: The update path currently always creates a NormalTree for
newly added document types; replicate the same conditional tree selection as in
the insert path by checking document_type.blast_syncable() and
document_type.documents_countable() and creating the matching tree: if
blast_syncable() create a ProvableCountTree (use
Element::empty_provable_count_tree_with_flags /
Element::empty_provable_count_tree and
LowLevelDriveOperation::insert_for_known_path_key_element with path_items and
vec![0]), else if documents_countable() call
LowLevelDriveOperation::for_known_path_key_empty_count_tree with path_items,
vec![0], and storage_flags.as_ref(), otherwise fall back to calling
batch_insert_empty_tree with Key(vec![0]), passing storage_flags.as_ref(), &mut
batch_operations and platform_version.drive; mirror the storage_flags handling
used in the insert code so update and insert behavior are consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68ecc863-7609-4966-bab9-2ef4bf430a49

📥 Commits

Reviewing files that changed from the base of the PR and between be7f38d and 1e21664.

📒 Files selected for processing (16)
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/v12.rs
✅ Files skipped from review due to trivial changes (3)
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/v12.rs

Comment on lines +71 to +75
/// When true, the primary key tree uses a CountTree enabling O(1) total document count queries
pub(in crate::data_contract) documents_countable: bool,
/// When true, the primary key tree uses a ProvableCountTree enabling BLAST sync.
/// Implies documents_countable = true.
pub(in crate::data_contract) blast_syncable: bool,
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restrict direct writes to capability flags to preserve invariants

blast_syncable is documented to imply documents_countable, but both fields are writable across crate::data_contract via pub(in crate::data_contract). That allows bypassing DocumentTypeV2Setters and creating inconsistent internal state.

🔧 Proposed fix
-    pub(in crate::data_contract) documents_countable: bool,
+    documents_countable: bool,
...
-    pub(in crate::data_contract) blast_syncable: bool,
+    blast_syncable: bool,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/src/data_contract/document_type/v2/mod.rs` around lines 71 -
75, The two capability flags documents_countable and blast_syncable currently
have crate-level mutable visibility which allows bypassing DocumentTypeV2Setters
and breaking the invariant that blast_syncable implies documents_countable; make
both fields private (remove pub(in crate::data_contract)) and enforce the
invariant in the setter methods on DocumentTypeV2 (DocumentTypeV2Setters): when
setting blast_syncable = true also set documents_countable = true, and when
setting documents_countable = false prevent or clear blast_syncable (or return
an error) to avoid inconsistency; update any internal callers to use the
provided getters/setters instead of writing the fields directly.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't validate a concrete current-SHA bug here. Crate-internal visibility alone does not create an observable invariant break, and I did not find a call path in this PR that bypasses the existing constructor/setter normalization to persist an inconsistent state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all direct field assignments to documents_countable and blast_syncable
rg -n 'documents_countable\s*=' --type rust -C 2
echo "---"
rg -n 'blast_syncable\s*=' --type rust -C 2

Length of output: 7843


@thepastaclaw, you're right — I've confirmed your analysis. The search results show two relevant facts:

  1. The getter absorbs field-level inconsistency: documents_countable() is defined as self.documents_countable || self.blast_syncable, so even a direct write of blast_syncable = true without touching documents_countable still produces the correct observable result from the getter.

  2. All production callers use the getter: the drive insert_contract code calls document_type.documents_countable() / document_type.blast_syncable(), not the raw fields. The test at Line 243 even demonstrates this pattern intentionally.

The try_from_schema v2 direct assignment (v2.documents_countable = documents_countable || blast_syncable) also manually preserves the invariant correctly without going through the setter.

There's no concrete current-SHA bug here — the getter provides a second layer of invariant enforcement. I'll withdraw this concern.


✏️ Learnings added
Learnt from: thepastaclaw
Repo: dashpay/platform PR: 3457
File: packages/rs-dpp/src/data_contract/document_type/v2/mod.rs:71-75
Timestamp: 2026-04-08T12:17:18.923Z
Learning: In `packages/rs-dpp/src/data_contract/document_type/v2/`, the `documents_countable` and `blast_syncable` fields on `DocumentTypeV2` are `pub(in crate::data_contract)`. This is intentional. The getter `documents_countable()` returns `self.documents_countable || self.blast_syncable`, which absorbs any field-level inconsistency. All production callers (including drive's `insert_contract`) use the getter, not the raw field. Direct field writes are valid within `crate::data_contract` and are used in both `try_from_schema/v2` and tests; the getter ensures observable correctness even when the setter is bypassed. No tighter visibility restriction is required.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: QuantumExplorer
Repo: dashpay/platform PR: 3457
File: packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs:64-79
Timestamp: 2026-04-08T09:53:34.375Z
Learning: In `packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema`, the v1 parser intentionally ignores the `documentsCountable` field because it has no field to parse it into. The v2 parser handles `documentsCountable` explicitly. Parser-version selection (controlled by `try_from_schema` version in the platform version constant, e.g. `CONTRACT_VERSIONS_V4` for protocol v12+) is the gate that prevents older parsers from producing a `DocumentTypeV2` with `documents_countable: true`, so no explicit rejection in v1 is needed.

Comment thread packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs Outdated
@QuantumExplorer QuantumExplorer force-pushed the feat/documents-countable branch 2 times, most recently from 9e4cd7c to 4fa6850 Compare April 8, 2026 11:01
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

I validated the current SHA from the completed Codex review outputs plus direct code inspection. Two consensus-critical problems remain: contract updates still create newly added countable / BLAST document types with a plain NormalTree, and several stateless query / fee-estimation paths still hard-code TreeType::NormalTree even when execution now uses CountTree / ProvableCountTree. The other live CodeRabbit threads were either already fixed or already retracted in-thread on the current SHA.

I validated existing CodeRabbit comments separately in-thread instead of restating them here.

Reviewed commit: 4fa6850

🔴 1 blocking

1 additional finding

🔴 blocking: Stateless query and fee-estimation paths still underprice countable / BLAST document trees as `NormalTree`

packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs (lines 44-45)

Primary document storage now executes on CountTree / ProvableCountTree for countable or BLAST-syncable document types, but related stateless paths still hard-code TreeType::NormalTree. The affected code includes insert-time existence checks here, delete-time reads in packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs, delete-cost estimation in packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs, and add-cost estimation in packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs. Because CountTree/ProvableCountTree maintain additional count/provable metadata, pricing these operations as NormalTree creates a consensus-accepted undercharge for attacker-chosen document types that enable the new flags.

🤖 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/drive/document/insert/add_document_for_contract_operations/v0/mod.rs`:
- [BLOCKING] lines 44-45: Stateless query and fee-estimation paths still underprice countable / BLAST document trees as `NormalTree`
  Primary document storage now executes on `CountTree` / `ProvableCountTree` for countable or BLAST-syncable document types, but related stateless paths still hard-code `TreeType::NormalTree`. The affected code includes insert-time existence checks here, delete-time reads in `packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs`, delete-cost estimation in `packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs`, and add-cost estimation in `packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs`. Because CountTree/ProvableCountTree maintain additional count/provable metadata, pricing these operations as NormalTree creates a consensus-accepted undercharge for attacker-chosen document types that enable the new flags.

@QuantumExplorer QuantumExplorer force-pushed the feat/documents-countable branch from 4fa6850 to 0568dd4 Compare April 8, 2026 16:21
},
"documentsCountable": {
"type": "boolean",
"description": "When true, the primary key tree uses a CountTree enabling O(1) total document count queries"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't it allow more than that? Wouldn't it allow counts where some indexed property is something? What is the primary key if something has multiple indexes? Would it be better to allow selecting an individual index as countable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is for primary key only, we already did another PR for indexes (which might need fixing).

Comment on lines +67 to +70
let primary_key_tree_type = if document_type.blast_syncable() {
TreeType::ProvableCountTree
} else if document_type.documents_countable() {
TreeType::CountTree
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these differences explained somewhere? I don't think I realized the counts for normal count trees aren't provable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The count is provable. But sub counts are not. Sub counts are only important in blast sync and basically don't give meaningful information except for BLAST sync.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs (1)

125-148: ⚠️ Potential issue | 🟠 Major

Keep the per-document history subtree as NormalTree.

At Line 145 this branch is creating the document_id subtree under primary_key_path, but it passes primary_key_tree_type as the type of the new subtree. For history-enabled types, the count/provable-count tree needs to stay on primary_key_path so it counts document IDs; the child document_id bucket should remain NormalTree. The estimator added in packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs already models that split, so the runtime path is diverging from the estimated layout.

🔧 Suggested adjustment
             let apply_type = if estimated_costs_only_with_layer_info.is_none() {
                 BatchInsertTreeApplyType::StatefulBatchInsertTree
             } else {
                 BatchInsertTreeApplyType::StatelessBatchInsertTree {
-                    in_tree_type: TreeType::NormalTree,
-                    tree_type: primary_key_tree_type,
+                    in_tree_type: primary_key_tree_type,
+                    tree_type: TreeType::NormalTree,
                     flags_len: storage_flags
                         .map(|s| s.serialized_size())
                         .unwrap_or_default(),
                 }
             };
             // we first insert an empty tree if the document is new
             self.batch_insert_empty_tree_if_not_exists(
                 path_key_info,
-                primary_key_tree_type,
+                TreeType::NormalTree,
                 storage_flags,
                 apply_type,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs`
around lines 125 - 148, The code is creating the per-document (document_id)
subtree using primary_key_tree_type, which makes history-enabled document types
get Count/ProvableCount trees under each document_id; instead, ensure the child
document_id subtree is always created as TreeType::NormalTree. Update the call
to batch_insert_empty_tree_if_not_exists (and any place where a new document_id
subtree type is passed) to use TreeType::NormalTree for the document_id subtree
while leaving primary_key_tree_type unchanged for the parent primary_key_path;
keep the existing apply_type/flags logic intact so the estimator and runtime
layout match.
🧹 Nitpick comments (1)
packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs (1)

73-79: Consider centralizing primary-key tree selection.

This blast_syncable()/documents_countable()TreeType mapping now exists in multiple insert/update/estimation paths. A small helper on the document-type or drive side would make the storage-layout rule single-source and reduce drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs`
around lines 73 - 79, The primary-key TreeType selection logic (the conditional
using document_type.blast_syncable(), document_type.documents_countable() to set
primary_key_tree_type to TreeType::ProvableCountTree / TreeType::CountTree /
TreeType::NormalTree) is duplicated across insert/update/estimation code paths;
create a single helper method (e.g., DocumentType::primary_key_tree_type() or
Drive::primary_key_tree_type_for(document_type)) that encapsulates this mapping
and replace the inline conditional in
add_estimation_costs_for_add_document_to_primary_storage::v0::mod.rs (and other
insert/update/estimation modules) to call that helper, keeping the unique
symbols document_type, blast_syncable(), documents_countable(),
primary_key_tree_type, and TreeType as the mapping contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs`:
- Around line 125-148: The code is creating the per-document (document_id)
subtree using primary_key_tree_type, which makes history-enabled document types
get Count/ProvableCount trees under each document_id; instead, ensure the child
document_id subtree is always created as TreeType::NormalTree. Update the call
to batch_insert_empty_tree_if_not_exists (and any place where a new document_id
subtree type is passed) to use TreeType::NormalTree for the document_id subtree
while leaving primary_key_tree_type unchanged for the parent primary_key_path;
keep the existing apply_type/flags logic intact so the estimator and runtime
layout match.

---

Nitpick comments:
In
`@packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs`:
- Around line 73-79: The primary-key TreeType selection logic (the conditional
using document_type.blast_syncable(), document_type.documents_countable() to set
primary_key_tree_type to TreeType::ProvableCountTree / TreeType::CountTree /
TreeType::NormalTree) is duplicated across insert/update/estimation code paths;
create a single helper method (e.g., DocumentType::primary_key_tree_type() or
Drive::primary_key_tree_type_for(document_type)) that encapsulates this mapping
and replace the inline conditional in
add_estimation_costs_for_add_document_to_primary_storage::v0::mod.rs (and other
insert/update/estimation modules) to call that helper, keeping the unique
symbols document_type, blast_syncable(), documents_countable(),
primary_key_tree_type, and TreeType as the mapping contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57e10670-6fbf-4eda-af45-176e7a525d52

📥 Commits

Reviewing files that changed from the base of the PR and between 1e21664 and 0568dd4.

📒 Files selected for processing (21)
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/fees/op.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/v12.rs
✅ Files skipped from review due to trivial changes (8)
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-platform-version/src/version/v12.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs

@QuantumExplorer
Copy link
Copy Markdown
Member Author

@thepastaclaw are things good now?

@thepastaclaw
Copy link
Copy Markdown
Collaborator

Not quite yet. I re-checked the current head (0568dd49) and the earlier update-path fix landed, but a few tree-type mismatches are still live on the stateless side:

  • packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs still does the stateless grove_has_raw existence check with in_tree_type: TreeType::NormalTree.
  • packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs still does the stateless grove_get_raw read with in_tree_type: TreeType::NormalTree.
  • packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v0/mod.rs still does stateless batch delete with in_tree_type: TreeType::NormalTree.

So countable / BLAST document types are still being modeled as NormalTree in some consensus-relevant stateless query / fee paths.

There is also still the history-layout mismatch in packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs: the per-document child subtree is still created with primary_key_tree_type, but that child tree should stay NormalTree even when the parent primary-key layer is CountTree / ProvableCountTree.

So my answer is: better, but not good yet. I’d want one more push that fixes those remaining paths before clearing this.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The earlier storage-layout bugs around contract update and the dedicated estimation helpers are fixed, but this SHA still has three merge-blocking gaps in the new tree-type rollout. Pre-v12 contracts can already carry the new schema keys even though older parsers ignore them, and the remaining insert paths still mis-handle CountTree/ProvableCountTree semantics in ways that keep stateless costing and stateful execution out of sync.

Reviewed commit: 0568dd4

🔴 3 blocking

1 additional finding

🔴 blocking: Stateless non-history inserts still price count/provable primary storage as `NormalTree`

packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs (lines 445-452)

The non-history batch_insert_if_not_exists path still hard-codes in_tree_type: TreeType::NormalTree even though this PR now creates the primary key tree as CountTree or ProvableCountTree for documents_countable / blast_syncable document types. That keeps fee estimation and dry-run validation on the cheaper tree variant while execution writes into the heavier count/provable tree, so flagged document types can still be accepted with underpriced insert costs. The dedicated estimation helper was fixed earlier in the patch, but this inline stateless insert path is still out of sync with the actual storage layout.

💡 Suggested change
            let primary_key_tree_type = if document_type.blast_syncable() {
                TreeType::ProvableCountTree
            } else if document_type.documents_countable() {
                TreeType::CountTree
            } else {
                TreeType::NormalTree
            };
            let apply_type = if estimated_costs_only_with_layer_info.is_none() {
                BatchInsertApplyType::StatefulBatchInsert
            } else {
                BatchInsertApplyType::StatelessBatchInsert {
                    in_tree_type: primary_key_tree_type,
                    target: QueryTargetValue(document_type.estimated_size(platform_version)? as u32),
                }
            };
🤖 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/schema/meta_schemas/document/v0/document-meta.json`:
- [BLOCKING] lines 508-514: Pre-v12 contracts can smuggle in tree-type flags that activate after the v12 parser switch
  These keys are now accepted by the shared document meta-schema for every protocol version, but pre-v12 parsing still dispatches to the older document-type parsers that validate against this schema and then ignore the fields. Because contracts serialize the raw document schemas and reparse them on deserialization with the current `platform_version`, a contract accepted before v12 can later be reloaded after upgrade with `documentsCountable` / `blastSyncable` suddenly taking effect even though its primary tree was originally created as a normal tree. That creates a storage-layout invariant break across protocol activation and can surface later as fee/proof divergence or block-processing failures for document types that were accepted too early.

In `packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs`:
- [BLOCKING] lines 136-147: Keep-history inserts swap the parent and child tree types
  In the `documents_keep_history()` branch, the stateless apply type still treats the parent as `NormalTree` and the child as `primary_key_tree_type`, and the stateful insert creates the per-document history subtree with `primary_key_tree_type` as well. Those are backwards: the parent being inserted into is the primary key tree, which can now be `CountTree` / `ProvableCountTree`, while the per-document history subtree should remain a plain `NormalTree`. As written, stateful execution builds the wrong subtree type and stateless costing models both sides of the insert against the wrong tree variants, so history-enabled countable/BLAST document types no longer preserve a consistent storage-layout/cost invariant.
- [BLOCKING] lines 445-452: Stateless non-history inserts still price count/provable primary storage as `NormalTree`
  The non-history `batch_insert_if_not_exists` path still hard-codes `in_tree_type: TreeType::NormalTree` even though this PR now creates the primary key tree as `CountTree` or `ProvableCountTree` for `documents_countable` / `blast_syncable` document types. That keeps fee estimation and dry-run validation on the cheaper tree variant while execution writes into the heavier count/provable tree, so flagged document types can still be accepted with underpriced insert costs. The dedicated estimation helper was fixed earlier in the patch, but this inline stateless insert path is still out of sync with the actual storage layout.

Comment thread packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json Outdated
@QuantumExplorer QuantumExplorer force-pushed the feat/documents-countable branch from 0568dd4 to 5adbe0d Compare April 8, 2026 22:42
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs (1)

40-49: Consider centralizing primary-key tree selection.

This blast_syncable / documents_countable / NormalTree mapping now exists in several insert/delete/estimation paths. One missed call site already caused drift in this PR, so a shared helper would make future consensus-layout changes harder to miss.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs`
around lines 40 - 49, Extract the repeated mapping logic
(document_type.blast_syncable -> TreeType::ProvableCountTree,
document_type.documents_countable -> TreeType::CountTree, else ->
TreeType::NormalTree) into a single helper function (e.g., fn
primary_key_tree_type_for(document_type: &DocumentType) -> TreeType) and replace
the inline logic in add_document_for_contract_operations::v0 (the block that
sets primary_key_tree_type) and all other insert/delete/estimation call sites
with calls to that helper; ensure the helper lives in a shared module so all
consumers (insert, delete, estimation paths) import and use it to avoid drift.
packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs (1)

667-941: Add a V2-backed accessor test.

These new V2 branches are not exercised by this module’s tests; the current fixture coverage is still V0-oriented, so regressions in token-cost delegation or documents_countable / blast_syncable dispatch would slip through. Please add at least one unit test that constructs a DocumentType::V2 and checks owned/ref/mut-ref accessors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs` around
lines 667 - 941, Add a unit test that constructs a DocumentType::V2 instance
(with deterministic values for token costs and
documents_countable/blast_syncable), then verify the V2-tail dispatch for owned,
reference, and mutable-reference variants: call DocumentTypeV1Getters methods
(document_creation_token_cost, document_replacement_token_cost,
document_deletion_token_cost, document_transfer_token_cost,
document_update_price_token_cost, document_purchase_token_cost,
all_document_token_costs, all_external_token_costs_contract_tokens) and
DocumentTypeV2Getters methods (documents_countable, blast_syncable) on
DocumentType::V2, DocumentTypeRef::V2, and DocumentTypeMutRef::V2 and assert
they return the expected values from the underlying V2 struct; place the test
alongside existing tests for this module so V2 branches are exercised.
packages/rs-dpp/src/data_contract/document_type/v2/mod.rs (2)

108-140: Consider collapsing the duplicated V0/V1→V2 conversions.

These two From impls copy the same field list almost verbatim, which makes future DocumentTypeV2 additions easy to miss in one path. A small shared constructor/helper with token_costs as the only varying input would make this less brittle.

Also applies to: 142-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/src/data_contract/document_type/v2/mod.rs` around lines 108 -
140, The two From impls for DocumentTypeV2 (From<DocumentTypeV0> and
From<DocumentTypeV1>) duplicate the same field mapping; extract a shared
constructor/helper (e.g., DocumentTypeV2::from_common or a private fn
build_from_fields) that accepts a source struct (or common fields) and a
TokenCosts value, move the repeated field assignments (name, schema, indices,
index_structure, flattened_properties, properties, identifier_paths,
binary_paths, required_fields, transient_fields, documents_keep_history,
documents_mutable, documents_can_be_deleted, documents_transferable, trade_mode,
creation_restriction_mode, data_contract_id,
requires_identity_encryption_bounded_key,
requires_identity_decryption_bounded_key, security_level_requirement,
#[cfg(feature="validation")] json_schema_validator, documents_countable,
blast_syncable) into that helper, and then have both From<DocumentTypeV0> and
From<DocumentTypeV1> call it passing the appropriate
TokenCosts::V0(Default::default()) or TokenCosts::V1(value) so only the
token_costs difference remains in each From impl.

185-195: Pin the test helper to a fixed platform version.

Using PlatformVersion::latest() here makes these unit tests vulnerable to unrelated protocol-profile changes. A concrete test version would keep the V0/V1→V2 conversion coverage stable over time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/src/data_contract/document_type/v2/mod.rs` around lines 185 -
195, The test helper make_v0 currently calls IndexLevel::try_from_indices with
PlatformVersion::latest(), which makes the unit test brittle; replace
PlatformVersion::latest() with a pinned, concrete platform version used for
tests (e.g., the specific PlatformVersion variant/constructor your test-suite
uses) so DocumentTypeV0 -> V2 conversion remains stable; update the call in
make_v0 (and any similar helpers) to pass that fixed PlatformVersion instead of
PlatformVersion::latest(), using the existing test constant or explicit version
constructor used elsewhere in the codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs`:
- Around line 666-696: The new validation block in try_from_schema v1 (the if
full_validation branch that calls Value::inner_optional_bool_value on
property_names::DOCUMENTS_COUNTABLE and property_names::BLAST_SYNCABLE and
returns ProtocolError::ConsensusError/UnsupportedFeatureError) must be removed
or disabled so V1 stays tolerant: V1 should ignore V2-only flags instead of
failing; revert the change that inspects documentsCountable/blastSyncable and
the associated error path (ProtocolError::ConsensusError /
UnsupportedFeatureError) and ensure routing of protocol_version >= 12 schemas to
the V2 parser remains the mechanism that enforces those features, leaving
try_from_schema v1 to simply skip/map only fields present in the V1 output type
(do not rely on full_validation to enforce this).

---

Nitpick comments:
In `@packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs`:
- Around line 667-941: Add a unit test that constructs a DocumentType::V2
instance (with deterministic values for token costs and
documents_countable/blast_syncable), then verify the V2-tail dispatch for owned,
reference, and mutable-reference variants: call DocumentTypeV1Getters methods
(document_creation_token_cost, document_replacement_token_cost,
document_deletion_token_cost, document_transfer_token_cost,
document_update_price_token_cost, document_purchase_token_cost,
all_document_token_costs, all_external_token_costs_contract_tokens) and
DocumentTypeV2Getters methods (documents_countable, blast_syncable) on
DocumentType::V2, DocumentTypeRef::V2, and DocumentTypeMutRef::V2 and assert
they return the expected values from the underlying V2 struct; place the test
alongside existing tests for this module so V2 branches are exercised.

In `@packages/rs-dpp/src/data_contract/document_type/v2/mod.rs`:
- Around line 108-140: The two From impls for DocumentTypeV2
(From<DocumentTypeV0> and From<DocumentTypeV1>) duplicate the same field
mapping; extract a shared constructor/helper (e.g., DocumentTypeV2::from_common
or a private fn build_from_fields) that accepts a source struct (or common
fields) and a TokenCosts value, move the repeated field assignments (name,
schema, indices, index_structure, flattened_properties, properties,
identifier_paths, binary_paths, required_fields, transient_fields,
documents_keep_history, documents_mutable, documents_can_be_deleted,
documents_transferable, trade_mode, creation_restriction_mode, data_contract_id,
requires_identity_encryption_bounded_key,
requires_identity_decryption_bounded_key, security_level_requirement,
#[cfg(feature="validation")] json_schema_validator, documents_countable,
blast_syncable) into that helper, and then have both From<DocumentTypeV0> and
From<DocumentTypeV1> call it passing the appropriate
TokenCosts::V0(Default::default()) or TokenCosts::V1(value) so only the
token_costs difference remains in each From impl.
- Around line 185-195: The test helper make_v0 currently calls
IndexLevel::try_from_indices with PlatformVersion::latest(), which makes the
unit test brittle; replace PlatformVersion::latest() with a pinned, concrete
platform version used for tests (e.g., the specific PlatformVersion
variant/constructor your test-suite uses) so DocumentTypeV0 -> V2 conversion
remains stable; update the call in make_v0 (and any similar helpers) to pass
that fixed PlatformVersion instead of PlatformVersion::latest(), using the
existing test constant or explicit version constructor used elsewhere in the
codebase.

In
`@packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs`:
- Around line 40-49: Extract the repeated mapping logic
(document_type.blast_syncable -> TreeType::ProvableCountTree,
document_type.documents_countable -> TreeType::CountTree, else ->
TreeType::NormalTree) into a single helper function (e.g., fn
primary_key_tree_type_for(document_type: &DocumentType) -> TreeType) and replace
the inline logic in add_document_for_contract_operations::v0 (the block that
sets primary_key_tree_type) and all other insert/delete/estimation call sites
with calls to that helper; ensure the helper lives in a shared module so all
consumers (insert, delete, estimation paths) import and use it to avoid drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f4801f72-3c6d-4969-ba1c-d018958e7213

📥 Commits

Reviewing files that changed from the base of the PR and between 0568dd4 and 5adbe0d.

📒 Files selected for processing (25)
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs
  • packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/fees/op.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/v12.rs
✅ Files skipped from review due to trivial changes (3)
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs
  • packages/rs-drive/src/fees/op.rs
  • packages/rs-platform-version/src/version/v12.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs

@thepastaclaw
Copy link
Copy Markdown
Collaborator

Re-checked the current head (5adbe0d9) and the blockers I called out earlier do look fixed now.

What changed on the paths I was worried about:

  • the stateless insert/delete/remove paths now pick the primary-key tree type from documentsCountable / blastSyncable instead of hard-coding NormalTree
  • the per-document child subtree under primary storage stays NormalTree, so the history layout mismatch is gone
  • the pre-v12 parser path now rejects documentsCountable / blastSyncable, and validate_update now rejects flipping either flag later

So from my side: yes, things look good now.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This force-push fixes the earlier CountTree/ProvableCountTree insertion/costing mismatches, but two merge-blocking contract-parsing bugs remain. First, protocol v12 routes document-type parsing through the new v2 parser, yet that parser forwards documentsCountable / blastSyncable straight into the v1 parser with full_validation intact, so full validation rejects the very fields this PR introduces and contract creation cannot use the feature. Second, the new pre-v12 rejection only runs during full validation; contracts already accepted into state before this fix can still be deserialized after the v12 parser switch with those flags taking effect even though their primary storage trees were originally created as normal trees. I also confirmed a smaller coverage gap: the new immutability checks in validate_update do not have matching unit tests like the neighboring config invariants do.

Reviewed commit: 5adbe0d

🔴 2 blocking | 🟡 1 suggestion(s)

🤖 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/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs`:
- [BLOCKING] lines 57-70: V2 document-type parsing forwards the new flags into the V1 rejection path
  Protocol v12 switches `document_type.try_from_schema` to version 2, but `DocumentTypeV2::try_from_schema()` only reads `documentsCountable` / `blastSyncable` and then calls `DocumentTypeV1::try_from_schema()` with the original `schema` and the same `full_validation` flag. The new v1 guard at `try_from_schema/v1/mod.rs:670-695` rejects either field whenever full validation is enabled, so any contract-create or contract-update flow that parses a v12 schema with one of these flags set will raise `UnsupportedFeatureError` before the v2 wrapper can ever reattach the values. In other words, the feature is dead on arrival in the production validation path. Strip the two v2-only keys before delegating to v1 (or explicitly bypass that v1 guard from the v2 entry point) so v12 contracts can actually create count/provable document types.

In `packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs`:
- [BLOCKING] lines 670-695: The pre-v12 flag rejection still lets already-stored contracts mutate meaning after the v12 parser switch
  The new `documentsCountable` / `blastSyncable` rejection only runs when `full_validation` is true. Contracts read back from Drive use `DataContract::versioned_deserialize(..., false, ...)`, which forwards `full_validation = false` through `DataContractV1::try_from_platform_versioned_v1()` into `create_document_types_from_document_schemas()` and then `DocumentType::try_from_schema()`. On a v12 node that deserialization path now uses the v2 parser, so any contract that was previously accepted into chain state with these raw schema keys present will be reloaded as a count/provable document type even though its primary key tree was originally created as a normal tree. The top-level document meta-schema on the base branch did not forbid unknown document-type properties, so this is not just hypothetical future input: old state can already contain the latent keys. The new guard prevents fresh pre-v12 submissions, but it does not protect existing chain data from changing storage semantics after upgrade. The fix needs to sanitize or reject those fields on deserialization as well, not only during full-validation submission paths.

In `packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs`:
- [SUGGESTION] lines 183-211: The new countable/blast immutability checks are missing the companion tests the neighboring invariants already have
  `validate_config()` now rejects changes to `documents_countable()` and `blast_syncable()`, which is the right invariant because changing those flags after creation would change the primary storage tree shape. However, the test module below still only covers the older config immutability checks (transferable, can_be_deleted, keep_history, mutable, encryption/decryption key requirements, security level, etc.). A quick grep in this file shows no test exercising the new countable/blast branches. Given that these guards protect storage-layout stability, they deserve the same regression coverage as the surrounding checks.

@QuantumExplorer QuantumExplorer force-pushed the feat/documents-countable branch from 5adbe0d to b1064f8 Compare April 9, 2026 06:21
@thepastaclaw
Copy link
Copy Markdown
Collaborator

Re-checked the current head (b1064f8f). One of my earlier parser blockers is fixed now: the v12 create/update path no longer dead-ends in the v1 rejection path.

But I still do not think this is fully good yet.

The remaining problem is the pre-v12 deserialization path. DataContract::versioned_deserialize(..., false, ...) still reparses document schemas using the current platform_version, and on v12 that dispatches DocumentType::try_from_schema() to the new v2 parser. Meanwhile the shared document meta-schema still does not globally forbid extra top-level keys, so a contract accepted before v12 could already carry raw documentsCountable / blastSyncable fields that the old parser ignored. After upgrade, that same serialized schema would be reloaded as countable / BLAST even though the primary key tree was originally created as a normal tree.

So from my side the answer is: not quite yet — I still want that old-state migration / sanitize / rejection hole closed before I clear it.

@QuantumExplorer QuantumExplorer force-pushed the feat/documents-countable branch from b1064f8 to cc855a8 Compare April 9, 2026 07:40
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json (1)

508-515: ⚠️ Potential issue | 🔴 Critical

Blocking: protocol-agnostic schema acceptance still allows pre-v12 flag smuggling.

Line 508-515 makes these keys valid in the shared document meta-schema for all versions. Without a hard pre-v12 sanitize/reject path during deserialization/reparse, legacy contracts can be interpreted post-upgrade with different primary-tree semantics than originally materialized.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json` around
lines 508 - 515, The schema currently exposes documentsCountable and
blastSyncable as valid keys for all versions which allows pre-v12 contracts to
smuggle new primary-tree flags; update the validation flow so those keys are
only accepted when protocolVersion >= 12: remove or hide documentsCountable and
blastSyncable from the shared, protocol-agnostic document meta acceptance and
instead validate them in the v12+ document meta validator (or add an explicit
check in the document meta deserialization/reparse path) that rejects or strips
these fields for protocolVersion < 12; ensure the deserializer/reparser path
that handles document meta enforces this reject/strip behavior so legacy
contracts cannot be interpreted with v12 primary-tree semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json`:
- Around line 508-515: The schema currently exposes documentsCountable and
blastSyncable as valid keys for all versions which allows pre-v12 contracts to
smuggle new primary-tree flags; update the validation flow so those keys are
only accepted when protocolVersion >= 12: remove or hide documentsCountable and
blastSyncable from the shared, protocol-agnostic document meta acceptance and
instead validate them in the v12+ document meta validator (or add an explicit
check in the document meta deserialization/reparse path) that rejects or strips
these fields for protocolVersion < 12; ensure the deserializer/reparser path
that handles document meta enforces this reject/strip behavior so legacy
contracts cannot be interpreted with v12 primary-tree semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66bc0296-6d90-4e7e-856b-f361772932a2

📥 Commits

Reviewing files that changed from the base of the PR and between b1064f8 and cc855a8.

📒 Files selected for processing (27)
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs
  • packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/mod.rs
  • packages/rs-drive/src/drive/document/primary_key_tree_type.rs
  • packages/rs-drive/src/fees/op.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/v12.rs
✅ Files skipped from review due to trivial changes (5)
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-drive/src/fees/op.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs

@thepastaclaw
Copy link
Copy Markdown
Collaborator

Re-checked the current head (cc855a8b) and I still do not think this is fully good yet.

The remaining blocker is the same old-state/deserialization hole, and it still reproduces on the current SHA:

  • DataContract::versioned_deserialize(..., false, ...) is still used on state-read paths (for example rs-drive/src/verify/contract/verify_contract/v0/mod.rs, rs-drive/src/drive/contract/get_fetch/fetch_contract_with_history/v0/mod.rs, and rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs).
  • That deserialization path still reparses the stored raw document_schemas with the current platform_version (DataContractV1::try_from_platform_versioned_v1() -> create_document_types_from_document_schemas() -> DocumentType::try_from_schema()). On protocol v12 that now dispatches to the new v2 parser.
  • The shared document meta-schema still allows extra top-level keys in practice: it requires the schema field "additionalProperties": false, but it does not set root-level additionalProperties: false. So a pre-v12 contract could already have raw documentsCountable / blastSyncable keys stored in state while the old parser ignored them.
  • After upgrade, the v2 parser reads those latent keys and can reinterpret that stored document type as countable / BLAST even though its primary key tree was originally created as a normal tree.

So from my side the answer is still: not good yet — I still want that deserialization / old-state migration hole closed before I clear this.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This force-push fixes the previous dead-on-arrival parser bug: v12 contracts can now actually parse documentsCountable / blastSyncable, and the Drive insert/delete/cost paths consistently derive the matching tree type. But the replacement change removed the pre-v12 rejection entirely, which reopens a consensus/storage-layout problem across upgrades: protocol-11 contracts can now persist those keys in raw schema JSON, be created over a NormalTree, and then get reinterpreted after a v12 upgrade as CountTree / ProvableCountTree document types when the stored contract is deserialized with the current platform version. I also confirmed the new immutability guards still have no direct regression tests.

Reviewed commit: b1064f8

🔴 1 blocking | 🟡 1 suggestion(s)

🤖 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/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs`:
- [BLOCKING] lines 666-670: Relaxing the V1 parser reopens pre-v12 schema smuggling for countable document types
  These lines now intentionally ignore `documentsCountable` / `blastSyncable` in the v1 parser. That is unsafe because the shared document meta-schema already allows both keys (`packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json:508-515`), and pre-v12 platform versions still dispatch `DocumentType::try_from_schema()` to the v1 parser (`packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs:35`, `v3.rs:37`). A protocol-11 contract can therefore submit those raw schema fields, pass validation, and get stored with a `DocumentTypeV1` that creates a `NormalTree` at contract insertion time (`packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs:287-307`). Later, Drive fetches stored contracts by deserializing them with the *current* platform version (`packages/rs-drive/src/drive/contract/get_fetch/fetch_contract/v0/mod.rs:81-85`), and that reconstruction path calls `create_document_types_from_document_schemas()` / `DocumentType::try_from_schema()` with the current version (`packages/rs-dpp/src/data_contract/v1/serialization/mod.rs:154-165`). After a network upgrade to v12, the same stored raw schema is reparsed through the v2 parser, which now reads those keys and makes document operations use `CountTree` / `ProvableCountTree` semantics (`packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs:71-77`) even though the underlying tree was created as `NormalTree`. That is a consensus-critical storage-layout mismatch across upgrades. The v1 path still needs to reject or normalize these keys for pre-v12 contracts instead of silently preserving them in serialized state.

In `packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs`:
- [SUGGESTION] lines 183-210: The new countable/blast immutability guards still have no direct regression tests
  `validate_config()` now correctly rejects changes to `documents_countable` and `blast_syncable`, which is the guard that prevents an update from changing the primary tree type after creation. But the surrounding test module still only covers the older immutability rules (creation restriction, trade mode, transferable, deletable, keep-history, mutable, bounded-key requirements, security level, etc.). There is still no focused test that builds two v2 document types differing only in `documentsCountable` or `blastSyncable` and asserts `DocumentTypeUpdateError` is returned. Because these flags affect storage/proof layout, they deserve the same regression coverage as the neighboring config invariants.

Comment on lines +666 to +670
// Note: documentsCountable / blastSyncable schema keys are intentionally
// ignored here. The v1 parser produces DocumentTypeV1 which has no countable
// fields. When protocol v12+ is active, the v2 parser is used instead, which
// reads these keys and produces DocumentTypeV2. The v1 parser should never
// reject unknown keys — it simply doesn't map them to its output type.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Relaxing the V1 parser reopens pre-v12 schema smuggling for countable document types

These lines now intentionally ignore documentsCountable / blastSyncable in the v1 parser. That is unsafe because the shared document meta-schema already allows both keys (packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json:508-515), and pre-v12 platform versions still dispatch DocumentType::try_from_schema() to the v1 parser (packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs:35, v3.rs:37). A protocol-11 contract can therefore submit those raw schema fields, pass validation, and get stored with a DocumentTypeV1 that creates a NormalTree at contract insertion time (packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs:287-307). Later, Drive fetches stored contracts by deserializing them with the current platform version (packages/rs-drive/src/drive/contract/get_fetch/fetch_contract/v0/mod.rs:81-85), and that reconstruction path calls create_document_types_from_document_schemas() / DocumentType::try_from_schema() with the current version (packages/rs-dpp/src/data_contract/v1/serialization/mod.rs:154-165). After a network upgrade to v12, the same stored raw schema is reparsed through the v2 parser, which now reads those keys and makes document operations use CountTree / ProvableCountTree semantics (packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs:71-77) even though the underlying tree was created as NormalTree. That is a consensus-critical storage-layout mismatch across upgrades. The v1 path still needs to reject or normalize these keys for pre-v12 contracts instead of silently preserving them in serialized state.

source: ['codex-general', 'codex-security-auditor']

🤖 Fix this 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/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs`:
- [BLOCKING] lines 666-670: Relaxing the V1 parser reopens pre-v12 schema smuggling for countable document types
  These lines now intentionally ignore `documentsCountable` / `blastSyncable` in the v1 parser. That is unsafe because the shared document meta-schema already allows both keys (`packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json:508-515`), and pre-v12 platform versions still dispatch `DocumentType::try_from_schema()` to the v1 parser (`packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs:35`, `v3.rs:37`). A protocol-11 contract can therefore submit those raw schema fields, pass validation, and get stored with a `DocumentTypeV1` that creates a `NormalTree` at contract insertion time (`packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs:287-307`). Later, Drive fetches stored contracts by deserializing them with the *current* platform version (`packages/rs-drive/src/drive/contract/get_fetch/fetch_contract/v0/mod.rs:81-85`), and that reconstruction path calls `create_document_types_from_document_schemas()` / `DocumentType::try_from_schema()` with the current version (`packages/rs-dpp/src/data_contract/v1/serialization/mod.rs:154-165`). After a network upgrade to v12, the same stored raw schema is reparsed through the v2 parser, which now reads those keys and makes document operations use `CountTree` / `ProvableCountTree` semantics (`packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs:71-77`) even though the underlying tree was created as `NormalTree`. That is a consensus-critical storage-layout mismatch across upgrades. The v1 path still needs to reject or normalize these keys for pre-v12 contracts instead of silently preserving them in serialized state.

Comment on lines +183 to +210
if new_document_type.documents_countable() != self.documents_countable() {
return SimpleConsensusValidationResult::new_with_error(
DocumentTypeUpdateError::new(
self.data_contract_id(),
self.name(),
format!(
"document type can not change whether its documents are countable: changing from {} to {}",
self.documents_countable(),
new_document_type.documents_countable()
),
)
.into(),
);
}

if new_document_type.blast_syncable() != self.blast_syncable() {
return SimpleConsensusValidationResult::new_with_error(
DocumentTypeUpdateError::new(
self.data_contract_id(),
self.name(),
format!(
"document type can not change whether it is blast syncable: changing from {} to {}",
self.blast_syncable(),
new_document_type.blast_syncable()
),
)
.into(),
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: The new countable/blast immutability guards still have no direct regression tests

validate_config() now correctly rejects changes to documents_countable and blast_syncable, which is the guard that prevents an update from changing the primary tree type after creation. But the surrounding test module still only covers the older immutability rules (creation restriction, trade mode, transferable, deletable, keep-history, mutable, bounded-key requirements, security level, etc.). There is still no focused test that builds two v2 document types differing only in documentsCountable or blastSyncable and asserts DocumentTypeUpdateError is returned. Because these flags affect storage/proof layout, they deserve the same regression coverage as the neighboring config invariants.

source: ['claude-general', 'claude-security-auditor', 'claude-rust-quality', 'codex-general', 'codex-rust-quality']

🤖 Fix this 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/data_contract/document_type/methods/validate_update/v0/mod.rs`:
- [SUGGESTION] lines 183-210: The new countable/blast immutability guards still have no direct regression tests
  `validate_config()` now correctly rejects changes to `documents_countable` and `blast_syncable`, which is the guard that prevents an update from changing the primary tree type after creation. But the surrounding test module still only covers the older immutability rules (creation restriction, trade mode, transferable, deletable, keep-history, mutable, bounded-key requirements, security level, etc.). There is still no focused test that builds two v2 document types differing only in `documentsCountable` or `blastSyncable` and asserts `DocumentTypeUpdateError` is returned. Because these flags affect storage/proof layout, they deserve the same regression coverage as the neighboring config invariants.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This force-push refactors rs-drive so inserts, deletes, and estimation paths all compute the primary-key tree type through one helper, which does remove some duplication. But it does not fix the actual blocker from the previous SHA: pre-v12 contracts can still persist documentsCountable / blastSyncable in raw schema JSON, and after a v12 upgrade those stored schemas are reparsed into count/provable document types even though the underlying tree was originally created as NormalTree. I also re-checked the non-blocking follow-up and the new immutability guards still lack direct regression tests in validate_update.

Reviewed commit: cc855a8

🔴 1 blocking | 🟡 1 suggestion(s)

🤖 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/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs`:
- [BLOCKING] lines 666-670: Pre-v12 contracts can still smuggle count-tree flags and be reinterpreted after upgrade
  The v1 parser still intentionally ignores `documentsCountable` / `blastSyncable` instead of rejecting or normalizing them, while the shared document meta-schema still allows those keys. That means a pre-v12 contract can carry the raw fields in `document_schemas`, pass validation as a `DocumentTypeV1`, and be inserted with a physical `NormalTree`. Later, stored contracts are reconstructed from serialized schema bytes with the current platform version (`packages/rs-dpp/src/data_contract/v1/serialization/mod.rs:154-165` and `packages/rs-drive/src/drive/contract/get_fetch/fetch_contract/v0/mod.rs:81-85`). Once the node is on protocol v12, that same raw schema is reparsed through `DocumentTypeV2::try_from_schema`, which revives the flags and makes `primary_key_tree_type` select `CountTree` / `ProvableCountTree` for runtime document operations (`packages/rs-drive/src/drive/document/primary_key_tree_type.rs:18-48`, plus the new call sites in the touched insert/delete/estimation files). So this refactor centralizes the tree selection, but it still centralizes the wrong assumption: old on-disk `NormalTree` contracts can be reinterpreted post-upgrade as count/provable trees. That is a storage-layout mismatch in consensus-critical code, and this SHA does not close it.

In `packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs`:
- [SUGGESTION] lines 183-210: The countable and blast-syncable immutability guards still have no dedicated tests
  `validate_config()` now rejects changes to `documents_countable` and `blast_syncable`, which is the right invariant because those flags determine the physical primary-key tree layout. But the test module in this file still only has explicit regression cases for the older immutable properties; a targeted search shows the new error strings at lines 189 and 204, yet there is still no corresponding test that builds two document types differing only in these flags and asserts `DocumentTypeUpdateError` is returned. Because these guards protect storage layout rather than a cosmetic schema field, they deserve the same direct coverage as the neighboring transferability/history/mutability/security-level checks.

Comment on lines +666 to +670
// Note: documentsCountable / blastSyncable schema keys are intentionally
// ignored here. The v1 parser produces DocumentTypeV1 which has no countable
// fields. When protocol v12+ is active, the v2 parser is used instead, which
// reads these keys and produces DocumentTypeV2. The v1 parser should never
// reject unknown keys — it simply doesn't map them to its output type.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Pre-v12 contracts can still smuggle count-tree flags and be reinterpreted after upgrade

The v1 parser still intentionally ignores documentsCountable / blastSyncable instead of rejecting or normalizing them, while the shared document meta-schema still allows those keys. That means a pre-v12 contract can carry the raw fields in document_schemas, pass validation as a DocumentTypeV1, and be inserted with a physical NormalTree. Later, stored contracts are reconstructed from serialized schema bytes with the current platform version (packages/rs-dpp/src/data_contract/v1/serialization/mod.rs:154-165 and packages/rs-drive/src/drive/contract/get_fetch/fetch_contract/v0/mod.rs:81-85). Once the node is on protocol v12, that same raw schema is reparsed through DocumentTypeV2::try_from_schema, which revives the flags and makes primary_key_tree_type select CountTree / ProvableCountTree for runtime document operations (packages/rs-drive/src/drive/document/primary_key_tree_type.rs:18-48, plus the new call sites in the touched insert/delete/estimation files). So this refactor centralizes the tree selection, but it still centralizes the wrong assumption: old on-disk NormalTree contracts can be reinterpreted post-upgrade as count/provable trees. That is a storage-layout mismatch in consensus-critical code, and this SHA does not close it.

source: ['codex-general', 'codex-security-auditor', 'codex-rust-quality']

🤖 Fix this 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/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs`:
- [BLOCKING] lines 666-670: Pre-v12 contracts can still smuggle count-tree flags and be reinterpreted after upgrade
  The v1 parser still intentionally ignores `documentsCountable` / `blastSyncable` instead of rejecting or normalizing them, while the shared document meta-schema still allows those keys. That means a pre-v12 contract can carry the raw fields in `document_schemas`, pass validation as a `DocumentTypeV1`, and be inserted with a physical `NormalTree`. Later, stored contracts are reconstructed from serialized schema bytes with the current platform version (`packages/rs-dpp/src/data_contract/v1/serialization/mod.rs:154-165` and `packages/rs-drive/src/drive/contract/get_fetch/fetch_contract/v0/mod.rs:81-85`). Once the node is on protocol v12, that same raw schema is reparsed through `DocumentTypeV2::try_from_schema`, which revives the flags and makes `primary_key_tree_type` select `CountTree` / `ProvableCountTree` for runtime document operations (`packages/rs-drive/src/drive/document/primary_key_tree_type.rs:18-48`, plus the new call sites in the touched insert/delete/estimation files). So this refactor centralizes the tree selection, but it still centralizes the wrong assumption: old on-disk `NormalTree` contracts can be reinterpreted post-upgrade as count/provable trees. That is a storage-layout mismatch in consensus-critical code, and this SHA does not close it.

Comment on lines +183 to +210
if new_document_type.documents_countable() != self.documents_countable() {
return SimpleConsensusValidationResult::new_with_error(
DocumentTypeUpdateError::new(
self.data_contract_id(),
self.name(),
format!(
"document type can not change whether its documents are countable: changing from {} to {}",
self.documents_countable(),
new_document_type.documents_countable()
),
)
.into(),
);
}

if new_document_type.blast_syncable() != self.blast_syncable() {
return SimpleConsensusValidationResult::new_with_error(
DocumentTypeUpdateError::new(
self.data_contract_id(),
self.name(),
format!(
"document type can not change whether it is blast syncable: changing from {} to {}",
self.blast_syncable(),
new_document_type.blast_syncable()
),
)
.into(),
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: The countable and blast-syncable immutability guards still have no dedicated tests

validate_config() now rejects changes to documents_countable and blast_syncable, which is the right invariant because those flags determine the physical primary-key tree layout. But the test module in this file still only has explicit regression cases for the older immutable properties; a targeted search shows the new error strings at lines 189 and 204, yet there is still no corresponding test that builds two document types differing only in these flags and asserts DocumentTypeUpdateError is returned. Because these guards protect storage layout rather than a cosmetic schema field, they deserve the same direct coverage as the neighboring transferability/history/mutability/security-level checks.

source: ['codex-general', 'codex-rust-quality']

🤖 Fix this 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/data_contract/document_type/methods/validate_update/v0/mod.rs`:
- [SUGGESTION] lines 183-210: The countable and blast-syncable immutability guards still have no dedicated tests
  `validate_config()` now rejects changes to `documents_countable` and `blast_syncable`, which is the right invariant because those flags determine the physical primary-key tree layout. But the test module in this file still only has explicit regression cases for the older immutable properties; a targeted search shows the new error strings at lines 189 and 204, yet there is still no corresponding test that builds two document types differing only in these flags and asserts `DocumentTypeUpdateError` is returned. Because these guards protect storage layout rather than a cosmetic schema field, they deserve the same direct coverage as the neighboring transferability/history/mutability/security-level checks.

Adds two new fields to DocumentTypeV2:

- `documents_countable: bool` — primary key tree uses CountTree for
  O(1) total document count queries
- `range_countable: bool` — primary key tree uses ProvableCountTree
  for provable range counting. Implies documents_countable = true.

Changes:
- DocumentTypeV2 struct with both fields
- V2 variant added to DocumentType/DocumentTypeRef/DocumentTypeMutRef
- DocumentTypeV2Getters/Setters traits
- Schema parsing v2 with protocol version 12+ gating
- Drive: CountTree for countable, ProvableCountTree for range_countable
- DocumentTypePrimaryKeyTreeType extension trait in rs-drive with
  own versioning (primary_key_tree_type: 0)
- Meta-schema: documentsCountable and rangeCountable properties
- Update validation: reject changes to countable/range_countable
- All stateless insert/delete/estimation paths use correct tree type

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@QuantumExplorer QuantumExplorer force-pushed the feat/documents-countable branch from cc855a8 to 51d1808 Compare April 10, 2026 08:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs`:
- Around line 77-89: The dispatcher is choosing DocumentType::try_from_schema_v2
based solely on platform_version, which can mis-parse legacy contracts; modify
the dispatcher in try_from_schema so that before calling
DocumentType::try_from_schema_v2 you check data_contract_system_version (the
stored contract system version) and if data_contract_system_version < 2 either
(a) route to the v1 parser (e.g., call the existing v1 path) or (b)
sanitize/remove V2-only flags like documentsCountable/rangeCountable from schema
when full_validation == false, and only allow V2 parsing when
data_contract_system_version >= 2; update uses of full_validation and
validation_operations accordingly to ensure safe deserialization of pre-v12
contracts.

In
`@packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs`:
- Around line 45-63: The code currently reads documents_countable and
range_countable from the schema (variables documents_countable and
range_countable in try_from_schema v2) and later applies them unconditionally,
which allows legacy schemas to be reinterpreted; change try_from_schema (mod.rs)
so these two flags are only honored when the persisted contract/parser version
indicates V2 (or newer) — e.g., check the persisted contract/parser version
field available during deserialization and if the version is older than the V2
threshold, ignore/sanitize DOCUMENTS_COUNTABLE and RANGE_COUNTABLE (force false
or use the original NormalTree semantics) before applying them at the
application site (the code path that currently uses the variables around lines
82–83). Ensure the gating uses the persisted version from the contract/metadata
used to parse the schema rather than the current runtime platform version.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 470928c6-ae21-43df-b985-629e49e53ca4

📥 Commits

Reviewing files that changed from the base of the PR and between cc855a8 and 51d1808.

📒 Files selected for processing (30)
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs
  • packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/mod.rs
  • packages/rs-drive/src/drive/document/primary_key_tree_type.rs
  • packages/rs-drive/src/fees/op.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs
  • packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs
  • packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs
  • packages/rs-platform-version/src/version/v12.rs
✅ Files skipped from review due to trivial changes (5)
  • packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs
  • packages/rs-drive/src/fees/op.rs
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/document/mod.rs
  • packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json
  • packages/rs-platform-version/src/version/v12.rs
  • packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs
  • packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs
  • packages/rs-drive/src/drive/document/primary_key_tree_type.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs

Comment on lines +77 to +89
2 => DocumentType::try_from_schema_v2(
data_contract_id,
data_contract_system_version,
contract_config_version,
name,
schema,
schema_defs,
token_configurations,
data_contact_config,
full_validation,
validation_operations,
platform_version,
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find deserialization call sites and whether `full_validation = false` is used.
rg -n -C3 --type=rust 'versioned_deserialize\s*\('
rg -n -C3 --type=rust 'versioned_deserialize\s*\([^)]*false'

# 2) Inspect document-type parser version routing and where it is sourced from.
rg -n -C4 --type=rust 'document_type_versions|class_method_versions|try_from_schema'

# 3) Locate schema handling of V2-only keys.
rg -n -C3 --type=rust '"documentsCountable"|"rangeCountable"|DOCUMENTS_COUNTABLE|RANGE_COUNTABLE'

Repository: dashpay/platform

Length of output: 50373


🏁 Script executed:

cat -n packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs | head -100

Repository: dashpay/platform

Length of output: 4285


🏁 Script executed:

# Check the versioning config and how parser version is selected
rg -n "try_from_schema_v2|SCHEMA_VERSION|parser.*version" packages/rs-dpp/src/data_contract/ -A 2 -B 2 | head -80

Repository: dashpay/platform

Length of output: 2147


🏁 Script executed:

# Look for documentsCountable and rangeCountable handling in V2
rg -n "documentsCountable|rangeCountable" packages/rs-dpp/src/ -B 3 -A 3 | head -100

Repository: dashpay/platform

Length of output: 3040


🏁 Script executed:

# Check if contract version is evaluated before platform version in deserialization
rg -n "try_from_schema|document_type_version" packages/rs-dpp/src/data_contract/mod.rs -B 5 -A 5 | head -60

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Check how deserialization of existing contracts uses version info
rg -n "versioned_deserialize.*DataContract\|from_bytes.*schema" packages/rs-dpp/src/data_contract/mod.rs -B 3 -A 3

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Look for any migration or version guarding during contract deserialization
rg -n "system_version|contract_version.*from" packages/rs-dpp/src/data_contract/ -B 2 -A 2 | head -100

Repository: dashpay/platform

Length of output: 9192


🏁 Script executed:

# Check if data_contract_system_version is used to guard the parser selection
rg -n "data_contract_system_version|contract_structure_version" packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/ -B 2 -A 2

Repository: dashpay/platform

Length of output: 8092


🏁 Script executed:

# Check how deserialization flow works - from versioned_deserialize to try_from_schema
cat -n packages/rs-dpp/src/data_contract/mod.rs | sed -n '145,200p'

Repository: dashpay/platform

Length of output: 2360


🏁 Script executed:

# Check if there's any version check before trying V2 parser, or safeguard against legacy contracts
rg -n "contract_structure_version|system_version" packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/ -B 3 -A 3

Repository: dashpay/platform

Length of output: 3613


🏁 Script executed:

# Check how try_from_platform_versioned routes to try_from_schema
cat -n packages/rs-dpp/src/data_contract/mod.rs | sed -n '200,280p'

Repository: dashpay/platform

Length of output: 2906


🏁 Script executed:

# Look for where the contract structure version is determined and if it gates parser selection
rg -n "try_from_platform_versioned" packages/rs-dpp/src/data_contract/ -A 20 | head -100

Repository: dashpay/platform

Length of output: 9607


🏁 Script executed:

# Check if serialization includes schema version or if schema is always re-parsed from raw value
rg -n "DataContractInSerializationFormat" packages/rs-dpp/src/data_contract/serialization/ -B 2 -A 8 | head -80

Repository: dashpay/platform

Length of output: 148


🏁 Script executed:

# Find where DataContractInSerializationFormat is defined
fd -i "serialization" packages/rs-dpp/src/data_contract/ -type f | head -20

Repository: dashpay/platform

Length of output: 230


🏁 Script executed:

# Check main contract try_from_platform_versioned implementation
rg -n "fn try_from_platform_versioned" packages/rs-dpp/src/data_contract/mod.rs -A 30 | head -80

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Check the document types creation method
rg -n "create_document_types_from_document_schemas" packages/rs-dpp/src/data_contract/ -A 15 | head -100

Repository: dashpay/platform

Length of output: 8591


🏁 Script executed:

# Check the actual dispatcher logic more carefully - is there any guard based on contract version?
cat -n packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs | sed -n '28,96p'

Repository: dashpay/platform

Length of output: 2998


🏁 Script executed:

# Check if data_contract_system_version is used to gate V2 parser access
rg -n "data_contract_system_version.*2|system_version.*[>=<].*2" packages/rs-dpp/src/data_contract/ -B 2 -A 2

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Look at v2 parser implementation to see if it validates contract version
cat -n packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs | head -120

Repository: dashpay/platform

Length of output: 5863


🏁 Script executed:

# Check if there's any contract version guard preventing V2-specific behavior
rg -n "data_contract_system_version.*<|system_version.*<" packages/rs-dpp/src/data_contract/document_type/ -B 2 -A 2

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Check the comment more carefully - look for what "primary key tree type" is and if it's affected by countable flags
rg -n "primary.*key|tree.*type|NormalTree" packages/rs-dpp/src/data_contract/ -B 1 -A 1 | head -40

Repository: dashpay/platform

Length of output: 3723


🏁 Script executed:

# Verify: does the V2 parser always get used for deserialization in v12+, or is there a guard?
rg -n "CONTRACT_VERSIONS_V4|contract_structure_version.*2" packages/rs-dpp/ -B 2 -A 2 | head -60

Repository: dashpay/platform

Length of output: 759


🏁 Script executed:

# Final verification: check if full_validation is true in schema parsing paths during deserialization
rg -n "create_document_types_from_document_schemas" packages/rs-dpp/src/data_contract/ -B 5 -A 1 | grep -E "(full_validation|create_document)" | head -20

Repository: dashpay/platform

Length of output: 2971


🏁 Script executed:

# Check the immutability safeguard mentioned in code
cat -n packages/rs-dpp/src/data_contract/document_type/index_level/mod.rs | sed -n '285,310p'

Repository: dashpay/platform

Length of output: 1139


Clarify version-gating strategy for V2 schema parser on pre-v12 contracts.

The V2 parser for document schemas (line 77) is dispatched based on the current platform version (v12+), not the contract's stored system version. When legacy contracts stored under v11 are deserialized in v12+, the V2 parser will read documentsCountable and rangeCountable fields if present, affecting the primary key tree type (NormalTree → CountTree/ProvableCountTree). While the code comment acknowledges this and notes that updates are gated by immutability checks (line 291), the parser selection itself has no explicit guard.

Strengthen this by either:

  • Adding a check: if data_contract_system_version < 2, reject or sanitize V2-only flags during deserialization, or
  • Document the defensive assumption (that pre-v12 contracts never contain these flags) more explicitly in the dispatcher.

The current immutability validation prevents updates from changing countable state, but does not prevent initial deserialization with full_validation=false from misinterpreting a legacy schema that happens to include these optional fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs`
around lines 77 - 89, The dispatcher is choosing
DocumentType::try_from_schema_v2 based solely on platform_version, which can
mis-parse legacy contracts; modify the dispatcher in try_from_schema so that
before calling DocumentType::try_from_schema_v2 you check
data_contract_system_version (the stored contract system version) and if
data_contract_system_version < 2 either (a) route to the v1 parser (e.g., call
the existing v1 path) or (b) sanitize/remove V2-only flags like
documentsCountable/rangeCountable from schema when full_validation == false, and
only allow V2 parsing when data_contract_system_version >= 2; update uses of
full_validation and validation_operations accordingly to ensure safe
deserialization of pre-v12 contracts.

Comment on lines +45 to +63
let documents_countable = schema_map_opt
.as_ref()
.and_then(|schema_map| {
Value::inner_optional_bool_value(schema_map, DOCUMENTS_COUNTABLE)
.map_err(consensus_or_protocol_value_error)
.transpose()
})
.transpose()?
.unwrap_or(false);

let range_countable = schema_map_opt
.as_ref()
.and_then(|schema_map| {
Value::inner_optional_bool_value(schema_map, RANGE_COUNTABLE)
.map_err(consensus_or_protocol_value_error)
.transpose()
})
.transpose()?
.unwrap_or(false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Blocker: prevent legacy schema flag replay during deserialization.

Line 45–63 parses documentsCountable / rangeCountable directly from schema content, and Line 82–83 applies them unconditionally. This allows legacy stored schemas to be reinterpreted as countable/range-countable after upgrade, which can diverge from the already-created primary-key tree layout (originally NormalTree).

Please gate these fields by persisted contract/parser version (not just current runtime platform context), or sanitize legacy schemas before applying V2 flags.

Also applies to: 82-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs`
around lines 45 - 63, The code currently reads documents_countable and
range_countable from the schema (variables documents_countable and
range_countable in try_from_schema v2) and later applies them unconditionally,
which allows legacy schemas to be reinterpreted; change try_from_schema (mod.rs)
so these two flags are only honored when the persisted contract/parser version
indicates V2 (or newer) — e.g., check the persisted contract/parser version
field available during deserialization and if the version is older than the V2
threshold, ignore/sanitize DOCUMENTS_COUNTABLE and RANGE_COUNTABLE (force false
or use the original NormalTree semantics) before applying them at the
application site (the code path that currently uses the variables around lines
82–83). Ensure the gating uses the persisted version from the contract/metadata
used to parse the schema rather than the current runtime platform version.

@thepastaclaw
Copy link
Copy Markdown
Collaborator

Re-checked the current head (51d18085) and I still do not think this is good yet.

The remaining blocker is the same old-state/deserialization hole, and it is still live on this SHA:

  • DataContract::versioned_deserialize(..., false, ...) still reparses stored document schemas with the current platform_version (packages/rs-dpp/src/data_contract/v1/serialization/mod.rs -> create_document_types_from_document_schemas() -> DocumentType::try_from_schema()).
  • DocumentType::try_from_schema() still dispatches to the v2 parser solely from the current platform version (packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs), even though it is already passed data_contract_system_version.
  • The shared document meta-schema still allows raw top-level documentsCountable / rangeCountable keys in practice, because it requires a schema field named "additionalProperties": false but does not set root-level additionalProperties: false on the meta-schema itself (packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json).

So a pre-v12 contract can still be accepted/stored with those raw keys ignored by the old parser, get its physical primary tree created as NormalTree, and then later be reloaded on a v12 node as DocumentTypeV2 with count/range-countable semantics revived from the stored schema JSON. That is still a storage-layout mismatch across upgrade.

So from my side: not good yet. I'd want the parser/deserialization path gated on the persisted contract system version (or those v2-only keys stripped/rejected for older contracts during reload), and then I'd be happy to clear it.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Both agents converge on a real storage-layout reinterpretation vulnerability: pre-v12 contracts can carry documentsCountable/rangeCountable in their raw schema, get a physical NormalTree, and then be deserialized through the v2 parser post-upgrade, causing all fee estimation paths to use the wrong tree type. The immutability guards for these flags also lack the regression tests that every other guard in the same file has. Additional architectural concerns around tree-type logic duplication are valid but lower priority.

Reviewed commit: 51d1808

🔴 1 blocking | 🟡 2 suggestion(s) | 💬 1 nitpick(s)

1 additional finding

🔴 blocking: V1 parser preserves smuggled countable flags that the V2 parser will revive after v12 upgrade

packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs (lines 666-684)

The v1 parser (lines 666-670) explicitly documents that it ignores documentsCountable/rangeCountable, but these keys are preserved in the raw schema Value stored on DocumentTypeV1.schema (line 684) and persisted to GroveDB. Meanwhile, the document meta-schema (document-meta.json:508-514) explicitly declares both fields as allowed boolean properties at any protocol version.

After v12 activation, stored contracts are deserialized using the current platform_version, which dispatches to try_from_schema: 2 (confirmed in v4.rs:36). The v2 parser (try_from_schema/v2/mod.rs:45-63) reads these flags from the raw schema and sets them on DocumentTypeV2 (line 82). This changes the return value of primary_key_tree_type() from NormalTree to CountTree/ProvableCountTree for a tree that was physically created as NormalTree.

The 6+ call sites using primary_key_tree_type() (document insert, delete, estimation cost paths) will then compute fees against the wrong tree type. While all nodes agree on the same incorrect result (no consensus fork), fee calculations are wrong, and future count query support would produce incorrect results on these contracts.

The comment at v2/mod.rs:36-42 claims this is safe because "the primary key tree type is set correctly at contract creation time," but this conflates one-time physical tree creation with per-operation tree type selection during deserialization.

Fix: Strip documentsCountable and rangeCountable from the schema Value in the v1 parser before storing it on DocumentTypeV1.schema. This prevents the flags from reaching persistent storage regardless of which parser later deserializes the contract. Alternatively, have the v2 parser force these flags to false for contracts with created_at_epoch predating v12.

🤖 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/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs`:
- [BLOCKING] lines 666-684: V1 parser preserves smuggled countable flags that the V2 parser will revive after v12 upgrade
  The v1 parser (lines 666-670) explicitly documents that it ignores `documentsCountable`/`rangeCountable`, but these keys are preserved in the raw `schema` Value stored on `DocumentTypeV1.schema` (line 684) and persisted to GroveDB. Meanwhile, the document meta-schema (`document-meta.json:508-514`) explicitly declares both fields as allowed boolean properties at any protocol version.

After v12 activation, stored contracts are deserialized using the current `platform_version`, which dispatches to `try_from_schema: 2` (confirmed in `v4.rs:36`). The v2 parser (`try_from_schema/v2/mod.rs:45-63`) reads these flags from the raw schema and sets them on `DocumentTypeV2` (line 82). This changes the return value of `primary_key_tree_type()` from `NormalTree` to `CountTree`/`ProvableCountTree` for a tree that was physically created as `NormalTree`.

The 6+ call sites using `primary_key_tree_type()` (document insert, delete, estimation cost paths) will then compute fees against the wrong tree type. While all nodes agree on the same incorrect result (no consensus fork), fee calculations are wrong, and future count query support would produce incorrect results on these contracts.

The comment at `v2/mod.rs:36-42` claims this is safe because "the primary key tree type is set correctly at contract creation time," but this conflates one-time physical tree creation with per-operation tree type selection during deserialization.

**Fix**: Strip `documentsCountable` and `rangeCountable` from the schema `Value` in the v1 parser before storing it on `DocumentTypeV1.schema`. This prevents the flags from reaching persistent storage regardless of which parser later deserializes the contract. Alternatively, have the v2 parser force these flags to `false` for contracts with `created_at_epoch` predating v12.

In `packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs`:
- [SUGGESTION] lines 183-211: No regression tests for documents_countable and range_countable immutability guards
  The `validate_config()` method correctly rejects changes to `documents_countable` (line 183) and `range_countable` (line 198), which is critical because these flags determine the physical primary key tree layout. However, the `validate_config` test module (lines 288-984) has explicit regression tests for all 9 other immutable config properties — `creation_restriction_mode`, `trade_mode`, `transferable`, `can_be_deleted`, `keep_history`, `mutable`, `encryption_key_req`, `decryption_key_req`, `security_level_requirement` — but none for the two new guards.

Because these flags protect physical storage layout (not just schema semantics), they are higher-stakes than most config immutability checks. A future refactor could inadvertently remove them without any test catching the regression. Add two tests following the existing `should_return_invalid_result_when_<property>_is_changed()` pattern.

In `packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs`:
- [SUGGESTION] lines 288-315: Tree-type selection logic duplicated instead of using centralized primary_key_tree_type()
  The contract insert path (lines 288-315) and update path (`update_contract/v0/mod.rs:352-380`) both inline the same if/else chain for selecting NormalTree vs CountTree vs ProvableCountTree. Document operations use the centralized `DocumentTypePrimaryKeyTreeType::primary_key_tree_type()` from `primary_key_tree_type.rs`, which participates in version dispatch (`platform_version.drive.methods.document.primary_key_tree_type`). The inline copies skip version dispatch entirely — if a v1 of `primary_key_tree_type` is introduced with different selection logic, contract creation will diverge from document operations. Both sites should call the centralized method to maintain a single source of truth.

Comment on lines +183 to +211
if new_document_type.documents_countable() != self.documents_countable() {
return SimpleConsensusValidationResult::new_with_error(
DocumentTypeUpdateError::new(
self.data_contract_id(),
self.name(),
format!(
"document type can not change whether its documents are countable: changing from {} to {}",
self.documents_countable(),
new_document_type.documents_countable()
),
)
.into(),
);
}

if new_document_type.range_countable() != self.range_countable() {
return SimpleConsensusValidationResult::new_with_error(
DocumentTypeUpdateError::new(
self.data_contract_id(),
self.name(),
format!(
"document type can not change whether it is range countable: changing from {} to {}",
self.range_countable(),
new_document_type.range_countable()
),
)
.into(),
);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: No regression tests for documents_countable and range_countable immutability guards

The validate_config() method correctly rejects changes to documents_countable (line 183) and range_countable (line 198), which is critical because these flags determine the physical primary key tree layout. However, the validate_config test module (lines 288-984) has explicit regression tests for all 9 other immutable config properties — creation_restriction_mode, trade_mode, transferable, can_be_deleted, keep_history, mutable, encryption_key_req, decryption_key_req, security_level_requirement — but none for the two new guards.

Because these flags protect physical storage layout (not just schema semantics), they are higher-stakes than most config immutability checks. A future refactor could inadvertently remove them without any test catching the regression. Add two tests following the existing should_return_invalid_result_when_<property>_is_changed() pattern.

source: ['claude', 'codex']

🤖 Fix this 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/data_contract/document_type/methods/validate_update/v0/mod.rs`:
- [SUGGESTION] lines 183-211: No regression tests for documents_countable and range_countable immutability guards
  The `validate_config()` method correctly rejects changes to `documents_countable` (line 183) and `range_countable` (line 198), which is critical because these flags determine the physical primary key tree layout. However, the `validate_config` test module (lines 288-984) has explicit regression tests for all 9 other immutable config properties — `creation_restriction_mode`, `trade_mode`, `transferable`, `can_be_deleted`, `keep_history`, `mutable`, `encryption_key_req`, `decryption_key_req`, `security_level_requirement` — but none for the two new guards.

Because these flags protect physical storage layout (not just schema semantics), they are higher-stakes than most config immutability checks. A future refactor could inadvertently remove them without any test catching the regression. Add two tests following the existing `should_return_invalid_result_when_<property>_is_changed()` pattern.

Comment on lines +288 to +315
if document_type.range_countable() {
// Use a ProvableCountTree for range countable support (implies countable)
let path_items: Vec<Vec<u8>> = type_path.iter().map(|s| s.to_vec()).collect();
batch_operations.push(
LowLevelDriveOperation::for_known_path_key_empty_provable_count_tree(
path_items,
vec![0],
storage_flags.as_ref(),
),
);
} else if document_type.documents_countable() {
// Use a CountTree so total document count is available in O(1)
let path_items: Vec<Vec<u8>> = type_path.iter().map(|s| s.to_vec()).collect();
batch_operations.push(LowLevelDriveOperation::for_known_path_key_empty_count_tree(
path_items,
vec![0],
storage_flags.as_ref(),
));
} else {
let key_info = Key(vec![0]);
self.batch_insert_empty_tree(
type_path,
key_info,
storage_flags.as_ref(),
&mut batch_operations,
&platform_version.drive,
)?;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Tree-type selection logic duplicated instead of using centralized primary_key_tree_type()

The contract insert path (lines 288-315) and update path (update_contract/v0/mod.rs:352-380) both inline the same if/else chain for selecting NormalTree vs CountTree vs ProvableCountTree. Document operations use the centralized DocumentTypePrimaryKeyTreeType::primary_key_tree_type() from primary_key_tree_type.rs, which participates in version dispatch (platform_version.drive.methods.document.primary_key_tree_type). The inline copies skip version dispatch entirely — if a v1 of primary_key_tree_type is introduced with different selection logic, contract creation will diverge from document operations. Both sites should call the centralized method to maintain a single source of truth.

source: ['claude']

🤖 Fix this 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/drive/contract/insert/insert_contract/v0/mod.rs`:
- [SUGGESTION] lines 288-315: Tree-type selection logic duplicated instead of using centralized primary_key_tree_type()
  The contract insert path (lines 288-315) and update path (`update_contract/v0/mod.rs:352-380`) both inline the same if/else chain for selecting NormalTree vs CountTree vs ProvableCountTree. Document operations use the centralized `DocumentTypePrimaryKeyTreeType::primary_key_tree_type()` from `primary_key_tree_type.rs`, which participates in version dispatch (`platform_version.drive.methods.document.primary_key_tree_type`). The inline copies skip version dispatch entirely — if a v1 of `primary_key_tree_type` is introduced with different selection logic, contract creation will diverge from document operations. Both sites should call the centralized method to maintain a single source of truth.

Comment on lines +201 to +208
impl DocumentTypeV2Getters for DocumentTypeV2 {
fn documents_countable(&self) -> bool {
self.documents_countable || self.range_countable
}

fn range_countable(&self) -> bool {
self.range_countable
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Redundant invariant enforcement — documents_countable getter duplicates constructor's OR logic

The documents_countable() getter (line 203) returns self.documents_countable || self.range_countable, but the v2 constructor (try_from_schema/v2/mod.rs:82) already sets v2.documents_countable = documents_countable || range_countable, and set_range_countable() also enforces this invariant. The dual enforcement is harmless but creates ambiguity about which layer is authoritative. If the field always reflects the invariant at construction time, the getter can simply return self.documents_countable. This prevents subtle inconsistencies if a third code path constructs DocumentTypeV2 without the OR.

💡 Suggested change
Suggested change
impl DocumentTypeV2Getters for DocumentTypeV2 {
fn documents_countable(&self) -> bool {
self.documents_countable || self.range_countable
}
fn range_countable(&self) -> bool {
self.range_countable
}
fn documents_countable(&self) -> bool {
self.documents_countable
}

source: ['claude']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants