From 51d18085d9539bc1d89ec3d63a767f6e531e52fa Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 8 Apr 2026 12:31:30 +0300 Subject: [PATCH 01/14] feat(dpp): add documents_countable and blast_syncable to DocumentTypeV2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../document/v0/document-meta.json | 8 + .../document_type/accessors/mod.rs | 176 ++++++++++ .../document_type/accessors/v2/mod.rs | 20 ++ .../class_methods/try_from_schema/mod.rs | 16 +- .../class_methods/try_from_schema/v1/mod.rs | 6 + .../class_methods/try_from_schema/v2/mod.rs | 119 +++++++ .../methods/validate_update/v0/mod.rs | 34 +- .../methods/versioned_methods.rs | 2 + .../src/data_contract/document_type/mod.rs | 13 + .../document_type/v2/accessors.rs | 226 +++++++++++++ .../src/data_contract/document_type/v2/mod.rs | 306 ++++++++++++++++++ .../document_type/v2/random_document_type.rs | 40 +++ .../methods/validate_update/v0/mod.rs | 1 + .../contract/insert/insert_contract/v0/mod.rs | 37 ++- .../contract/update/update_contract/v0/mod.rs | 38 ++- .../v0/mod.rs | 6 +- .../v0/mod.rs | 6 +- .../v0/mod.rs | 7 +- .../v0/mod.rs | 6 +- .../v0/mod.rs | 9 +- .../add_document_to_primary_storage/v0/mod.rs | 12 +- packages/rs-drive/src/drive/document/mod.rs | 4 + .../drive/document/primary_key_tree_type.rs | 110 +++++++ packages/rs-drive/src/fees/op.rs | 17 + .../dpp_versions/dpp_contract_versions/mod.rs | 1 + .../dpp_versions/dpp_contract_versions/v4.rs | 66 ++++ .../drive_document_method_versions/mod.rs | 1 + .../drive_document_method_versions/v1.rs | 1 + .../drive_document_method_versions/v2.rs | 1 + .../rs-platform-version/src/version/v12.rs | 4 +- 30 files changed, 1261 insertions(+), 32 deletions(-) create mode 100644 packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/v2/mod.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs create mode 100644 packages/rs-drive/src/drive/document/primary_key_tree_type.rs create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs diff --git a/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json index 3747878cf17..8ea5e256d08 100644 --- a/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json +++ b/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json @@ -505,6 +505,14 @@ ], "description": "Key requirements. 0 - Unique Non Replaceable, 1 - Multiple, 2 - Multiple with reference to latest key." }, + "documentsCountable": { + "type": "boolean", + "description": "When true, the primary key tree uses a CountTree enabling O(1) total document count queries. Only effective from protocol version 12." + }, + "rangeCountable": { + "type": "boolean", + "description": "When true, the primary key tree uses a ProvableCountTree enabling range countable. Only effective from protocol version 12. Implies documentsCountable." + }, "tokenCost": { "type": "object", "properties": { diff --git a/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs b/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs index af22f81c8fc..562374652dd 100644 --- a/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs @@ -1,5 +1,6 @@ mod v0; mod v1; +mod v2; use crate::data_contract::document_type::index::Index; use crate::data_contract::document_type::index_level::IndexLevel; @@ -21,12 +22,14 @@ use indexmap::IndexMap; use std::collections::{BTreeMap, BTreeSet}; pub use v0::*; pub use v1::*; +pub use v2::*; impl DocumentTypeV0MutGetters for DocumentType { fn schema_mut(&mut self) -> &mut Value { match self { DocumentType::V0(v0) => v0.schema_mut(), DocumentType::V1(v1) => v1.schema_mut(), + DocumentType::V2(v2) => v2.schema_mut(), } } } @@ -36,6 +39,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.name(), DocumentType::V1(v1) => v1.name(), + DocumentType::V2(v2) => v2.name(), } } @@ -43,6 +47,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.schema(), DocumentType::V1(v1) => v1.schema(), + DocumentType::V2(v2) => v2.schema(), } } @@ -50,6 +55,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.schema_owned(), DocumentType::V1(v1) => v1.schema_owned(), + DocumentType::V2(v2) => v2.schema_owned(), } } @@ -57,6 +63,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.indexes(), DocumentType::V1(v1) => v1.indexes(), + DocumentType::V2(v2) => v2.indexes(), } } @@ -64,6 +71,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.find_contested_index(), DocumentType::V1(v1) => v1.find_contested_index(), + DocumentType::V2(v2) => v2.find_contested_index(), } } @@ -71,6 +79,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.index_structure(), DocumentType::V1(v1) => v1.index_structure(), + DocumentType::V2(v2) => v2.index_structure(), } } @@ -78,6 +87,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.flattened_properties(), DocumentType::V1(v1) => v1.flattened_properties(), + DocumentType::V2(v2) => v2.flattened_properties(), } } @@ -85,6 +95,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.properties(), DocumentType::V1(v1) => v1.properties(), + DocumentType::V2(v2) => v2.properties(), } } @@ -92,6 +103,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.identifier_paths(), DocumentType::V1(v1) => v1.identifier_paths(), + DocumentType::V2(v2) => v2.identifier_paths(), } } @@ -99,6 +111,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.binary_paths(), DocumentType::V1(v1) => v1.binary_paths(), + DocumentType::V2(v2) => v2.binary_paths(), } } @@ -106,6 +119,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.required_fields(), DocumentType::V1(v1) => v1.required_fields(), + DocumentType::V2(v2) => v2.required_fields(), } } @@ -113,6 +127,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.transient_fields(), DocumentType::V1(v1) => v1.transient_fields(), + DocumentType::V2(v2) => v2.transient_fields(), } } @@ -120,6 +135,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.documents_keep_history(), DocumentType::V1(v1) => v1.documents_keep_history(), + DocumentType::V2(v2) => v2.documents_keep_history(), } } @@ -127,6 +143,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.documents_mutable(), DocumentType::V1(v1) => v1.documents_mutable(), + DocumentType::V2(v2) => v2.documents_mutable(), } } @@ -134,6 +151,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.documents_can_be_deleted(), DocumentType::V1(v1) => v1.documents_can_be_deleted(), + DocumentType::V2(v2) => v2.documents_can_be_deleted(), } } @@ -141,6 +159,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.documents_transferable(), DocumentType::V1(v1) => v1.documents_transferable(), + DocumentType::V2(v2) => v2.documents_transferable(), } } @@ -148,6 +167,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.trade_mode(), DocumentType::V1(v1) => v1.trade_mode(), + DocumentType::V2(v2) => v2.trade_mode(), } } @@ -155,6 +175,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.creation_restriction_mode(), DocumentType::V1(v1) => v1.creation_restriction_mode(), + DocumentType::V2(v2) => v2.creation_restriction_mode(), } } @@ -162,6 +183,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.data_contract_id(), DocumentType::V1(v1) => v1.data_contract_id(), + DocumentType::V2(v2) => v2.data_contract_id(), } } @@ -169,6 +191,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.requires_identity_encryption_bounded_key(), DocumentType::V1(v1) => v1.requires_identity_encryption_bounded_key(), + DocumentType::V2(v2) => v2.requires_identity_encryption_bounded_key(), } } @@ -176,6 +199,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.requires_identity_decryption_bounded_key(), DocumentType::V1(v1) => v1.requires_identity_decryption_bounded_key(), + DocumentType::V2(v2) => v2.requires_identity_decryption_bounded_key(), } } @@ -183,6 +207,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.security_level_requirement(), DocumentType::V1(v1) => v1.security_level_requirement(), + DocumentType::V2(v2) => v2.security_level_requirement(), } } @@ -191,6 +216,7 @@ impl DocumentTypeV0Getters for DocumentType { match self { DocumentType::V0(v0) => v0.json_schema_validator_ref(), DocumentType::V1(v1) => v1.json_schema_validator_ref(), + DocumentType::V2(v2) => v2.json_schema_validator_ref(), } } } @@ -200,6 +226,7 @@ impl DocumentTypeV0Setters for DocumentType { match self { DocumentType::V0(v0) => v0.set_data_contract_id(data_contract_id), DocumentType::V1(v1) => v1.set_data_contract_id(data_contract_id), + DocumentType::V2(v2) => v2.set_data_contract_id(data_contract_id), } } } @@ -209,6 +236,7 @@ impl DocumentTypeV1Setters for DocumentType { match self { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_creation_token_cost(cost), + DocumentType::V2(v2) => v2.set_document_creation_token_cost(cost), } } @@ -216,6 +244,7 @@ impl DocumentTypeV1Setters for DocumentType { match self { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_replacement_token_cost(cost), + DocumentType::V2(v2) => v2.set_document_replacement_token_cost(cost), } } @@ -223,6 +252,7 @@ impl DocumentTypeV1Setters for DocumentType { match self { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_deletion_token_cost(cost), + DocumentType::V2(v2) => v2.set_document_deletion_token_cost(cost), } } @@ -230,6 +260,7 @@ impl DocumentTypeV1Setters for DocumentType { match self { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_transfer_token_cost(cost), + DocumentType::V2(v2) => v2.set_document_transfer_token_cost(cost), } } @@ -237,6 +268,7 @@ impl DocumentTypeV1Setters for DocumentType { match self { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_price_update_token_cost(cost), + DocumentType::V2(v2) => v2.set_document_price_update_token_cost(cost), } } @@ -244,6 +276,7 @@ impl DocumentTypeV1Setters for DocumentType { match self { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_purchase_token_cost(cost), + DocumentType::V2(v2) => v2.set_document_purchase_token_cost(cost), } } } @@ -253,6 +286,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.name(), DocumentTypeRef::V1(v1) => v1.name(), + DocumentTypeRef::V2(v2) => v2.name(), } } @@ -260,6 +294,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.schema(), DocumentTypeRef::V1(v1) => v1.schema(), + DocumentTypeRef::V2(v2) => v2.schema(), } } @@ -267,6 +302,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.clone().schema_owned(), DocumentTypeRef::V1(v1) => v1.clone().schema_owned(), + DocumentTypeRef::V2(v2) => v2.clone().schema_owned(), } } @@ -274,6 +310,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.indexes(), DocumentTypeRef::V1(v1) => v1.indexes(), + DocumentTypeRef::V2(v2) => v2.indexes(), } } @@ -281,6 +318,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.find_contested_index(), DocumentTypeRef::V1(v1) => v1.find_contested_index(), + DocumentTypeRef::V2(v2) => v2.find_contested_index(), } } @@ -288,6 +326,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.index_structure(), DocumentTypeRef::V1(v1) => v1.index_structure(), + DocumentTypeRef::V2(v2) => v2.index_structure(), } } @@ -295,6 +334,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.flattened_properties(), DocumentTypeRef::V1(v1) => v1.flattened_properties(), + DocumentTypeRef::V2(v2) => v2.flattened_properties(), } } @@ -302,6 +342,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.properties(), DocumentTypeRef::V1(v1) => v1.properties(), + DocumentTypeRef::V2(v2) => v2.properties(), } } @@ -309,6 +350,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.identifier_paths(), DocumentTypeRef::V1(v1) => v1.identifier_paths(), + DocumentTypeRef::V2(v2) => v2.identifier_paths(), } } @@ -316,6 +358,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.binary_paths(), DocumentTypeRef::V1(v1) => v1.binary_paths(), + DocumentTypeRef::V2(v2) => v2.binary_paths(), } } @@ -323,6 +366,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.required_fields(), DocumentTypeRef::V1(v1) => v1.required_fields(), + DocumentTypeRef::V2(v2) => v2.required_fields(), } } @@ -330,6 +374,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.transient_fields(), DocumentTypeRef::V1(v1) => v1.transient_fields(), + DocumentTypeRef::V2(v2) => v2.transient_fields(), } } @@ -337,6 +382,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.documents_keep_history(), DocumentTypeRef::V1(v1) => v1.documents_keep_history(), + DocumentTypeRef::V2(v2) => v2.documents_keep_history(), } } @@ -344,6 +390,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.documents_mutable(), DocumentTypeRef::V1(v1) => v1.documents_mutable(), + DocumentTypeRef::V2(v2) => v2.documents_mutable(), } } @@ -351,6 +398,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.documents_can_be_deleted(), DocumentTypeRef::V1(v1) => v1.documents_can_be_deleted(), + DocumentTypeRef::V2(v2) => v2.documents_can_be_deleted(), } } @@ -358,6 +406,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.documents_transferable(), DocumentTypeRef::V1(v1) => v1.documents_transferable(), + DocumentTypeRef::V2(v2) => v2.documents_transferable(), } } @@ -365,6 +414,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.trade_mode(), DocumentTypeRef::V1(v1) => v1.trade_mode(), + DocumentTypeRef::V2(v2) => v2.trade_mode(), } } @@ -372,6 +422,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.creation_restriction_mode(), DocumentTypeRef::V1(v1) => v1.creation_restriction_mode(), + DocumentTypeRef::V2(v2) => v2.creation_restriction_mode(), } } @@ -379,6 +430,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.data_contract_id(), DocumentTypeRef::V1(v1) => v1.data_contract_id(), + DocumentTypeRef::V2(v2) => v2.data_contract_id(), } } @@ -386,6 +438,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.requires_identity_encryption_bounded_key(), DocumentTypeRef::V1(v1) => v1.requires_identity_encryption_bounded_key(), + DocumentTypeRef::V2(v2) => v2.requires_identity_encryption_bounded_key(), } } @@ -393,6 +446,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.requires_identity_decryption_bounded_key(), DocumentTypeRef::V1(v1) => v1.requires_identity_decryption_bounded_key(), + DocumentTypeRef::V2(v2) => v2.requires_identity_decryption_bounded_key(), } } @@ -400,6 +454,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.security_level_requirement(), DocumentTypeRef::V1(v1) => v1.security_level_requirement(), + DocumentTypeRef::V2(v2) => v2.security_level_requirement(), } } @@ -408,6 +463,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => v0.json_schema_validator_ref(), DocumentTypeRef::V1(v1) => v1.json_schema_validator_ref(), + DocumentTypeRef::V2(v2) => v2.json_schema_validator_ref(), } } } @@ -416,6 +472,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.name(), DocumentTypeMutRef::V1(v1) => v1.name(), + DocumentTypeMutRef::V2(v2) => v2.name(), } } @@ -423,6 +480,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.schema(), DocumentTypeMutRef::V1(v1) => v1.schema(), + DocumentTypeMutRef::V2(v2) => v2.schema(), } } @@ -430,6 +488,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.clone().schema_owned(), DocumentTypeMutRef::V1(v1) => v1.clone().schema_owned(), + DocumentTypeMutRef::V2(v2) => v2.clone().schema_owned(), } } @@ -437,6 +496,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.indexes(), DocumentTypeMutRef::V1(v1) => v1.indexes(), + DocumentTypeMutRef::V2(v2) => v2.indexes(), } } @@ -444,6 +504,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.find_contested_index(), DocumentTypeMutRef::V1(v1) => v1.find_contested_index(), + DocumentTypeMutRef::V2(v2) => v2.find_contested_index(), } } @@ -451,6 +512,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.index_structure(), DocumentTypeMutRef::V1(v1) => v1.index_structure(), + DocumentTypeMutRef::V2(v2) => v2.index_structure(), } } @@ -458,6 +520,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.flattened_properties(), DocumentTypeMutRef::V1(v1) => v1.flattened_properties(), + DocumentTypeMutRef::V2(v2) => v2.flattened_properties(), } } @@ -465,6 +528,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.properties(), DocumentTypeMutRef::V1(v1) => v1.properties(), + DocumentTypeMutRef::V2(v2) => v2.properties(), } } @@ -472,6 +536,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.identifier_paths(), DocumentTypeMutRef::V1(v1) => v1.identifier_paths(), + DocumentTypeMutRef::V2(v2) => v2.identifier_paths(), } } @@ -479,6 +544,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.binary_paths(), DocumentTypeMutRef::V1(v1) => v1.binary_paths(), + DocumentTypeMutRef::V2(v2) => v2.binary_paths(), } } @@ -486,6 +552,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.required_fields(), DocumentTypeMutRef::V1(v1) => v1.required_fields(), + DocumentTypeMutRef::V2(v2) => v2.required_fields(), } } @@ -493,6 +560,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.transient_fields(), DocumentTypeMutRef::V1(v1) => v1.transient_fields(), + DocumentTypeMutRef::V2(v2) => v2.transient_fields(), } } @@ -500,6 +568,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.documents_keep_history(), DocumentTypeMutRef::V1(v1) => v1.documents_keep_history(), + DocumentTypeMutRef::V2(v2) => v2.documents_keep_history(), } } @@ -507,6 +576,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.documents_mutable(), DocumentTypeMutRef::V1(v1) => v1.documents_mutable(), + DocumentTypeMutRef::V2(v2) => v2.documents_mutable(), } } @@ -514,6 +584,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.documents_can_be_deleted(), DocumentTypeMutRef::V1(v1) => v1.documents_can_be_deleted(), + DocumentTypeMutRef::V2(v2) => v2.documents_can_be_deleted(), } } @@ -521,6 +592,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.documents_transferable(), DocumentTypeMutRef::V1(v1) => v1.documents_transferable(), + DocumentTypeMutRef::V2(v2) => v2.documents_transferable(), } } @@ -528,6 +600,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.trade_mode(), DocumentTypeMutRef::V1(v1) => v1.trade_mode(), + DocumentTypeMutRef::V2(v2) => v2.trade_mode(), } } @@ -535,6 +608,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.creation_restriction_mode(), DocumentTypeMutRef::V1(v1) => v1.creation_restriction_mode(), + DocumentTypeMutRef::V2(v2) => v2.creation_restriction_mode(), } } @@ -542,6 +616,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.data_contract_id(), DocumentTypeMutRef::V1(v1) => v1.data_contract_id(), + DocumentTypeMutRef::V2(v2) => v2.data_contract_id(), } } @@ -549,6 +624,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.requires_identity_encryption_bounded_key(), DocumentTypeMutRef::V1(v1) => v1.requires_identity_encryption_bounded_key(), + DocumentTypeMutRef::V2(v2) => v2.requires_identity_encryption_bounded_key(), } } @@ -556,6 +632,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.requires_identity_decryption_bounded_key(), DocumentTypeMutRef::V1(v1) => v1.requires_identity_decryption_bounded_key(), + DocumentTypeMutRef::V2(v2) => v2.requires_identity_decryption_bounded_key(), } } @@ -563,6 +640,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.security_level_requirement(), DocumentTypeMutRef::V1(v1) => v1.security_level_requirement(), + DocumentTypeMutRef::V2(v2) => v2.security_level_requirement(), } } @@ -571,6 +649,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.json_schema_validator_ref(), DocumentTypeMutRef::V1(v1) => v1.json_schema_validator_ref(), + DocumentTypeMutRef::V2(v2) => v2.json_schema_validator_ref(), } } } @@ -580,6 +659,7 @@ impl DocumentTypeV0Setters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(v0) => v0.set_data_contract_id(data_contract_id), DocumentTypeMutRef::V1(v1) => v1.set_data_contract_id(data_contract_id), + DocumentTypeMutRef::V2(v2) => v2.set_data_contract_id(data_contract_id), } } } @@ -589,6 +669,7 @@ impl DocumentTypeV1Getters for DocumentType { match self { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_creation_token_cost(), + DocumentType::V2(v2) => v2.document_creation_token_cost(), } } @@ -596,6 +677,7 @@ impl DocumentTypeV1Getters for DocumentType { match self { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_replacement_token_cost(), + DocumentType::V2(v2) => v2.document_replacement_token_cost(), } } @@ -603,6 +685,7 @@ impl DocumentTypeV1Getters for DocumentType { match self { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_deletion_token_cost(), + DocumentType::V2(v2) => v2.document_deletion_token_cost(), } } @@ -610,6 +693,7 @@ impl DocumentTypeV1Getters for DocumentType { match self { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_transfer_token_cost(), + DocumentType::V2(v2) => v2.document_transfer_token_cost(), } } @@ -617,6 +701,7 @@ impl DocumentTypeV1Getters for DocumentType { match self { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_update_price_token_cost(), + DocumentType::V2(v2) => v2.document_update_price_token_cost(), } } @@ -624,6 +709,7 @@ impl DocumentTypeV1Getters for DocumentType { match self { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_purchase_token_cost(), + DocumentType::V2(v2) => v2.document_purchase_token_cost(), } } @@ -631,6 +717,7 @@ impl DocumentTypeV1Getters for DocumentType { match self { DocumentType::V0(_) => vec![], DocumentType::V1(v1) => v1.all_document_token_costs(), + DocumentType::V2(v2) => v2.all_document_token_costs(), } } @@ -640,6 +727,7 @@ impl DocumentTypeV1Getters for DocumentType { match self { DocumentType::V0(_) => BTreeMap::new(), DocumentType::V1(v1) => v1.all_external_token_costs_contract_tokens(), + DocumentType::V2(v2) => v2.all_external_token_costs_contract_tokens(), } } } @@ -649,6 +737,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_creation_token_cost(), + DocumentTypeRef::V2(v2) => v2.document_creation_token_cost(), } } @@ -656,6 +745,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_replacement_token_cost(), + DocumentTypeRef::V2(v2) => v2.document_replacement_token_cost(), } } @@ -663,6 +753,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_deletion_token_cost(), + DocumentTypeRef::V2(v2) => v2.document_deletion_token_cost(), } } @@ -670,6 +761,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_transfer_token_cost(), + DocumentTypeRef::V2(v2) => v2.document_transfer_token_cost(), } } @@ -677,6 +769,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_update_price_token_cost(), + DocumentTypeRef::V2(v2) => v2.document_update_price_token_cost(), } } @@ -684,6 +777,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_purchase_token_cost(), + DocumentTypeRef::V2(v2) => v2.document_purchase_token_cost(), } } @@ -691,6 +785,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(_) => vec![], DocumentTypeRef::V1(v1) => v1.all_document_token_costs(), + DocumentTypeRef::V2(v2) => v2.all_document_token_costs(), } } @@ -700,6 +795,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(_) => BTreeMap::new(), DocumentTypeRef::V1(v1) => v1.all_external_token_costs_contract_tokens(), + DocumentTypeRef::V2(v2) => v2.all_external_token_costs_contract_tokens(), } } } @@ -709,6 +805,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_creation_token_cost(), + DocumentTypeMutRef::V2(v2) => v2.document_creation_token_cost(), } } @@ -716,6 +813,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_replacement_token_cost(), + DocumentTypeMutRef::V2(v2) => v2.document_replacement_token_cost(), } } @@ -723,6 +821,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_deletion_token_cost(), + DocumentTypeMutRef::V2(v2) => v2.document_deletion_token_cost(), } } @@ -730,6 +829,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_transfer_token_cost(), + DocumentTypeMutRef::V2(v2) => v2.document_transfer_token_cost(), } } @@ -737,6 +837,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_update_price_token_cost(), + DocumentTypeMutRef::V2(v2) => v2.document_update_price_token_cost(), } } @@ -744,6 +845,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_purchase_token_cost(), + DocumentTypeMutRef::V2(v2) => v2.document_purchase_token_cost(), } } @@ -751,6 +853,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(_) => vec![], DocumentTypeMutRef::V1(v1) => v1.all_document_token_costs(), + DocumentTypeMutRef::V2(v2) => v2.all_document_token_costs(), } } @@ -760,6 +863,79 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { match self { DocumentTypeMutRef::V0(_) => BTreeMap::new(), DocumentTypeMutRef::V1(v1) => v1.all_external_token_costs_contract_tokens(), + DocumentTypeMutRef::V2(v2) => v2.all_external_token_costs_contract_tokens(), + } + } +} + +impl DocumentTypeV2Getters for DocumentType { + fn documents_countable(&self) -> bool { + match self { + DocumentType::V0(_) => false, + DocumentType::V1(_) => false, + DocumentType::V2(v2) => v2.documents_countable(), + } + } + + fn range_countable(&self) -> bool { + match self { + DocumentType::V0(_) => false, + DocumentType::V1(_) => false, + DocumentType::V2(v2) => v2.range_countable(), + } + } +} + +impl DocumentTypeV2Setters for DocumentType { + fn set_documents_countable(&mut self, countable: bool) { + match self { + DocumentType::V0(_) => { /* no-op */ } + DocumentType::V1(_) => { /* no-op */ } + DocumentType::V2(v2) => v2.set_documents_countable(countable), + } + } + + fn set_range_countable(&mut self, range_countable: bool) { + match self { + DocumentType::V0(_) => { /* no-op */ } + DocumentType::V1(_) => { /* no-op */ } + DocumentType::V2(v2) => v2.set_range_countable(range_countable), + } + } +} + +impl DocumentTypeV2Getters for DocumentTypeRef<'_> { + fn documents_countable(&self) -> bool { + match self { + DocumentTypeRef::V0(_) => false, + DocumentTypeRef::V1(_) => false, + DocumentTypeRef::V2(v2) => v2.documents_countable(), + } + } + + fn range_countable(&self) -> bool { + match self { + DocumentTypeRef::V0(_) => false, + DocumentTypeRef::V1(_) => false, + DocumentTypeRef::V2(v2) => v2.range_countable(), + } + } +} + +impl DocumentTypeV2Getters for DocumentTypeMutRef<'_> { + fn documents_countable(&self) -> bool { + match self { + DocumentTypeMutRef::V0(_) => false, + DocumentTypeMutRef::V1(_) => false, + DocumentTypeMutRef::V2(v2) => v2.documents_countable(), + } + } + + fn range_countable(&self) -> bool { + match self { + DocumentTypeMutRef::V0(_) => false, + DocumentTypeMutRef::V1(_) => false, + DocumentTypeMutRef::V2(v2) => v2.range_countable(), } } } diff --git a/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs new file mode 100644 index 00000000000..ac72b5c0bca --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs @@ -0,0 +1,20 @@ +/// Trait providing getters for DocumentTypeV2-specific fields. +pub trait DocumentTypeV2Getters { + /// Returns whether documents of this type are countable. + /// When true, the primary key tree uses a CountTree enabling O(1) total document count queries. + fn documents_countable(&self) -> bool; + + /// Returns whether this document type supports range countable. + /// When true, the primary key tree uses a ProvableCountTree. + /// Implies documents_countable = true. + fn range_countable(&self) -> bool; +} + +/// Trait providing setters for DocumentTypeV2-specific fields. +pub trait DocumentTypeV2Setters { + /// Sets whether documents of this type are countable. + fn set_documents_countable(&mut self, countable: bool); + + /// Sets whether this document type supports range countable. + fn set_range_countable(&mut self, range_countable: bool); +} diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs index 37de63fe927..040fe25d0fc 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs @@ -17,6 +17,7 @@ use std::collections::{BTreeMap, BTreeSet}; mod v0; mod v1; +mod v2; const NOT_ALLOWED_SYSTEM_PROPERTIES: [&str; 1] = ["$id"]; @@ -73,9 +74,22 @@ impl DocumentType { platform_version, ) .map(|document_type| document_type.into()), + 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, + ), version => Err(ProtocolError::UnknownVersionMismatch { method: "try_from_schema".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, }), } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs index 39e29550162..8088b4b4091 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs @@ -663,6 +663,12 @@ impl DocumentTypeV1 { .transpose() }; + // Note: documentsCountable / rangeCountable 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. + let token_costs = TokenCostsV0 { create: extract_cost("create")?, replace: extract_cost("replace")?, diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs new file mode 100644 index 00000000000..a5599fc0d4f --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v2/mod.rs @@ -0,0 +1,119 @@ +use crate::data_contract::config::DataContractConfig; +use crate::data_contract::document_type::class_methods::consensus_or_protocol_value_error; +use crate::data_contract::document_type::property_names::{DOCUMENTS_COUNTABLE, RANGE_COUNTABLE}; +use crate::data_contract::document_type::v1::DocumentTypeV1; +use crate::data_contract::document_type::v2::DocumentTypeV2; +use crate::data_contract::document_type::DocumentType; +use crate::data_contract::{TokenConfiguration, TokenContractPosition}; +use crate::validation::operations::ProtocolValidationOperation; +use crate::version::PlatformVersion; +use crate::ProtocolError; +use platform_value::{Identifier, Value}; +use std::collections::BTreeMap; + +impl DocumentTypeV2 { + /// Parses a document type schema with V2-specific fields (`documentsCountable`, + /// `rangeCountable`). Delegates core parsing to the V1 parser, then wraps the + /// result in a `DocumentTypeV2` with the additional fields set. + /// + /// This parser is only reachable from protocol version 12+ (via CONTRACT_VERSIONS_V4). + #[allow(clippy::too_many_arguments)] + pub(super) fn try_from_schema( + data_contract_id: Identifier, + data_contract_system_version: u16, + contract_config_version: u16, + name: &str, + schema: Value, + schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, + data_contact_config: &DataContractConfig, + full_validation: bool, + validation_operations: &mut impl Extend, + platform_version: &PlatformVersion, + ) -> Result { + // Extract V2-specific fields before the V1 parser consumes the schema map. + // + // Note on pre-v12 contracts: contracts created before v12 used the v1 parser + // which ignores these fields. After v12 upgrade, deserialization uses the v2 + // parser which will read them. This is safe because the contract update path + // runs through the v2 parser with full_validation=true, and the primary key + // tree type is set correctly at contract creation time. Pre-v12 contracts + // can only have these flags if they were explicitly set in the schema — the + // meta-schema allows them as optional boolean properties. + let schema_map_opt = schema.to_map().ok(); + + 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); + + // Delegate core parsing to V1 + let v1 = DocumentTypeV1::try_from_schema( + 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, + )?; + + // Convert to V2 and set the new fields + let mut v2: DocumentTypeV2 = v1.into(); + v2.documents_countable = documents_countable || range_countable; + v2.range_countable = range_countable; + Ok(v2) + } +} + +impl DocumentType { + /// Dispatches to `DocumentTypeV2::try_from_schema` and wraps the result. + #[allow(clippy::too_many_arguments)] + pub(in crate::data_contract::document_type::class_methods) fn try_from_schema_v2( + data_contract_id: Identifier, + data_contract_system_version: u16, + contract_config_version: u16, + name: &str, + schema: Value, + schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, + data_contact_config: &DataContractConfig, + full_validation: bool, + validation_operations: &mut impl Extend, + platform_version: &PlatformVersion, + ) -> Result { + DocumentTypeV2::try_from_schema( + 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, + ) + .map(DocumentType::V2) + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs index ce0acdf7b44..3f7ad290107 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs @@ -1,6 +1,8 @@ use crate::consensus::basic::data_contract::IncompatibleDocumentTypeSchemaError; use crate::consensus::state::data_contract::document_type_update_error::DocumentTypeUpdateError; -use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; +use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV2Getters, +}; use crate::data_contract::document_type::schema::validate_schema_compatibility; use crate::data_contract::document_type::DocumentTypeRef; use crate::data_contract::errors::DataContractError; @@ -178,6 +180,36 @@ impl DocumentTypeRef<'_> { ); } + 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(), + ); + } + SimpleConsensusValidationResult::new() } diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs index 686aaba0c4b..588c826c383 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs @@ -2,6 +2,7 @@ use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; use crate::data_contract::document_type::methods::DocumentTypeBasicMethods; use crate::data_contract::document_type::v0::DocumentTypeV0; use crate::data_contract::document_type::v1::DocumentTypeV1; +use crate::data_contract::document_type::v2::DocumentTypeV2; use crate::data_contract::document_type::{ DocumentPropertyType, DocumentType, DocumentTypeRef, Index, DEFAULT_HASH_SIZE, MAX_INDEX_SIZE, }; @@ -594,6 +595,7 @@ pub trait DocumentTypeV0MethodsVersioned: DocumentTypeV0Getters + DocumentTypeBa impl DocumentTypeV0MethodsVersioned for DocumentTypeV0 {} impl DocumentTypeV0MethodsVersioned for DocumentTypeV1 {} +impl DocumentTypeV0MethodsVersioned for DocumentTypeV2 {} impl DocumentTypeV0MethodsVersioned for DocumentType {} impl DocumentTypeV0MethodsVersioned for DocumentTypeRef<'_> {} diff --git a/packages/rs-dpp/src/data_contract/document_type/mod.rs b/packages/rs-dpp/src/data_contract/document_type/mod.rs index aadb6eedbfc..ff05f513d85 100644 --- a/packages/rs-dpp/src/data_contract/document_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/mod.rs @@ -18,6 +18,7 @@ pub mod schema; mod token_costs; pub mod v0; pub mod v1; +pub mod v2; #[cfg(feature = "validation")] pub(crate) mod validator; @@ -26,6 +27,7 @@ use crate::data_contract::document_type::methods::{ }; use crate::data_contract::document_type::v0::DocumentTypeV0; use crate::data_contract::document_type::v1::DocumentTypeV1; +use crate::data_contract::document_type::v2::DocumentTypeV2; use crate::document::Document; use crate::fee::Credits; use crate::version::PlatformVersion; @@ -74,18 +76,22 @@ pub(crate) mod property_names { pub const CONTENT_MEDIA_TYPE: &str = "contentMediaType"; pub const ENCRYPTION_KEY_REQUIREMENTS: &str = "encryptionKeyReqs"; pub const DECRYPTION_KEY_REQUIREMENTS: &str = "decryptionKeyReqs"; + pub const DOCUMENTS_COUNTABLE: &str = "documentsCountable"; + pub const RANGE_COUNTABLE: &str = "rangeCountable"; } #[derive(Clone, Copy, Debug, PartialEq)] pub enum DocumentTypeRef<'a> { V0(&'a DocumentTypeV0), V1(&'a DocumentTypeV1), + V2(&'a DocumentTypeV2), } #[derive(Debug)] pub enum DocumentTypeMutRef<'a> { V0(&'a mut DocumentTypeV0), V1(&'a mut DocumentTypeV1), + V2(&'a mut DocumentTypeV2), } #[allow(clippy::large_enum_variant)] @@ -93,6 +99,7 @@ pub enum DocumentTypeMutRef<'a> { pub enum DocumentType { V0(DocumentTypeV0), V1(DocumentTypeV1), + V2(DocumentTypeV2), } impl DocumentType { @@ -100,6 +107,7 @@ impl DocumentType { match self { DocumentType::V0(v0) => DocumentTypeRef::V0(v0), DocumentType::V1(v1) => DocumentTypeRef::V1(v1), + DocumentType::V2(v2) => DocumentTypeRef::V2(v2), } } @@ -107,6 +115,7 @@ impl DocumentType { match self { DocumentType::V0(v0) => DocumentTypeMutRef::V0(v0), DocumentType::V1(v1) => DocumentTypeMutRef::V1(v1), + DocumentType::V2(v2) => DocumentTypeMutRef::V2(v2), } } @@ -122,6 +131,9 @@ impl DocumentType { DocumentType::V1(v1) => { v1.prefunded_voting_balance_for_document(document, platform_version) } + DocumentType::V2(v2) => { + v2.prefunded_voting_balance_for_document(document, platform_version) + } } } } @@ -131,6 +143,7 @@ impl DocumentTypeRef<'_> { match self { DocumentTypeRef::V0(v0) => DocumentType::V0((*v0).to_owned()), DocumentTypeRef::V1(v1) => DocumentType::V1((*v1).to_owned()), + DocumentTypeRef::V2(v2) => DocumentType::V2((*v2).to_owned()), } } } diff --git a/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs b/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs new file mode 100644 index 00000000000..5c178ce3be8 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs @@ -0,0 +1,226 @@ +use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV0MutGetters, DocumentTypeV0Setters, DocumentTypeV1Getters, + DocumentTypeV2Getters, DocumentTypeV2Setters, +}; +use crate::data_contract::document_type::index::Index; +use crate::data_contract::document_type::index_level::IndexLevel; +use crate::data_contract::document_type::property::DocumentProperty; + +use platform_value::{Identifier, Value}; + +use crate::data_contract::document_type::restricted_creation::CreationRestrictionMode; +use crate::data_contract::document_type::token_costs::accessors::TokenCostGettersV0; +use crate::data_contract::document_type::v2::DocumentTypeV2; +#[cfg(feature = "validation")] +use crate::data_contract::document_type::validator::StatelessJsonSchemaLazyValidator; +use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; +use crate::data_contract::TokenContractPosition; +use crate::document::transfer::Transferable; +use crate::identity::SecurityLevel; +use crate::nft::TradeMode; +use crate::tokens::token_amount_on_contract_token::DocumentActionTokenCost; +use indexmap::IndexMap; +use std::collections::{BTreeMap, BTreeSet}; + +impl DocumentTypeV0MutGetters for DocumentTypeV2 { + fn schema_mut(&mut self) -> &mut Value { + &mut self.schema + } +} + +impl DocumentTypeV0Getters for DocumentTypeV2 { + fn name(&self) -> &String { + &self.name + } + + fn schema(&self) -> &Value { + &self.schema + } + + fn schema_owned(self) -> Value { + self.schema + } + + fn indexes(&self) -> &BTreeMap { + &self.indices + } + + fn find_contested_index(&self) -> Option<&Index> { + self.indices + .iter() + .find(|(_, index)| index.contested_index.is_some()) + .map(|(_, contested_index)| contested_index) + } + + fn index_structure(&self) -> &IndexLevel { + &self.index_structure + } + + fn flattened_properties(&self) -> &IndexMap { + &self.flattened_properties + } + + fn properties(&self) -> &IndexMap { + &self.properties + } + + fn identifier_paths(&self) -> &BTreeSet { + &self.identifier_paths + } + + fn binary_paths(&self) -> &BTreeSet { + &self.binary_paths + } + + fn required_fields(&self) -> &BTreeSet { + &self.required_fields + } + fn transient_fields(&self) -> &BTreeSet { + &self.transient_fields + } + + fn documents_keep_history(&self) -> bool { + self.documents_keep_history + } + + fn documents_mutable(&self) -> bool { + self.documents_mutable + } + + fn documents_can_be_deleted(&self) -> bool { + self.documents_can_be_deleted + } + + fn documents_transferable(&self) -> Transferable { + self.documents_transferable + } + + fn trade_mode(&self) -> TradeMode { + self.trade_mode + } + + fn creation_restriction_mode(&self) -> CreationRestrictionMode { + self.creation_restriction_mode + } + + fn data_contract_id(&self) -> Identifier { + self.data_contract_id + } + + fn requires_identity_encryption_bounded_key(&self) -> Option { + self.requires_identity_encryption_bounded_key + } + + fn requires_identity_decryption_bounded_key(&self) -> Option { + self.requires_identity_decryption_bounded_key + } + + fn security_level_requirement(&self) -> SecurityLevel { + self.security_level_requirement + } + + #[cfg(feature = "validation")] + fn json_schema_validator_ref(&self) -> &StatelessJsonSchemaLazyValidator { + &self.json_schema_validator + } +} + +impl DocumentTypeV0Setters for DocumentTypeV2 { + fn set_data_contract_id(&mut self, data_contract_id: Identifier) { + self.data_contract_id = data_contract_id; + } +} + +impl DocumentTypeV1Getters for DocumentTypeV2 { + fn document_creation_token_cost(&self) -> Option { + self.token_costs.document_creation_token_cost() + } + + fn document_replacement_token_cost(&self) -> Option { + self.token_costs.document_replacement_token_cost() + } + + fn document_deletion_token_cost(&self) -> Option { + self.token_costs.document_deletion_token_cost() + } + + fn document_transfer_token_cost(&self) -> Option { + self.token_costs.document_transfer_token_cost() + } + + fn document_update_price_token_cost(&self) -> Option { + self.token_costs.document_price_update_token_cost() + } + + fn document_purchase_token_cost(&self) -> Option { + self.token_costs.document_purchase_token_cost() + } + + fn all_document_token_costs(&self) -> Vec<&DocumentActionTokenCost> { + let mut result = Vec::new(); + + if let Some(cost) = self.token_costs.document_creation_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_replacement_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_deletion_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_transfer_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_price_update_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_purchase_token_cost_ref() { + result.push(cost); + } + + result + } + + fn all_external_token_costs_contract_tokens( + &self, + ) -> BTreeMap> { + let mut map = BTreeMap::new(); + + for cost in self.all_document_token_costs() { + if let Some(contract_id) = cost.contract_id { + map.entry(contract_id) + .or_insert_with(BTreeSet::new) + .insert(cost.token_contract_position); + } + } + + map + } +} + +impl DocumentTypeV2Getters for DocumentTypeV2 { + fn documents_countable(&self) -> bool { + self.documents_countable || self.range_countable + } + + fn range_countable(&self) -> bool { + self.range_countable + } +} + +impl DocumentTypeV2Setters for DocumentTypeV2 { + fn set_documents_countable(&mut self, countable: bool) { + self.documents_countable = countable; + if !countable { + // Preserve invariant: range_countable implies documents_countable + self.range_countable = false; + } + } + + fn set_range_countable(&mut self, range_countable: bool) { + self.range_countable = range_countable; + if range_countable { + self.documents_countable = true; + } + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs new file mode 100644 index 00000000000..c5daf542be7 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs @@ -0,0 +1,306 @@ +use indexmap::IndexMap; +use std::collections::{BTreeMap, BTreeSet}; + +use crate::data_contract::document_type::index::Index; +use crate::data_contract::document_type::index_level::IndexLevel; +use crate::data_contract::document_type::property::DocumentProperty; +use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; + +use crate::data_contract::document_type::methods::{ + DocumentTypeBasicMethods, DocumentTypeV0Methods, +}; +use crate::data_contract::document_type::restricted_creation::CreationRestrictionMode; +use crate::data_contract::document_type::token_costs::accessors::TokenCostSettersV0; +use crate::data_contract::document_type::token_costs::TokenCosts; +use crate::data_contract::document_type::v0::DocumentTypeV0; +use crate::data_contract::document_type::v1::DocumentTypeV1; +#[cfg(feature = "validation")] +use crate::data_contract::document_type::validator::StatelessJsonSchemaLazyValidator; +use crate::document::transfer::Transferable; +use crate::identity::SecurityLevel; +use crate::nft::TradeMode; +use crate::tokens::token_amount_on_contract_token::DocumentActionTokenCost; +use platform_value::{Identifier, Value}; + +mod accessors; +#[cfg(feature = "random-document-types")] +pub mod random_document_type; + +#[derive(Debug, PartialEq, Clone)] +pub struct DocumentTypeV2 { + pub(in crate::data_contract) name: String, + pub(in crate::data_contract) schema: Value, + pub(in crate::data_contract) indices: BTreeMap, + pub(in crate::data_contract) index_structure: IndexLevel, + /// Flattened properties flatten all objects for quick lookups for indexes + /// Document field should not contain sub objects. + pub(in crate::data_contract) flattened_properties: IndexMap, + /// Document field can contain sub objects. + pub(in crate::data_contract) properties: IndexMap, + pub(in crate::data_contract) identifier_paths: BTreeSet, + pub(in crate::data_contract) binary_paths: BTreeSet, + /// The required fields on the document type + pub(in crate::data_contract) required_fields: BTreeSet, + /// The transient fields on the document type + pub(in crate::data_contract) transient_fields: BTreeSet, + /// Should documents keep history? + pub(in crate::data_contract) documents_keep_history: bool, + /// Are documents mutable? + pub(in crate::data_contract) documents_mutable: bool, + /// Can documents of this type be deleted? + pub(in crate::data_contract) documents_can_be_deleted: bool, + /// Can documents be transferred without a trade? + pub(in crate::data_contract) documents_transferable: Transferable, + /// How are these documents traded? + pub(in crate::data_contract) trade_mode: TradeMode, + /// Is document creation restricted? + pub(in crate::data_contract) creation_restriction_mode: CreationRestrictionMode, + /// The data contract id + pub(in crate::data_contract) data_contract_id: Identifier, + /// Encryption key storage requirements + pub(in crate::data_contract) requires_identity_encryption_bounded_key: + Option, + /// Decryption key storage requirements + pub(in crate::data_contract) requires_identity_decryption_bounded_key: + Option, + pub(in crate::data_contract) security_level_requirement: SecurityLevel, + #[cfg(feature = "validation")] + pub(in crate::data_contract) json_schema_validator: StatelessJsonSchemaLazyValidator, + /// The token costs associated with state transitions on this document type + pub(in crate::data_contract) token_costs: TokenCosts, + /// 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 range countable. + /// Implies documents_countable = true. + pub(in crate::data_contract) range_countable: bool, +} + +impl DocumentTypeBasicMethods for DocumentTypeV2 {} + +impl DocumentTypeV0Methods for DocumentTypeV2 {} + +impl crate::data_contract::document_type::accessors::DocumentTypeV1Setters for DocumentTypeV2 { + fn set_document_creation_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_creation_token_cost(cost) + } + + fn set_document_replacement_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_replacement_token_cost(cost) + } + + fn set_document_deletion_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_deletion_token_cost(cost) + } + + fn set_document_transfer_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_transfer_token_cost(cost) + } + + fn set_document_price_update_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_price_update_token_cost(cost) + } + + fn set_document_purchase_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_purchase_token_cost(cost) + } +} + +impl From for DocumentTypeV2 { + fn from(value: DocumentTypeV0) -> Self { + DocumentTypeV2 { + name: value.name, + schema: value.schema, + indices: value.indices, + index_structure: value.index_structure, + flattened_properties: value.flattened_properties, + properties: value.properties, + identifier_paths: value.identifier_paths, + binary_paths: value.binary_paths, + required_fields: value.required_fields, + transient_fields: value.transient_fields, + documents_keep_history: value.documents_keep_history, + documents_mutable: value.documents_mutable, + documents_can_be_deleted: value.documents_can_be_deleted, + documents_transferable: value.documents_transferable, + trade_mode: value.trade_mode, + creation_restriction_mode: value.creation_restriction_mode, + data_contract_id: value.data_contract_id, + requires_identity_encryption_bounded_key: value + .requires_identity_encryption_bounded_key, + requires_identity_decryption_bounded_key: value + .requires_identity_decryption_bounded_key, + security_level_requirement: value.security_level_requirement, + #[cfg(feature = "validation")] + json_schema_validator: value.json_schema_validator, + token_costs: TokenCosts::V0(Default::default()), + documents_countable: false, + range_countable: false, + } + } +} + +impl From for DocumentTypeV2 { + fn from(value: DocumentTypeV1) -> Self { + DocumentTypeV2 { + name: value.name, + schema: value.schema, + indices: value.indices, + index_structure: value.index_structure, + flattened_properties: value.flattened_properties, + properties: value.properties, + identifier_paths: value.identifier_paths, + binary_paths: value.binary_paths, + required_fields: value.required_fields, + transient_fields: value.transient_fields, + documents_keep_history: value.documents_keep_history, + documents_mutable: value.documents_mutable, + documents_can_be_deleted: value.documents_can_be_deleted, + documents_transferable: value.documents_transferable, + trade_mode: value.trade_mode, + creation_restriction_mode: value.creation_restriction_mode, + data_contract_id: value.data_contract_id, + requires_identity_encryption_bounded_key: value + .requires_identity_encryption_bounded_key, + requires_identity_decryption_bounded_key: value + .requires_identity_decryption_bounded_key, + security_level_requirement: value.security_level_requirement, + #[cfg(feature = "validation")] + json_schema_validator: value.json_schema_validator, + token_costs: value.token_costs, + documents_countable: false, + range_countable: false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV2Getters, DocumentTypeV2Setters, + }; + use crate::data_contract::document_type::v0::DocumentTypeV0; + use crate::data_contract::document_type::DocumentType; + + fn make_v0() -> DocumentTypeV0 { + DocumentTypeV0 { + name: "test".to_string(), + schema: Value::Null, + indices: BTreeMap::new(), + index_structure: IndexLevel::try_from_indices( + Vec::::new(), + "test", + platform_version::version::PlatformVersion::latest(), + ) + .unwrap(), + flattened_properties: IndexMap::new(), + properties: IndexMap::new(), + identifier_paths: BTreeSet::new(), + binary_paths: BTreeSet::new(), + required_fields: BTreeSet::new(), + transient_fields: BTreeSet::new(), + documents_keep_history: false, + documents_mutable: true, + documents_can_be_deleted: true, + documents_transferable: Transferable::Never, + trade_mode: TradeMode::None, + creation_restriction_mode: CreationRestrictionMode::NoRestrictions, + data_contract_id: Identifier::default(), + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + security_level_requirement: SecurityLevel::HIGH, + #[cfg(feature = "validation")] + json_schema_validator: Default::default(), + } + } + + #[test] + fn from_v0_sets_countable_and_blast_to_false() { + let v2: DocumentTypeV2 = make_v0().into(); + assert!(!v2.documents_countable); + assert!(!v2.range_countable); + } + + #[test] + fn from_v1_sets_countable_and_blast_to_false() { + let v1: DocumentTypeV1 = make_v0().into(); + let v2: DocumentTypeV2 = v1.into(); + assert!(!v2.documents_countable); + assert!(!v2.range_countable); + } + + #[test] + fn documents_countable_getter() { + let mut v2: DocumentTypeV2 = make_v0().into(); + assert!(!v2.documents_countable()); + v2.documents_countable = true; + assert!(v2.documents_countable()); + } + + #[test] + fn range_countable_implies_documents_countable() { + let mut v2: DocumentTypeV2 = make_v0().into(); + v2.range_countable = true; + assert!(v2.documents_countable()); + assert!(v2.range_countable()); + } + + #[test] + fn set_range_countable_also_sets_documents_countable() { + let mut v2: DocumentTypeV2 = make_v0().into(); + v2.set_range_countable(true); + assert!(v2.range_countable); + assert!(v2.documents_countable); + } + + #[test] + fn set_documents_countable_true_does_not_affect_blast() { + let mut v2: DocumentTypeV2 = make_v0().into(); + v2.set_documents_countable(true); + assert!(v2.documents_countable()); + assert!(!v2.range_countable()); + } + + #[test] + fn set_documents_countable_false_clears_range_countable() { + let mut v2: DocumentTypeV2 = make_v0().into(); + v2.set_range_countable(true); + assert!(v2.range_countable()); + assert!(v2.documents_countable()); + + // Setting countable to false must also clear range_countable + v2.set_documents_countable(false); + assert!(!v2.documents_countable()); + assert!(!v2.range_countable()); + } + + #[test] + fn v2_preserves_v0_fields() { + let v0 = make_v0(); + let v2: DocumentTypeV2 = v0.into(); + assert_eq!(v2.name(), "test"); + assert!(v2.documents_mutable()); + assert!(v2.documents_can_be_deleted()); + } + + #[test] + fn document_type_enum_v0_v1_return_false() { + let dt = DocumentType::V0(make_v0()); + assert!(!dt.documents_countable()); + assert!(!dt.range_countable()); + + let dt = DocumentType::V1(make_v0().into()); + assert!(!dt.documents_countable()); + assert!(!dt.range_countable()); + } + + #[test] + fn document_type_enum_v2_dispatch() { + let mut v2: DocumentTypeV2 = make_v0().into(); + v2.documents_countable = true; + v2.range_countable = true; + let dt = DocumentType::V2(v2); + assert!(dt.documents_countable()); + assert!(dt.range_countable()); + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs b/packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs new file mode 100644 index 00000000000..849206ab2a6 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/v2/random_document_type.rs @@ -0,0 +1,40 @@ +use crate::data_contract::document_type::v0::random_document_type::RandomDocumentTypeParameters; +use crate::data_contract::document_type::v1::DocumentTypeV1; +use crate::data_contract::document_type::v2::DocumentTypeV2; +use crate::version::PlatformVersion; +use crate::ProtocolError; +use platform_value::Identifier; +use rand::rngs::StdRng; + +impl DocumentTypeV2 { + pub fn random_document_type( + parameters: RandomDocumentTypeParameters, + data_contract_id: Identifier, + rng: &mut StdRng, + platform_version: &PlatformVersion, + ) -> Result { + Ok(DocumentTypeV1::random_document_type( + parameters, + data_contract_id, + rng, + platform_version, + )? + .into()) + } + + /// This is used to create an invalid random document type, often for testing + pub fn invalid_random_document_type( + parameters: RandomDocumentTypeParameters, + data_contract_id: Identifier, + rng: &mut StdRng, + platform_version: &PlatformVersion, + ) -> Result { + Ok(DocumentTypeV1::invalid_random_document_type( + parameters, + data_contract_id, + rng, + platform_version, + )? + .into()) + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs index 9657db4afa3..a3b07876b5c 100644 --- a/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs @@ -497,6 +497,7 @@ mod tests { { DocumentTypeMutRef::V0(dt) => dt.documents_mutable = false, DocumentTypeMutRef::V1(dt) => dt.documents_mutable = false, + DocumentTypeMutRef::V2(dt) => dt.documents_mutable = false, } let result = old_data_contract diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs index 2dc73f217df..a97f11ad63e 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs @@ -12,6 +12,7 @@ use dpp::data_contract::config::v0::DataContractConfigGettersV0; use dpp::data_contract::DataContract; use dpp::fee::fee_result::FeeResult; +use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; use dpp::data_contract::document_type::methods::DocumentTypeBasicMethods; use dpp::serialization::PlatformSerializableWithPlatformVersion; @@ -284,14 +285,34 @@ impl Drive { ]; // primary key tree - 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, - )?; + if document_type.range_countable() { + // Use a ProvableCountTree for range countable support (implies countable) + let path_items: Vec> = 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> = 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, + )?; + } let mut index_cache: HashSet<&[u8]> = HashSet::new(); // for each type we should insert the indices that are top level diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs index ec1fd49f720..d3817a3fcce 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs @@ -9,7 +9,7 @@ use crate::util::storage_flags::StorageFlags; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::config::v0::DataContractConfigGettersV0; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; use dpp::data_contract::DataContract; use dpp::fee::fee_result::FeeResult; @@ -349,13 +349,35 @@ impl Drive { ]; // primary key tree - self.batch_insert_empty_tree( - type_path, - KeyRef(&[0]), - storage_flags.as_ref().map(|flags| flags.as_ref()), - &mut batch_operations, - drive_version, - )?; + if document_type.range_countable() { + // Use a ProvableCountTree for range countable support (implies countable) + let path_items: Vec> = 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().map(|flags| 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> = 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().map(|flags| flags.as_ref()), + ), + ); + } else { + self.batch_insert_empty_tree( + type_path, + KeyRef(&[0]), + storage_flags.as_ref().map(|flags| flags.as_ref()), + &mut batch_operations, + drive_version, + )?; + } let mut index_cache: HashSet<&[u8]> = HashSet::new(); // for each type we should insert the indices that are top level diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs index 55420aceeb7..58b1277bdeb 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs @@ -1,6 +1,7 @@ +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; use grovedb::batch::KeyInfoPath; -use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; use dpp::data_contract::document_type::DocumentTypeRef; @@ -109,8 +110,9 @@ impl Drive { estimated_costs_only_with_layer_info, &platform_version.drive, )?; + let primary_key_tree_type = document_type.primary_key_tree_type(platform_version)?; DirectQueryType::StatelessDirectQuery { - in_tree_type: TreeType::NormalTree, + in_tree_type: primary_key_tree_type, query_target: QueryTargetValue( document_type.estimated_size(platform_version)? as u32 ), diff --git a/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs index ce3ca4d6e5b..f96fafe4fe3 100644 --- a/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v0/mod.rs @@ -1,8 +1,9 @@ +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; use grovedb::batch::KeyInfoPath; use grovedb::EstimatedLayerCount::PotentiallyAtMaxElements; +use grovedb::EstimatedLayerInformation; use grovedb::EstimatedLayerSizes::AllItems; -use grovedb::{EstimatedLayerInformation, TreeType}; use dpp::data_contract::document_type::DocumentTypeRef; @@ -64,10 +65,11 @@ impl Drive { None }; let flags_size = StorageFlags::approximate_size(true, approximate_size); + let primary_key_tree_type = document_type.primary_key_tree_type(platform_version)?; estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_path(primary_key_path), EstimatedLayerInformation { - tree_type: TreeType::NormalTree, + tree_type: primary_key_tree_type, estimated_layer_count: PotentiallyAtMaxElements, estimated_layer_sizes: AllItems( DEFAULT_HASH_SIZE_U8, diff --git a/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v0/mod.rs index 75a03dd83cc..7f70fb9054d 100644 --- a/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v0/mod.rs @@ -1,6 +1,7 @@ +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; use grovedb::batch::KeyInfoPath; -use grovedb::{EstimatedLayerInformation, MaybeTree, TransactionArg, TreeType}; +use grovedb::{EstimatedLayerInformation, MaybeTree, TransactionArg}; use dpp::data_contract::document_type::DocumentTypeRef; @@ -37,9 +38,11 @@ impl Drive { batch_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result<(), Error> { + let primary_key_tree_type = document_type.primary_key_tree_type(platform_version)?; + let apply_type = if estimated_costs_only_with_layer_info.is_some() { StatelessBatchDelete { - in_tree_type: TreeType::NormalTree, + in_tree_type: primary_key_tree_type, estimated_key_size: DEFAULT_HASH_SIZE_U32, estimated_value_size: document_type.estimated_size(platform_version)? as u32, } diff --git a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs index 5185812f53b..e9ccd48fef0 100644 --- a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_add_document_to_primary_storage/v0/mod.rs @@ -1,5 +1,6 @@ use crate::drive::constants::{AVERAGE_NUMBER_OF_UPDATES, AVERAGE_UPDATE_BYTE_COUNT_REQUIRED_SIZE}; use crate::drive::document::paths::contract_documents_keeping_history_primary_key_path_for_document_id; +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; use crate::util::storage_flags::StorageFlags; use crate::drive::Drive; @@ -70,6 +71,7 @@ impl Drive { }; let contract = document_and_contract_info.contract; let document_type = document_and_contract_info.document_type; + let primary_key_tree_type = document_type.primary_key_tree_type(platform_version)?; // at this level we have all the documents for the contract if document_type.documents_keep_history() { // if we keep history this level has trees @@ -84,7 +86,7 @@ impl Drive { estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_path(primary_key_path), EstimatedLayerInformation { - tree_type: TreeType::NormalTree, + tree_type: primary_key_tree_type, estimated_layer_count: PotentiallyAtMaxElements, estimated_layer_sizes: AllSubtrees( DEFAULT_HASH_SIZE_U8, @@ -137,7 +139,7 @@ impl Drive { estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_path(primary_key_path), EstimatedLayerInformation { - tree_type: TreeType::NormalTree, + tree_type: primary_key_tree_type, estimated_layer_count: PotentiallyAtMaxElements, estimated_layer_sizes: AllItems( DEFAULT_HASH_SIZE_U8, diff --git a/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs index 2993e01b996..5fd6f8c0c5b 100644 --- a/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v0/mod.rs @@ -1,4 +1,5 @@ use crate::drive::document::paths::contract_documents_primary_key_path; +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; @@ -12,7 +13,7 @@ use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; -use grovedb::{EstimatedLayerInformation, TransactionArg, TreeType}; +use grovedb::{EstimatedLayerInformation, TransactionArg}; use std::collections::HashMap; impl Drive { @@ -37,12 +38,16 @@ impl Drive { document_and_contract_info.document_type.name().as_str(), ); + let primary_key_tree_type = document_and_contract_info + .document_type + .primary_key_tree_type(platform_version)?; + // Apply means stateful query let query_type = if estimated_costs_only_with_layer_info.is_none() { StatefulDirectQuery } else { StatelessDirectQuery { - in_tree_type: TreeType::NormalTree, + in_tree_type: primary_key_tree_type, query_target: QueryTargetValue( document_and_contract_info .document_type diff --git a/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs b/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs index e9df07d11fc..626d3304691 100644 --- a/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v0/mod.rs @@ -1,3 +1,4 @@ +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; use dpp::data_contract::document_type::DocumentPropertyType; use grovedb::batch::key_info::KeyInfo; @@ -67,6 +68,9 @@ impl Drive { let drive_version = &platform_version.drive; let contract = document_and_contract_info.contract; let document_type = document_and_contract_info.document_type; + + let primary_key_tree_type = document_type.primary_key_tree_type(platform_version)?; + let primary_key_path = contract_documents_primary_key_path( contract.id_ref().as_bytes(), document_type.name().as_str(), @@ -122,11 +126,14 @@ impl Drive { inserted_storage_flags, ) }; + + // The per-document history subtree is always NormalTree. + // The parent (primary key tree) may be CountTree/ProvableCountTree. let apply_type = if estimated_costs_only_with_layer_info.is_none() { BatchInsertTreeApplyType::StatefulBatchInsertTree } else { BatchInsertTreeApplyType::StatelessBatchInsertTree { - in_tree_type: TreeType::NormalTree, + in_tree_type: primary_key_tree_type, tree_type: TreeType::NormalTree, flags_len: storage_flags .map(|s| s.serialized_size()) @@ -134,6 +141,7 @@ impl Drive { } }; // we first insert an empty tree if the document is new + // The per-document subtree is always NormalTree (it holds history entries) self.batch_insert_empty_tree_if_not_exists( path_key_info, TreeType::NormalTree, @@ -438,7 +446,7 @@ impl Drive { BatchInsertApplyType::StatefulBatchInsert } else { BatchInsertApplyType::StatelessBatchInsert { - in_tree_type: TreeType::NormalTree, + in_tree_type: primary_key_tree_type, target: QueryTargetValue(document_type.estimated_size(platform_version)? as u32), } }; diff --git a/packages/rs-drive/src/drive/document/mod.rs b/packages/rs-drive/src/drive/document/mod.rs index fb0cd21529a..e157918bd8d 100644 --- a/packages/rs-drive/src/drive/document/mod.rs +++ b/packages/rs-drive/src/drive/document/mod.rs @@ -40,6 +40,10 @@ mod update; #[cfg(any(feature = "server", feature = "verify"))] pub mod paths; +/// Primary key tree type resolution +#[cfg(feature = "server")] +pub mod primary_key_tree_type; + #[cfg(feature = "server")] /// Creates a reference to a document. fn make_document_reference( diff --git a/packages/rs-drive/src/drive/document/primary_key_tree_type.rs b/packages/rs-drive/src/drive/document/primary_key_tree_type.rs new file mode 100644 index 00000000000..92c5643c37f --- /dev/null +++ b/packages/rs-drive/src/drive/document/primary_key_tree_type.rs @@ -0,0 +1,110 @@ +use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::version::PlatformVersion; +use grovedb::TreeType; + +use crate::error::drive::DriveError; +use crate::error::Error; + +/// Extension trait for `DocumentTypeRef` that provides the tree type used +/// for primary key storage in Drive. +pub trait DocumentTypePrimaryKeyTreeType { + /// Returns the `TreeType` used for the primary key storage tree. + /// + /// The primary key tree (key `[0]` under the document type path) stores + /// document references keyed by document ID. The tree type depends on the + /// document type's configuration: + /// + /// - `range_countable = true` → `ProvableCountTree` + /// - `documents_countable = true` → `CountTree` + /// - otherwise → `NormalTree` + fn primary_key_tree_type(&self, platform_version: &PlatformVersion) -> Result; +} + +impl DocumentTypePrimaryKeyTreeType for DocumentTypeRef<'_> { + fn primary_key_tree_type(&self, platform_version: &PlatformVersion) -> Result { + match platform_version + .drive + .methods + .document + .primary_key_tree_type + { + 0 => { + if self.range_countable() { + Ok(TreeType::ProvableCountTree) + } else if self.documents_countable() { + Ok(TreeType::CountTree) + } else { + Ok(TreeType::NormalTree) + } + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "DocumentTypeRef::primary_key_tree_type".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::document_type::accessors::DocumentTypeV2Setters; + use dpp::data_contract::document_type::DocumentType; + use dpp::tests::json_document::json_document_to_contract_with_ids; + use dpp::version::PlatformVersion; + + fn make_doc_type() -> DocumentType { + let pv = PlatformVersion::latest(); + let contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/family/family-contract.json", + None, + None, + false, + pv, + ) + .expect("contract"); + let dt = contract + .document_type_for_name("person") + .expect("person type"); + dt.to_owned_document_type() + } + + #[test] + fn default_is_normal_tree() { + let dt = make_doc_type(); + let pv = PlatformVersion::latest(); + let result = dt.as_ref().primary_key_tree_type(pv).unwrap(); + assert_eq!(result, TreeType::NormalTree); + } + + #[test] + fn countable_is_count_tree() { + let mut dt = make_doc_type(); + dt.set_documents_countable(true); + let pv = PlatformVersion::latest(); + let result = dt.as_ref().primary_key_tree_type(pv).unwrap(); + assert_eq!(result, TreeType::CountTree); + } + + #[test] + fn blast_is_provable_count_tree() { + let mut dt = make_doc_type(); + dt.set_range_countable(true); + let pv = PlatformVersion::latest(); + let result = dt.as_ref().primary_key_tree_type(pv).unwrap(); + assert_eq!(result, TreeType::ProvableCountTree); + } + + #[test] + fn blast_takes_priority_over_countable() { + let mut dt = make_doc_type(); + dt.set_documents_countable(true); + dt.set_range_countable(true); + let pv = PlatformVersion::latest(); + let result = dt.as_ref().primary_key_tree_type(pv).unwrap(); + assert_eq!(result, TreeType::ProvableCountTree); + } +} diff --git a/packages/rs-drive/src/fees/op.rs b/packages/rs-drive/src/fees/op.rs index 7ed6bd5be52..ae7523804ab 100644 --- a/packages/rs-drive/src/fees/op.rs +++ b/packages/rs-drive/src/fees/op.rs @@ -443,6 +443,23 @@ impl LowLevelDriveOperation { LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree) } + /// Sets `GroveOperation` for inserting an empty provable count tree at the given path and key + pub fn for_known_path_key_empty_provable_count_tree( + path: Vec>, + key: Vec, + storage_flags: Option<&StorageFlags>, + ) -> Self { + let tree = match storage_flags { + Some(storage_flags) => Element::new_provable_count_tree_with_flags( + None, + storage_flags.to_some_element_flags(), + ), + None => Element::empty_provable_count_tree(), + }; + + LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree) + } + /// Sets `GroveOperation` for inserting an empty tree at the given path and key pub fn for_estimated_path_key_empty_tree( path: KeyInfoPath, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs index ea970e7d982..d4148ab1205 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs @@ -2,6 +2,7 @@ use versioned_feature_core::{FeatureVersion, FeatureVersionBounds}; pub mod v1; pub mod v2; pub mod v3; +pub mod v4; #[derive(Clone, Debug, Default)] pub struct DPPContractVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs new file mode 100644 index 00000000000..759b7687978 --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs @@ -0,0 +1,66 @@ +use crate::version::dpp_versions::dpp_contract_versions::{ + DPPContractVersions, DataContractMethodVersions, DocumentTypeClassMethodVersions, + DocumentTypeIndexVersions, DocumentTypeMethodVersions, DocumentTypeSchemaVersions, + DocumentTypeVersions, RecursiveSchemaValidatorVersions, TokenVersions, +}; +use versioned_feature_core::FeatureVersionBounds; + +// Introduced in protocol version 12, adds documents_countable feature via try_from_schema v2 +pub const CONTRACT_VERSIONS_V4: DPPContractVersions = DPPContractVersions { + max_serialized_size: 65000, + contract_serialization_version: FeatureVersionBounds { + min_version: 0, + max_version: 1, + default_current_version: 1, + }, + contract_structure_version: 1, + created_data_contract_structure: 0, + config: FeatureVersionBounds { + min_version: 1, + max_version: 1, + default_current_version: 1, + }, + methods: DataContractMethodVersions { + validate_document: 0, + validate_update: 0, + schema: 0, + validate_groups: 0, + equal_ignoring_time_fields: 0, + registration_cost: 1, + }, + document_type_versions: DocumentTypeVersions { + index_versions: DocumentTypeIndexVersions { + index_levels_from_indices: 0, + }, + class_method_versions: DocumentTypeClassMethodVersions { + try_from_schema: 2, // changed: supports documentsCountable + create_document_types_from_document_schemas: 1, + }, + structure_version: 0, + schema: DocumentTypeSchemaVersions { + should_add_creator_id: 1, + enrich_with_base_schema: 0, + find_identifier_and_binary_paths: 0, + validate_max_depth: 0, + max_depth: 256, + recursive_schema_validator_versions: RecursiveSchemaValidatorVersions { + traversal_validator: 0, + }, + validate_schema_compatibility: 0, + }, + methods: DocumentTypeMethodVersions { + create_document_from_data: 0, + create_document_with_prevalidated_properties: 0, + prefunded_voting_balance_for_document: 0, + contested_vote_poll_for_document: 0, + estimated_size: 0, + index_for_types: 0, + max_size: 0, + serialize_value_for_key: 0, + deserialize_value_for_key: 0, + }, + }, + token_versions: TokenVersions { + validate_structure_interval: 0, + }, +}; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs index 8abec60c116..3834e48de42 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs @@ -12,6 +12,7 @@ pub struct DriveDocumentMethodVersions { pub update: DriveDocumentUpdateMethodVersions, pub estimation_costs: DriveDocumentEstimationCostsMethodVersions, pub index_uniqueness: DriveDocumentIndexUniquenessMethodVersions, + pub primary_key_tree_type: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs index 91627b4c1ad..2d90ceb157e 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs @@ -67,4 +67,5 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V1: DriveDocumentMethodVersions = validate_document_purchase_transition_action_uniqueness: 0, validate_document_update_price_transition_action_uniqueness: 0, }, + primary_key_tree_type: 0, }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs index 60e06ac729e..9dcfdc800bc 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs @@ -69,4 +69,5 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V2: DriveDocumentMethodVersions = validate_document_purchase_transition_action_uniqueness: 1, // Changed validate_document_update_price_transition_action_uniqueness: 1, // Changed }, + primary_key_tree_type: 0, }; diff --git a/packages/rs-platform-version/src/version/v12.rs b/packages/rs-platform-version/src/version/v12.rs index fbe019d3afd..e7149151200 100644 --- a/packages/rs-platform-version/src/version/v12.rs +++ b/packages/rs-platform-version/src/version/v12.rs @@ -1,6 +1,6 @@ use crate::version::consensus_versions::ConsensusVersions; use crate::version::dpp_versions::dpp_asset_lock_versions::v1::DPP_ASSET_LOCK_VERSIONS_V1; -use crate::version::dpp_versions::dpp_contract_versions::v3::CONTRACT_VERSIONS_V3; +use crate::version::dpp_versions::dpp_contract_versions::v4::CONTRACT_VERSIONS_V4; use crate::version::dpp_versions::dpp_costs_versions::v1::DPP_COSTS_VERSIONS_V1; use crate::version::dpp_versions::dpp_document_versions::v3::DOCUMENT_VERSIONS_V3; use crate::version::dpp_versions::dpp_factory_versions::v1::DPP_FACTORY_VERSIONS_V1; @@ -51,7 +51,7 @@ pub const PLATFORM_V12: PlatformVersion = PlatformVersion { state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V1, state_transitions: STATE_TRANSITION_VERSIONS_V3, - contract_versions: CONTRACT_VERSIONS_V3, + contract_versions: CONTRACT_VERSIONS_V4, // changed: try_from_schema v2 supports documentsCountable document_versions: DOCUMENT_VERSIONS_V3, identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, From 409fea93488c95a48b690905c2ff91773edae321 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 16:33:56 +0700 Subject: [PATCH 02/14] fix(drive): strip v12-introduced flags from pre-v12 contracts on upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v11→v12 migration previously used ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES as its allowlist. After documentsCountable / rangeCountable were added to that list (so new v12 contracts can declare them under the v1 meta-schema's additionalProperties: false), the migration would no longer strip those flags from pre-v12 contracts that smuggled them into raw schema bytes. Post-upgrade, the v2 parser would revive the flags and make primary_key_tree_type() return CountTree / ProvableCountTree for a tree that was physically created as NormalTree, causing fee miscalculation across every document insert/delete/estimation path. Add a separate ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES list (V1 minus the v12-introduced flags) and a matching strip helper. The migration uses the pre-v12 helper so smuggled flags are stripped before the v2 parser ever sees them. The existing end-to-end migration test now also injects documentsCountable / rangeCountable and asserts they are absent post- migration both on disk and via the Drive fetch API. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../schema/allowed_top_level_properties.rs | 146 +++++++++++++++++- .../v0/mod.rs | 68 +++++--- ...trip_unknown_document_schema_properties.rs | 12 +- 3 files changed, 196 insertions(+), 30 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs b/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs index d716aac883e..91cdd587e45 100644 --- a/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs +++ b/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs @@ -1,11 +1,20 @@ use platform_value::Value; +/// Top-level property names introduced in protocol v12. Pre-v12 contracts +/// could not legitimately use these keys, but raw schema bytes may contain +/// them (the v0/v1 meta-schemas accept arbitrary booleans for these names). +/// The v11→v12 migration must strip these from stored contracts so the v2 +/// parser does not revive them and reinterpret a `NormalTree` as a +/// `CountTree` / `ProvableCountTree`. +pub const V12_INTRODUCED_DOCUMENT_SCHEMA_TOP_LEVEL_PROPERTIES: &[&str] = + &["documentsCountable", "rangeCountable"]; + /// The set of top-level property names allowed on a document type schema object /// as defined by the v1 document meta-schema. /// -/// Any key not in this list should be stripped from document type schemas -/// during the v12 protocol upgrade migration to prevent unknown properties -/// from changing storage semantics. +/// Used to validate new v12+ contracts. The v11→v12 migration uses the stricter +/// [`ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES`] list so smuggled v12-introduced +/// flags are stripped from pre-v12 contracts. pub const ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[ "type", "$schema", @@ -35,9 +44,41 @@ pub const ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[ "dependentRequired", ]; +/// Top-level property names that pre-v12 contracts were permitted to declare. +/// Equal to [`ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES`] minus +/// [`V12_INTRODUCED_DOCUMENT_SCHEMA_TOP_LEVEL_PROPERTIES`]. Used by the v11→v12 +/// migration so smuggled v12-introduced flags are stripped from stored pre-v12 +/// contracts before the v2 parser ever sees them. +pub const ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES: &[&str] = &[ + "type", + "$schema", + "$defs", + "indices", + "signatureSecurityLevelRequirement", + "documentsKeepHistory", + "documentsMutable", + "canBeDeleted", + "transferable", + "tradeMode", + "creationRestrictionMode", + "requiresIdentityEncryptionBoundedKey", + "requiresIdentityDecryptionBoundedKey", + "tokenCost", + "properties", + "transient", + "keywords", + "additionalProperties", + "required", + "$comment", + "description", + "minProperties", + "maxProperties", + "dependentRequired", +]; + /// Strips any top-level key from the document type schema `Value::Map` that -/// is not in the allowed set. Returns `true` if any keys were removed. -pub fn strip_unknown_properties_from_document_schema(schema: &mut Value) -> bool { +/// is not in `allowed`. Returns `true` if any keys were removed. +fn strip_to_allowlist(schema: &mut Value, allowed: &[&str]) -> bool { let map = match schema { Value::Map(map) => map, _ => return false, @@ -49,11 +90,27 @@ pub fn strip_unknown_properties_from_document_schema(schema: &mut Value) -> bool Value::Text(s) => s.as_str(), _ => return true, // keep non-string keys (shouldn't happen but safe) }; - ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES.contains(&key_str) + allowed.contains(&key_str) }); map.len() != before } +/// Strips any top-level key from the document type schema `Value::Map` that +/// is not in [`ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES`]. Returns `true` if any +/// keys were removed. +pub fn strip_unknown_properties_from_document_schema(schema: &mut Value) -> bool { + strip_to_allowlist(schema, ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES) +} + +/// Strips any top-level key from the document type schema `Value::Map` that +/// is not in [`ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES`]. This is the strip +/// invoked by the v11→v12 protocol upgrade migration so smuggled +/// v12-introduced flags are removed from stored pre-v12 contracts. Returns +/// `true` if any keys were removed. +pub fn strip_unknown_properties_from_pre_v12_document_schema(schema: &mut Value) -> bool { + strip_to_allowlist(schema, ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES) +} + #[cfg(test)] mod tests { use super::*; @@ -102,6 +159,83 @@ mod tests { assert!(!changed); } + #[test] + fn pre_v12_strip_removes_smuggled_v12_flags() { + // A pre-v12 contract that smuggled v12-introduced flags into its raw + // schema would, if left untouched at v12 upgrade, be reinterpreted by + // the v2 parser as countable / range-countable — a storage-layout + // mismatch with the underlying NormalTree. The migration must strip + // these flags. + let mut schema = platform_value!({ + "type": "object", + "properties": {}, + "additionalProperties": false, + "documentsCountable": true, + "rangeCountable": true, + }); + + let changed = strip_unknown_properties_from_pre_v12_document_schema(&mut schema); + assert!(changed, "expected the strip to report changes"); + + let map = schema.as_map().unwrap(); + let keys: Vec<&str> = map.iter().filter_map(|(k, _)| k.as_text()).collect(); + assert!(!keys.contains(&"documentsCountable")); + assert!(!keys.contains(&"rangeCountable")); + assert!(keys.contains(&"type")); + assert!(keys.contains(&"properties")); + assert!(keys.contains(&"additionalProperties")); + } + + #[test] + fn pre_v12_strip_keeps_v12_flags_under_v1_strip() { + // The non-pre-v12 strip is for new v12 contracts: documentsCountable / + // rangeCountable must survive there. + let mut schema = platform_value!({ + "type": "object", + "properties": {}, + "additionalProperties": false, + "documentsCountable": true, + "rangeCountable": true, + }); + + let changed = strip_unknown_properties_from_document_schema(&mut schema); + assert!(!changed); + } + + #[test] + fn pre_v12_allowlist_is_v1_minus_v12_introduced() { + let v1: std::collections::BTreeSet<&str> = ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES + .iter() + .copied() + .collect(); + let pre_v12: std::collections::BTreeSet<&str> = ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES + .iter() + .copied() + .collect(); + let v12_introduced: std::collections::BTreeSet<&str> = + V12_INTRODUCED_DOCUMENT_SCHEMA_TOP_LEVEL_PROPERTIES + .iter() + .copied() + .collect(); + + // pre_v12 ∪ v12_introduced == v1 + let union: std::collections::BTreeSet<&str> = + pre_v12.union(&v12_introduced).copied().collect(); + assert_eq!( + union, v1, + "pre-v12 allowlist + v12-introduced should equal the v1 allowlist" + ); + + // pre_v12 ∩ v12_introduced == ∅ + let intersection: std::collections::BTreeSet<&str> = + pre_v12.intersection(&v12_introduced).copied().collect(); + assert!( + intersection.is_empty(), + "pre-v12 allowlist must not contain v12-introduced properties: {:?}", + intersection + ); + } + #[test] fn allowlist_matches_v1_meta_schema_properties() { let v1_schema: serde_json::Value = serde_json::from_str(include_str!( diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index e8302729601..6f07ed40551 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -1015,7 +1015,11 @@ mod tests { ) .expect("expected to convert to serialization format"); - // Inject unknown property into the "person" document schema + // Inject unknown properties into the "person" document schema. These + // include both an arbitrary unknown key and the v12-introduced flags + // (`documentsCountable` / `rangeCountable`) — the latter must also be + // stripped from pre-v12 contracts so the v2 parser cannot revive them + // and reinterpret a NormalTree contract as a count tree post-upgrade. for (_doc_type_name, schema_value) in serialization_format.document_schemas_mut().iter_mut() { if let Some(map) = schema_value.as_map_mut() { @@ -1023,6 +1027,14 @@ mod tests { PlatformValue::Text("unknownSmuggled".to_string()), PlatformValue::Bool(true), )); + map.push(( + PlatformValue::Text("documentsCountable".to_string()), + PlatformValue::Bool(true), + )); + map.push(( + PlatformValue::Text("rangeCountable".to_string()), + PlatformValue::Bool(true), + )); } } @@ -1144,19 +1156,27 @@ mod tests { .expect("deserialize") .0; - let has_unknown_after = format_after.document_schemas().values().any(|schema| { - schema - .as_map() - .map(|map| { - map.iter() - .any(|(k, _)| k.as_text() == Some("unknownSmuggled")) - }) - .unwrap_or(false) - }); + let schema_has_key = |format: &DataContractInSerializationFormat, key: &str| -> bool { + format.document_schemas().values().any(|schema| { + schema + .as_map() + .map(|map| map.iter().any(|(k, _)| k.as_text() == Some(key))) + .unwrap_or(false) + }) + }; + assert!( - !has_unknown_after, + !schema_has_key(&format_after, "unknownSmuggled"), "Contract should NOT have unknownSmuggled property after v12 migration" ); + assert!( + !schema_has_key(&format_after, "documentsCountable"), + "Contract should NOT have smuggled documentsCountable after v12 migration" + ); + assert!( + !schema_has_key(&format_after, "rangeCountable"), + "Contract should NOT have smuggled rangeCountable after v12 migration" + ); // 7. Verify known properties are still present let has_type = format_after.document_schemas().values().any(|schema| { @@ -1207,19 +1227,27 @@ mod tests { ) .expect("convert to serialization format"); - let has_unknown_refetched = refetched_format.document_schemas().values().any(|schema| { - schema - .as_map() - .map(|map| { - map.iter() - .any(|(k, _)| k.as_text() == Some("unknownSmuggled")) + let schema_has_key_refetched = + |format: &DataContractInSerializationFormat, key: &str| -> bool { + format.document_schemas().values().any(|schema| { + schema + .as_map() + .map(|map| map.iter().any(|(k, _)| k.as_text() == Some(key))) + .unwrap_or(false) }) - .unwrap_or(false) - }); + }; assert!( - !has_unknown_refetched, + !schema_has_key_refetched(&refetched_format, "unknownSmuggled"), "Contract fetched through Drive API after migration should not have unknownSmuggled" ); + assert!( + !schema_has_key_refetched(&refetched_format, "documentsCountable"), + "Contract fetched through Drive API after migration should not have smuggled documentsCountable" + ); + assert!( + !schema_has_key_refetched(&refetched_format, "rangeCountable"), + "Contract fetched through Drive API after migration should not have smuggled rangeCountable" + ); } #[test] diff --git a/packages/rs-drive/src/drive/contract/migration/strip_unknown_document_schema_properties.rs b/packages/rs-drive/src/drive/contract/migration/strip_unknown_document_schema_properties.rs index bb50e2cd38e..bf42f86f55f 100644 --- a/packages/rs-drive/src/drive/contract/migration/strip_unknown_document_schema_properties.rs +++ b/packages/rs-drive/src/drive/contract/migration/strip_unknown_document_schema_properties.rs @@ -4,7 +4,7 @@ use crate::error::drive::DriveError; use crate::error::Error; use crate::query::QueryResultType; use crate::util::grove_operations::DirectQueryType; -use dpp::data_contract::document_type::schema::allowed_top_level_properties::strip_unknown_properties_from_document_schema; +use dpp::data_contract::document_type::schema::allowed_top_level_properties::strip_unknown_properties_from_pre_v12_document_schema; use dpp::data_contract::serialized_version::DataContractInSerializationFormat; use dpp::version::drive_versions::DriveVersion; use grovedb::{Element, PathQuery, Query, SizedQuery, Transaction}; @@ -12,8 +12,12 @@ use grovedb_path::SubtreePath; impl Drive { /// Iterates every data contract in state, checks each document type schema for - /// top-level properties not listed in the v1 document meta-schema, removes them, - /// and re-serializes the contract if anything changed. + /// top-level properties that pre-v12 contracts were not permitted to declare + /// (i.e. anything outside `ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES`), removes + /// them, and re-serializes the contract if anything changed. This includes + /// v12-introduced flags such as `documentsCountable` / `rangeCountable`, which + /// the v2 parser would otherwise revive on already-stored contracts and + /// reinterpret as a count tree mismatched with the underlying `NormalTree`. /// /// For historical contracts, all stored revisions are cleaned (not just the latest). /// @@ -166,7 +170,7 @@ impl Drive { let mut contract_modified = false; for (doc_type_name, schema_value) in serialization_format.document_schemas_mut().iter_mut() { - if strip_unknown_properties_from_document_schema(schema_value) { + if strip_unknown_properties_from_pre_v12_document_schema(schema_value) { tracing::info!( contract_id = hex::encode(contract_id_bytes), document_type = %doc_type_name, From 12f59e957e1c1f517023d726fe53a6144e3494be Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 16:54:28 +0700 Subject: [PATCH 03/14] chore(dpp): freeze v0 document meta-schema, remove leaked v12 flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0 document meta-schema is the validator that admitted every pre-v12 contract on disk; mutating it would change historical validation results and break consensus replay. Mark it frozen / view-only via a top-level \$comment, and remove documentsCountable / rangeCountable, which were inadvertently introduced into v0 by 51d18085d9. Those properties are v12+ and live in the v1 meta-schema; pre-v12 smuggling of the keys is handled by the v11→v12 migration strip (see commit 409fea9348). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../schema/meta_schemas/document/v0/document-meta.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json index 8ea5e256d08..79206f14bdb 100644 --- a/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json +++ b/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json @@ -1,6 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json", + "$comment": "FROZEN — DO NOT MODIFY. This v0 document meta-schema is the schema validator used to admit pre-v12 contracts and remains in view-only mode for state already on disk. Any new top-level property or rule MUST go in a newer meta-schema version (v1+) so historical validation results stay deterministic. Touching this file changes how every pre-v12 contract was admitted and breaks consensus replay.", "type": "object", "$defs": { "documentProperties": { @@ -505,14 +506,6 @@ ], "description": "Key requirements. 0 - Unique Non Replaceable, 1 - Multiple, 2 - Multiple with reference to latest key." }, - "documentsCountable": { - "type": "boolean", - "description": "When true, the primary key tree uses a CountTree enabling O(1) total document count queries. Only effective from protocol version 12." - }, - "rangeCountable": { - "type": "boolean", - "description": "When true, the primary key tree uses a ProvableCountTree enabling range countable. Only effective from protocol version 12. Implies documentsCountable." - }, "tokenCost": { "type": "object", "properties": { From 4d0b811aa46343e62868e98ab2fb0a59782d76ed Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 16:56:18 +0700 Subject: [PATCH 04/14] chore(dpp): mark v1 document meta-schema editable-until-3.1-release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a top-level \$comment noting that the v1 document meta-schema is still editable while Platform 3.1 is in development, but freezes once 3.1 ships and v12 activates on mainnet — after that, any change would alter historical validation results and break consensus replay. New top-level properties or rules must go in v2+. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../rs-dpp/schema/meta_schemas/document/v1/document-meta.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json index 1a185762741..e5e06f620bb 100644 --- a/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json +++ b/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json @@ -1,6 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json", + "$comment": "EDITABLE UNTIL 3.1 RELEASE — FROZEN AFTER. This v1 document meta-schema activates with protocol v12 (CONTRACT_VERSIONS_V4) and admits every v12+ contract written to disk. Once Platform 3.1 ships, mutating it would change historical validation results and break consensus replay. After release, any new top-level property or rule MUST go in a newer meta-schema version (v2+).", "type": "object", "$defs": { "documentProperties": { From da2be81e2406a3ecbb17e1fa8ca08e673097e034 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 17:05:14 +0700 Subject: [PATCH 05/14] test(dpp): regression tests for documents_countable / range_countable immutability validate_config rejects changes to documents_countable and range_countable because those flags determine the physical primary-key tree layout (NormalTree vs CountTree vs ProvableCountTree). The validate_config test module already covers the 9 other immutable config properties; add the missing two regression tests in the same should_return_invalid_result_when__is_changed pattern so a future refactor cannot silently drop the storage-layout guards. The range_countable test pins documentsCountable: true on both old and new so the documents_countable() check above passes equally and validate_config reaches the range_countable branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../methods/validate_update/v0/mod.rs | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs index 3f7ad290107..a2782615563 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v0/mod.rs @@ -981,6 +981,168 @@ mod tests { )] if e.additional_message() == "document type can not change the security level requirement for its updates: changing from MASTER to CRITICAL" ); } + + #[test] + fn should_return_invalid_result_when_documents_countable_is_changed() { + let platform_version = PlatformVersion::latest(); + let data_contract_id = Identifier::random(); + let document_type_name = "test"; + + let schema = platform_value!({ + "type": "object", + "properties": { + "test": { + "type": "string", + "position": 0, + } + }, + "documentsCountable": true, + "additionalProperties": false, + }); + + let config = DataContractConfig::default_for_version(platform_version) + .expect("should create a default config"); + + let old_document_type = DocumentType::try_from_schema( + data_contract_id, + 1, + config.version(), + document_type_name, + schema, + None, + &BTreeMap::new(), + &config, + false, + &mut Vec::new(), + platform_version, + ) + .expect("failed to create old document type"); + + let schema = platform_value!({ + "type": "object", + "properties": { + "test": { + "type": "string", + "position": 0, + } + }, + "documentsCountable": false, + "additionalProperties": false, + }); + + let config = DataContractConfig::default_for_version(platform_version) + .expect("should create a default config"); + + let new_document_type = DocumentType::try_from_schema( + data_contract_id, + 1, + config.version(), + document_type_name, + schema, + None, + &BTreeMap::new(), + &config, + false, + &mut Vec::new(), + platform_version, + ) + .expect("failed to create new document type"); + + let result = old_document_type + .as_ref() + .validate_config(new_document_type.as_ref()); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::StateError( + StateError::DocumentTypeUpdateError(e) + )] if e.additional_message() == "document type can not change whether its documents are countable: changing from true to false" + ); + } + + #[test] + fn should_return_invalid_result_when_range_countable_is_changed() { + // documents_countable must remain equal across old/new so that + // validate_config reaches the range_countable check below it. + // Setting documentsCountable: true on both keeps the + // documents_countable() getter true regardless of range_countable. + let platform_version = PlatformVersion::latest(); + let data_contract_id = Identifier::random(); + let document_type_name = "test"; + + let schema = platform_value!({ + "type": "object", + "properties": { + "test": { + "type": "string", + "position": 0, + } + }, + "documentsCountable": true, + "rangeCountable": false, + "additionalProperties": false, + }); + + let config = DataContractConfig::default_for_version(platform_version) + .expect("should create a default config"); + + let old_document_type = DocumentType::try_from_schema( + data_contract_id, + 1, + config.version(), + document_type_name, + schema, + None, + &BTreeMap::new(), + &config, + false, + &mut Vec::new(), + platform_version, + ) + .expect("failed to create old document type"); + + let schema = platform_value!({ + "type": "object", + "properties": { + "test": { + "type": "string", + "position": 0, + } + }, + "documentsCountable": true, + "rangeCountable": true, + "additionalProperties": false, + }); + + let config = DataContractConfig::default_for_version(platform_version) + .expect("should create a default config"); + + let new_document_type = DocumentType::try_from_schema( + data_contract_id, + 1, + config.version(), + document_type_name, + schema, + None, + &BTreeMap::new(), + &config, + false, + &mut Vec::new(), + platform_version, + ) + .expect("failed to create new document type"); + + let result = old_document_type + .as_ref() + .validate_config(new_document_type.as_ref()); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::StateError( + StateError::DocumentTypeUpdateError(e) + )] if e.additional_message() == "document type can not change whether it is range countable: changing from false to true" + ); + } } mod validate_schema { From e7f157d38905c65dbec8b83595561eb6b7518e49 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 17:10:38 +0700 Subject: [PATCH 06/14] =?UTF-8?q?chore(dpp):=20simplify=20pre-v12=20schema?= =?UTF-8?q?=20strip=20=E2=80=94=20drop=20unused=20symbols?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous shape introduced V12_INTRODUCED_DOCUMENT_SCHEMA_TOP_LEVEL_PROPERTIES, a strip_to_allowlist helper, and a strip_unknown_properties_from_document_schema (v1) helper. None had a production caller — they only powered tests that referred back to themselves. Keep only what the v11→v12 migration actually needs: - ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES (parity-tested against v1 meta-schema JSON, also referenced by rs-scripts). - ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES (= V1 list minus the v12-introduced flags, used by the migration strip). - strip_unknown_properties_from_pre_v12_document_schema (the migration's strip). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../schema/allowed_top_level_properties.rs | 99 +++---------------- 1 file changed, 14 insertions(+), 85 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs b/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs index 91cdd587e45..3ecc78ed28f 100644 --- a/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs +++ b/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs @@ -1,14 +1,5 @@ use platform_value::Value; -/// Top-level property names introduced in protocol v12. Pre-v12 contracts -/// could not legitimately use these keys, but raw schema bytes may contain -/// them (the v0/v1 meta-schemas accept arbitrary booleans for these names). -/// The v11→v12 migration must strip these from stored contracts so the v2 -/// parser does not revive them and reinterpret a `NormalTree` as a -/// `CountTree` / `ProvableCountTree`. -pub const V12_INTRODUCED_DOCUMENT_SCHEMA_TOP_LEVEL_PROPERTIES: &[&str] = - &["documentsCountable", "rangeCountable"]; - /// The set of top-level property names allowed on a document type schema object /// as defined by the v1 document meta-schema. /// @@ -45,10 +36,10 @@ pub const ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[ ]; /// Top-level property names that pre-v12 contracts were permitted to declare. -/// Equal to [`ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES`] minus -/// [`V12_INTRODUCED_DOCUMENT_SCHEMA_TOP_LEVEL_PROPERTIES`]. Used by the v11→v12 -/// migration so smuggled v12-introduced flags are stripped from stored pre-v12 -/// contracts before the v2 parser ever sees them. +/// Equal to [`ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES`] minus the v12-introduced +/// flags (`documentsCountable`, `rangeCountable`). Used by the v11→v12 migration +/// so smuggled v12-introduced flags are stripped from stored pre-v12 contracts +/// before the v2 parser ever sees them. pub const ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES: &[&str] = &[ "type", "$schema", @@ -77,8 +68,12 @@ pub const ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES: &[&str] = &[ ]; /// Strips any top-level key from the document type schema `Value::Map` that -/// is not in `allowed`. Returns `true` if any keys were removed. -fn strip_to_allowlist(schema: &mut Value, allowed: &[&str]) -> bool { +/// is not in [`ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES`]. This is the strip +/// invoked by the v11→v12 protocol upgrade migration so unknown keys (including +/// smuggled v12-introduced flags such as `documentsCountable` / `rangeCountable`) +/// are removed from stored pre-v12 contracts. Returns `true` if any keys were +/// removed. +pub fn strip_unknown_properties_from_pre_v12_document_schema(schema: &mut Value) -> bool { let map = match schema { Value::Map(map) => map, _ => return false, @@ -90,27 +85,11 @@ fn strip_to_allowlist(schema: &mut Value, allowed: &[&str]) -> bool { Value::Text(s) => s.as_str(), _ => return true, // keep non-string keys (shouldn't happen but safe) }; - allowed.contains(&key_str) + ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES.contains(&key_str) }); map.len() != before } -/// Strips any top-level key from the document type schema `Value::Map` that -/// is not in [`ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES`]. Returns `true` if any -/// keys were removed. -pub fn strip_unknown_properties_from_document_schema(schema: &mut Value) -> bool { - strip_to_allowlist(schema, ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES) -} - -/// Strips any top-level key from the document type schema `Value::Map` that -/// is not in [`ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES`]. This is the strip -/// invoked by the v11→v12 protocol upgrade migration so smuggled -/// v12-introduced flags are removed from stored pre-v12 contracts. Returns -/// `true` if any keys were removed. -pub fn strip_unknown_properties_from_pre_v12_document_schema(schema: &mut Value) -> bool { - strip_to_allowlist(schema, ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES) -} - #[cfg(test)] mod tests { use super::*; @@ -126,7 +105,7 @@ mod tests { "anotherUnknown": 42 }); - let changed = strip_unknown_properties_from_document_schema(&mut schema); + let changed = strip_unknown_properties_from_pre_v12_document_schema(&mut schema); assert!(changed); let map = schema.as_map().unwrap(); @@ -148,14 +127,14 @@ mod tests { "$comment": "test" }); - let changed = strip_unknown_properties_from_document_schema(&mut schema); + let changed = strip_unknown_properties_from_pre_v12_document_schema(&mut schema); assert!(!changed); } #[test] fn handles_non_map_value() { let mut schema = Value::Text("not a map".to_string()); - let changed = strip_unknown_properties_from_document_schema(&mut schema); + let changed = strip_unknown_properties_from_pre_v12_document_schema(&mut schema); assert!(!changed); } @@ -186,56 +165,6 @@ mod tests { assert!(keys.contains(&"additionalProperties")); } - #[test] - fn pre_v12_strip_keeps_v12_flags_under_v1_strip() { - // The non-pre-v12 strip is for new v12 contracts: documentsCountable / - // rangeCountable must survive there. - let mut schema = platform_value!({ - "type": "object", - "properties": {}, - "additionalProperties": false, - "documentsCountable": true, - "rangeCountable": true, - }); - - let changed = strip_unknown_properties_from_document_schema(&mut schema); - assert!(!changed); - } - - #[test] - fn pre_v12_allowlist_is_v1_minus_v12_introduced() { - let v1: std::collections::BTreeSet<&str> = ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES - .iter() - .copied() - .collect(); - let pre_v12: std::collections::BTreeSet<&str> = ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES - .iter() - .copied() - .collect(); - let v12_introduced: std::collections::BTreeSet<&str> = - V12_INTRODUCED_DOCUMENT_SCHEMA_TOP_LEVEL_PROPERTIES - .iter() - .copied() - .collect(); - - // pre_v12 ∪ v12_introduced == v1 - let union: std::collections::BTreeSet<&str> = - pre_v12.union(&v12_introduced).copied().collect(); - assert_eq!( - union, v1, - "pre-v12 allowlist + v12-introduced should equal the v1 allowlist" - ); - - // pre_v12 ∩ v12_introduced == ∅ - let intersection: std::collections::BTreeSet<&str> = - pre_v12.intersection(&v12_introduced).copied().collect(); - assert!( - intersection.is_empty(), - "pre-v12 allowlist must not contain v12-introduced properties: {:?}", - intersection - ); - } - #[test] fn allowlist_matches_v1_meta_schema_properties() { let v1_schema: serde_json::Value = serde_json::from_str(include_str!( From 590aff4f545d8a23071cc9e8c358362b328d045a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 17:48:29 +0700 Subject: [PATCH 07/14] refactor(dpp): rename allowlist to ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The constant's actual semantic is "what survives the v11→v12 migration into v1-document-meta-schema-conforming bytes", not "what v1 meta-schema accepts". The two are deliberately different: v1 meta-schema admits the v12-introduced flags (documentsCountable, rangeCountable) for new v12 contracts, but those keys must NOT survive the migration on pre-v12 contracts — otherwise the v2 parser would revive them and reinterpret a NormalTree as a count tree. - Rename ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES → ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES. - Update the doc comment to spell out the subset relationship. - Relax the parity test from "allowlist == v1 properties" to "allowlist ⊂ v1 properties" — the reverse direction would block the v12 feature. - Update the rs-scripts caller to the new name. Migration call site keeps using strip_unknown_properties_from_document_schema (unchanged); only the constant it consults was renamed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../schema/allowed_top_level_properties.rs | 119 +++++------------- ...trip_unknown_document_schema_properties.rs | 4 +- .../src/bin/check_contract_properties.rs | 4 +- 3 files changed, 33 insertions(+), 94 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs b/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs index 3ecc78ed28f..38380b3e760 100644 --- a/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs +++ b/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs @@ -1,46 +1,15 @@ use platform_value::Value; -/// The set of top-level property names allowed on a document type schema object -/// as defined by the v1 document meta-schema. +/// Top-level property names allowed to survive the v11→v12 migration that +/// transitions stored contracts into v1-document-meta-schema-conforming bytes. /// -/// Used to validate new v12+ contracts. The v11→v12 migration uses the stricter -/// [`ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES`] list so smuggled v12-introduced -/// flags are stripped from pre-v12 contracts. -pub const ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[ - "type", - "$schema", - "$defs", - "indices", - "signatureSecurityLevelRequirement", - "documentsKeepHistory", - "documentsMutable", - "canBeDeleted", - "transferable", - "tradeMode", - "creationRestrictionMode", - "requiresIdentityEncryptionBoundedKey", - "requiresIdentityDecryptionBoundedKey", - "documentsCountable", - "rangeCountable", - "tokenCost", - "properties", - "transient", - "keywords", - "additionalProperties", - "required", - "$comment", - "description", - "minProperties", - "maxProperties", - "dependentRequired", -]; - -/// Top-level property names that pre-v12 contracts were permitted to declare. -/// Equal to [`ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES`] minus the v12-introduced -/// flags (`documentsCountable`, `rangeCountable`). Used by the v11→v12 migration -/// so smuggled v12-introduced flags are stripped from stored pre-v12 contracts -/// before the v2 parser ever sees them. -pub const ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES: &[&str] = &[ +/// This is intentionally a *subset* of v1 meta-schema's `properties`: keys +/// introduced for protocol v12 (e.g. `documentsCountable`, `rangeCountable`) +/// are accepted by the v1 meta-schema for new v12 contracts but must NOT +/// appear here, because pre-v12 contracts could not legitimately have set +/// them — leaving them in stored bytes would let the v2 parser reinterpret +/// a `NormalTree` as a count tree post-upgrade. +pub const ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[ "type", "$schema", "$defs", @@ -68,12 +37,8 @@ pub const ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES: &[&str] = &[ ]; /// Strips any top-level key from the document type schema `Value::Map` that -/// is not in [`ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES`]. This is the strip -/// invoked by the v11→v12 protocol upgrade migration so unknown keys (including -/// smuggled v12-introduced flags such as `documentsCountable` / `rangeCountable`) -/// are removed from stored pre-v12 contracts. Returns `true` if any keys were -/// removed. -pub fn strip_unknown_properties_from_pre_v12_document_schema(schema: &mut Value) -> bool { +/// is not in the allowed set. Returns `true` if any keys were removed. +pub fn strip_unknown_properties_from_document_schema(schema: &mut Value) -> bool { let map = match schema { Value::Map(map) => map, _ => return false, @@ -85,7 +50,7 @@ pub fn strip_unknown_properties_from_pre_v12_document_schema(schema: &mut Value) Value::Text(s) => s.as_str(), _ => return true, // keep non-string keys (shouldn't happen but safe) }; - ALLOWED_DOCUMENT_SCHEMA_PRE_V12_PROPERTIES.contains(&key_str) + ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES.contains(&key_str) }); map.len() != before } @@ -105,7 +70,7 @@ mod tests { "anotherUnknown": 42 }); - let changed = strip_unknown_properties_from_pre_v12_document_schema(&mut schema); + let changed = strip_unknown_properties_from_document_schema(&mut schema); assert!(changed); let map = schema.as_map().unwrap(); @@ -127,46 +92,26 @@ mod tests { "$comment": "test" }); - let changed = strip_unknown_properties_from_pre_v12_document_schema(&mut schema); + let changed = strip_unknown_properties_from_document_schema(&mut schema); assert!(!changed); } #[test] fn handles_non_map_value() { let mut schema = Value::Text("not a map".to_string()); - let changed = strip_unknown_properties_from_pre_v12_document_schema(&mut schema); + let changed = strip_unknown_properties_from_document_schema(&mut schema); assert!(!changed); } #[test] - fn pre_v12_strip_removes_smuggled_v12_flags() { - // A pre-v12 contract that smuggled v12-introduced flags into its raw - // schema would, if left untouched at v12 upgrade, be reinterpreted by - // the v2 parser as countable / range-countable — a storage-layout - // mismatch with the underlying NormalTree. The migration must strip - // these flags. - let mut schema = platform_value!({ - "type": "object", - "properties": {}, - "additionalProperties": false, - "documentsCountable": true, - "rangeCountable": true, - }); - - let changed = strip_unknown_properties_from_pre_v12_document_schema(&mut schema); - assert!(changed, "expected the strip to report changes"); - - let map = schema.as_map().unwrap(); - let keys: Vec<&str> = map.iter().filter_map(|(k, _)| k.as_text()).collect(); - assert!(!keys.contains(&"documentsCountable")); - assert!(!keys.contains(&"rangeCountable")); - assert!(keys.contains(&"type")); - assert!(keys.contains(&"properties")); - assert!(keys.contains(&"additionalProperties")); - } - - #[test] - fn allowlist_matches_v1_meta_schema_properties() { + fn transition_allowlist_is_subset_of_v1_meta_schema_properties() { + // Every key the migration is willing to keep must be a key the v1 + // meta-schema actually accepts; otherwise the migration would leave + // behind keys that v1 validation later rejects. The reverse direction + // is intentionally NOT enforced — v1 has v12-introduced properties + // (documentsCountable, rangeCountable) that pre-v12 contracts could + // not legitimately set, and those are deliberately excluded from the + // transition allowlist so they get stripped from stored bytes. let v1_schema: serde_json::Value = serde_json::from_str(include_str!( "../../../../schema/meta_schemas/document/v1/document-meta.json" )) @@ -180,25 +125,19 @@ mod tests { .map(|k| k.as_str()) .collect(); - let allowlist: std::collections::BTreeSet<&str> = ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES - .iter() - .copied() - .collect(); + let allowlist: std::collections::BTreeSet<&str> = + ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES + .iter() + .copied() + .collect(); let in_allowlist_not_schema: Vec<&&str> = allowlist.difference(&schema_properties).collect(); - let in_schema_not_allowlist: Vec<&&str> = - schema_properties.difference(&allowlist).collect(); assert!( in_allowlist_not_schema.is_empty(), - "Properties in ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES but not in v1 meta-schema: {:?}", + "Properties in ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES but not in v1 meta-schema: {:?}", in_allowlist_not_schema ); - assert!( - in_schema_not_allowlist.is_empty(), - "Properties in v1 meta-schema but not in ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES: {:?}", - in_schema_not_allowlist - ); } } diff --git a/packages/rs-drive/src/drive/contract/migration/strip_unknown_document_schema_properties.rs b/packages/rs-drive/src/drive/contract/migration/strip_unknown_document_schema_properties.rs index bf42f86f55f..b616385ccff 100644 --- a/packages/rs-drive/src/drive/contract/migration/strip_unknown_document_schema_properties.rs +++ b/packages/rs-drive/src/drive/contract/migration/strip_unknown_document_schema_properties.rs @@ -4,7 +4,7 @@ use crate::error::drive::DriveError; use crate::error::Error; use crate::query::QueryResultType; use crate::util::grove_operations::DirectQueryType; -use dpp::data_contract::document_type::schema::allowed_top_level_properties::strip_unknown_properties_from_pre_v12_document_schema; +use dpp::data_contract::document_type::schema::allowed_top_level_properties::strip_unknown_properties_from_document_schema; use dpp::data_contract::serialized_version::DataContractInSerializationFormat; use dpp::version::drive_versions::DriveVersion; use grovedb::{Element, PathQuery, Query, SizedQuery, Transaction}; @@ -170,7 +170,7 @@ impl Drive { let mut contract_modified = false; for (doc_type_name, schema_value) in serialization_format.document_schemas_mut().iter_mut() { - if strip_unknown_properties_from_pre_v12_document_schema(schema_value) { + if strip_unknown_properties_from_document_schema(schema_value) { tracing::info!( contract_id = hex::encode(contract_id_bytes), document_type = %doc_type_name, diff --git a/packages/rs-scripts/src/bin/check_contract_properties.rs b/packages/rs-scripts/src/bin/check_contract_properties.rs index cb588f57e35..30b2d0288fc 100644 --- a/packages/rs-scripts/src/bin/check_contract_properties.rs +++ b/packages/rs-scripts/src/bin/check_contract_properties.rs @@ -1,7 +1,7 @@ use clap::Parser; use dapi_grpc::platform::v0 as platform_proto; use dapi_grpc::platform::v0::platform_client::PlatformClient; -use dpp::data_contract::document_type::schema::allowed_top_level_properties::ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES; +use dpp::data_contract::document_type::schema::allowed_top_level_properties::ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES; use dpp::data_contract::serialized_version::DataContractInSerializationFormat; use dpp::platform_value::Identifier; use serde::Deserialize; @@ -336,7 +336,7 @@ async fn main() { dpp::platform_value::Value::Text(s) => s.as_str(), _ => return None, }; - if ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES.contains(&key_str) { + if ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES.contains(&key_str) { None } else { Some(key_str) From 142a7a2cfe3c9fdc28d8d35b5b617f1056970853 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 18:00:51 +0700 Subject: [PATCH 08/14] =?UTF-8?q?test(dpp):=20tighten=20transition=20allow?= =?UTF-8?q?list=20parity=20to=20v0=20=E2=88=AA=20known=20v1=20additions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous subset-of-v1 check only caught one direction of drift: it prevented adding allowlist entries that v1 doesn't accept, but happily allowed v1 to grow new top-level properties without anyone deciding whether the migration should preserve or strip them on pre-v12 contracts. Replace it with a strict equality: allowlist == v0_properties ∪ STANDARD_V1_JSON_SCHEMA_KEYWORDS_KEPT_IN_TRANSITION The new constant lists exactly the JSON-Schema-standard keywords v1 added that we deliberately preserve (required, \$comment, description, minProperties, maxProperties, dependentRequired) — none affect storage layout. Any future v1 addition (like the v12-introduced documentsCountable / rangeCountable that prompted this whole work) breaks both tests until someone explicitly decides: include in the additions list (preserve), or leave out (strip from pre-v12 bytes). Also add a sanity test that every entry in the additions list actually is a v1 addition (in v1, not v0). Also expand the constant's doc comment to spell out why the harmless JSON-Schema-standard keys are kept while documentsCountable / rangeCountable are excluded. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../schema/allowed_top_level_properties.rs | 123 ++++++++++++++---- 1 file changed, 99 insertions(+), 24 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs b/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs index 38380b3e760..3835902cbad 100644 --- a/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs +++ b/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs @@ -4,11 +4,18 @@ use platform_value::Value; /// transitions stored contracts into v1-document-meta-schema-conforming bytes. /// /// This is intentionally a *subset* of v1 meta-schema's `properties`: keys -/// introduced for protocol v12 (e.g. `documentsCountable`, `rangeCountable`) -/// are accepted by the v1 meta-schema for new v12 contracts but must NOT -/// appear here, because pre-v12 contracts could not legitimately have set -/// them — leaving them in stored bytes would let the v2 parser reinterpret -/// a `NormalTree` as a count tree post-upgrade. +/// introduced for protocol v12 (`documentsCountable`, `rangeCountable`) are +/// accepted by v1 for new v12 contracts but must NOT appear here, because +/// pre-v12 contracts could not legitimately have set them — leaving them +/// in stored bytes would let the v2 parser reinterpret a `NormalTree` as a +/// count tree post-upgrade. +/// +/// Standard JSON-Schema keywords v1 also adds at the top level (`required`, +/// `$comment`, `description`, `minProperties`, `maxProperties`, +/// `dependentRequired`) are kept: they are either pure documentation or +/// describe the document instance shape, not the storage layout, so +/// preserving them across the upgrade does not change consensus-critical +/// semantics. pub const ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[ "type", "$schema", @@ -103,24 +110,45 @@ mod tests { assert!(!changed); } + /// JSON-Schema-standard keywords that v1 meta-schema introduced at the + /// top level (over v0) and that the v11→v12 transition deliberately + /// preserves. They are either pure documentation (`$comment`, + /// `description`) or describe instance shape (`required`, + /// `minProperties`, `maxProperties`, `dependentRequired`) — none of + /// them affect storage layout, so keeping them across the upgrade does + /// not change consensus-critical semantics. + /// + /// Adding any other top-level property to v1 will fail + /// [`transition_allowlist_equals_v0_plus_known_v1_additions`] and + /// require an explicit decision: include it here (preserve across + /// transition) or leave it out (strip from pre-v12 bytes). + const STANDARD_V1_JSON_SCHEMA_KEYWORDS_KEPT_IN_TRANSITION: &[&str] = &[ + "required", + "$comment", + "description", + "minProperties", + "maxProperties", + "dependentRequired", + ]; + #[test] - fn transition_allowlist_is_subset_of_v1_meta_schema_properties() { - // Every key the migration is willing to keep must be a key the v1 - // meta-schema actually accepts; otherwise the migration would leave - // behind keys that v1 validation later rejects. The reverse direction - // is intentionally NOT enforced — v1 has v12-introduced properties - // (documentsCountable, rangeCountable) that pre-v12 contracts could - // not legitimately set, and those are deliberately excluded from the - // transition allowlist so they get stripped from stored bytes. - let v1_schema: serde_json::Value = serde_json::from_str(include_str!( - "../../../../schema/meta_schemas/document/v1/document-meta.json" + fn transition_allowlist_equals_v0_plus_known_v1_additions() { + // The migration cleans pre-v12 stored bytes. Pre-v12 contracts were + // validated against the v0 document meta-schema, so the allowlist + // must contain exactly v0's top-level properties plus a deliberately + // chosen set of v1-introduced JSON-Schema-standard keywords. Any + // other v1 addition (e.g. `documentsCountable` / `rangeCountable`) + // is excluded so the v2 parser cannot revive it on pre-v12 contracts + // and reinterpret a `NormalTree` as a count tree. + let v0_schema: serde_json::Value = serde_json::from_str(include_str!( + "../../../../schema/meta_schemas/document/v0/document-meta.json" )) - .expect("v1 document meta-schema JSON must be valid"); + .expect("v0 document meta-schema JSON must be valid"); - let schema_properties: std::collections::BTreeSet<&str> = v1_schema + let v0_properties: std::collections::BTreeSet<&str> = v0_schema .get("properties") .and_then(|p| p.as_object()) - .expect("v1 meta-schema must have a 'properties' object") + .expect("v0 meta-schema must have a 'properties' object") .keys() .map(|k| k.as_str()) .collect(); @@ -131,13 +159,60 @@ mod tests { .copied() .collect(); - let in_allowlist_not_schema: Vec<&&str> = - allowlist.difference(&schema_properties).collect(); + let known_v1_additions: std::collections::BTreeSet<&str> = + STANDARD_V1_JSON_SCHEMA_KEYWORDS_KEPT_IN_TRANSITION + .iter() + .copied() + .collect(); - assert!( - in_allowlist_not_schema.is_empty(), - "Properties in ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES but not in v1 meta-schema: {:?}", - in_allowlist_not_schema + let expected: std::collections::BTreeSet<&str> = + v0_properties.union(&known_v1_additions).copied().collect(); + + assert_eq!( + allowlist, expected, + "ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES must equal \ + v0 meta-schema properties ∪ STANDARD_V1_JSON_SCHEMA_KEYWORDS_KEPT_IN_TRANSITION", ); } + + #[test] + fn known_v1_additions_are_actually_in_v1_but_not_v0() { + // Sanity check: every entry in the known-v1-additions list must + // genuinely be a v1 addition (in v1, not in v0). Otherwise the + // constant is misnamed and the design intent has drifted. + let v0_schema: serde_json::Value = serde_json::from_str(include_str!( + "../../../../schema/meta_schemas/document/v0/document-meta.json" + )) + .expect("v0 document meta-schema JSON must be valid"); + let v1_schema: serde_json::Value = serde_json::from_str(include_str!( + "../../../../schema/meta_schemas/document/v1/document-meta.json" + )) + .expect("v1 document meta-schema JSON must be valid"); + + let v0_props: std::collections::BTreeSet<&str> = v0_schema + .get("properties") + .and_then(|p| p.as_object()) + .unwrap() + .keys() + .map(|k| k.as_str()) + .collect(); + let v1_props: std::collections::BTreeSet<&str> = v1_schema + .get("properties") + .and_then(|p| p.as_object()) + .unwrap() + .keys() + .map(|k| k.as_str()) + .collect(); + + for &k in STANDARD_V1_JSON_SCHEMA_KEYWORDS_KEPT_IN_TRANSITION { + assert!( + v1_props.contains(k), + "{k} is in STANDARD_V1_JSON_SCHEMA_KEYWORDS_KEPT_IN_TRANSITION but not in v1 meta-schema" + ); + assert!( + !v0_props.contains(k), + "{k} is in STANDARD_V1_JSON_SCHEMA_KEYWORDS_KEPT_IN_TRANSITION but already in v0 meta-schema (so not a v1 addition)" + ); + } + } } From 818768b7300607fc5448a4049abe4f98b3697cef Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 18:15:53 +0700 Subject: [PATCH 09/14] refactor(drive): add batch_insert_empty_count_tree / batch_insert_empty_provable_count_tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contract insert and update paths inlined the same documents_countable / range_countable → tree-variant if/else, calling LowLevelDriveOperation constructors directly and rebuilding the path Vec themselves. Add Drive helpers parallel to batch_insert_empty_tree / batch_insert_empty_sum_tree so call sites just pick the right method: if document_type.range_countable() { batch_insert_empty_provable_count_tree(...) } else if document_type.documents_countable() { batch_insert_empty_count_tree(...) } else { batch_insert_empty_tree(...) } - Add for_estimated_path_key_empty_count_tree / _empty_provable_count_tree on LowLevelDriveOperation so the new helpers can also handle KeySize cost-estimation paths. - Add batch_insert_empty_count_tree (mod.rs + v0/mod.rs + 3 unit tests) and batch_insert_empty_provable_count_tree (mod.rs + v0/mod.rs + 3 unit tests), both versioned through DriveGroveBatchMethodVersions. - Wire DriveGroveBatchMethodVersions with the two new fields, defaulted to 0 in v1 of drive_grove_method_versions. - Update insert_contract/v0 and update_contract/v0 to use the helpers. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../contract/insert/insert_contract/v0/mod.rs | 36 +++--- .../contract/update/update_contract/v0/mod.rs | 36 +++--- packages/rs-drive/src/fees/op.rs | 32 +++++ .../batch_insert_empty_count_tree/mod.rs | 55 +++++++++ .../batch_insert_empty_count_tree/v0/mod.rs | 108 +++++++++++++++++ .../mod.rs | 56 +++++++++ .../v0/mod.rs | 112 ++++++++++++++++++ .../rs-drive/src/util/grove_operations/mod.rs | 6 + .../drive_grove_method_versions/mod.rs | 2 + .../drive_grove_method_versions/v1.rs | 2 + 10 files changed, 409 insertions(+), 36 deletions(-) create mode 100644 packages/rs-drive/src/util/grove_operations/batch_insert_empty_count_tree/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/batch_insert_empty_count_tree/v0/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_tree/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_tree/v0/mod.rs diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs index a97f11ad63e..1afe813f21d 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs @@ -284,27 +284,29 @@ impl Drive { type_key.as_bytes(), ]; - // primary key tree + // primary key tree — pick the tree variant that matches the document + // type's countable flags. Pre-v12 contracts always have both flags + // false, so they fall through to the plain NormalTree path. + let key_info = Key(vec![0]); if document_type.range_countable() { - // Use a ProvableCountTree for range countable support (implies countable) - let path_items: Vec> = 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(), - ), - ); + // ProvableCountTree supports range-countable queries (implies countable). + self.batch_insert_empty_provable_count_tree( + type_path, + key_info, + storage_flags.as_ref(), + &mut batch_operations, + &platform_version.drive, + )?; } else if document_type.documents_countable() { - // Use a CountTree so total document count is available in O(1) - let path_items: Vec> = type_path.iter().map(|s| s.to_vec()).collect(); - batch_operations.push(LowLevelDriveOperation::for_known_path_key_empty_count_tree( - path_items, - vec![0], + // CountTree gives O(1) total document count. + self.batch_insert_empty_count_tree( + type_path, + key_info, storage_flags.as_ref(), - )); + &mut batch_operations, + &platform_version.drive, + )?; } else { - let key_info = Key(vec![0]); self.batch_insert_empty_tree( type_path, key_info, diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs index 4f9a57b06c1..3a91cf5c7c7 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs @@ -348,27 +348,25 @@ impl Drive { type_key.as_bytes(), ]; - // primary key tree + // primary key tree — pick the tree variant that matches the + // document type's countable flags. Pre-v12 contracts always + // have both flags false, so they fall through to NormalTree. if document_type.range_countable() { - // Use a ProvableCountTree for range countable support (implies countable) - let path_items: Vec> = 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().map(|flags| flags.as_ref()), - ), - ); + self.batch_insert_empty_provable_count_tree( + type_path, + KeyRef(&[0]), + storage_flags.as_ref().map(|flags| flags.as_ref()), + &mut batch_operations, + drive_version, + )?; } else if document_type.documents_countable() { - // Use a CountTree so total document count is available in O(1) - let path_items: Vec> = 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().map(|flags| flags.as_ref()), - ), - ); + self.batch_insert_empty_count_tree( + type_path, + KeyRef(&[0]), + storage_flags.as_ref().map(|flags| flags.as_ref()), + &mut batch_operations, + drive_version, + )?; } else { self.batch_insert_empty_tree( type_path, diff --git a/packages/rs-drive/src/fees/op.rs b/packages/rs-drive/src/fees/op.rs index 70dc2ea8200..ebcaf8844c9 100644 --- a/packages/rs-drive/src/fees/op.rs +++ b/packages/rs-drive/src/fees/op.rs @@ -492,6 +492,38 @@ impl LowLevelDriveOperation { LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree) } + /// Sets `GroveOperation` for inserting an empty count tree at the given (estimated) path and key + pub fn for_estimated_path_key_empty_count_tree( + path: KeyInfoPath, + key: KeyInfo, + storage_flags: Option<&StorageFlags>, + ) -> Self { + let tree = match storage_flags { + Some(storage_flags) => { + Element::empty_count_tree_with_flags(storage_flags.to_some_element_flags()) + } + None => Element::empty_count_tree(), + }; + + LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree) + } + + /// Sets `GroveOperation` for inserting an empty provable count tree at the given (estimated) path and key + pub fn for_estimated_path_key_empty_provable_count_tree( + path: KeyInfoPath, + key: KeyInfo, + storage_flags: Option<&StorageFlags>, + ) -> Self { + let tree = match storage_flags { + Some(storage_flags) => { + Element::empty_provable_count_tree_with_flags(storage_flags.to_some_element_flags()) + } + None => Element::empty_provable_count_tree(), + }; + + LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree) + } + /// Sets `GroveOperation` for inserting an element at the given path and key pub fn insert_for_known_path_key_element( path: Vec>, diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_count_tree/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_count_tree/mod.rs new file mode 100644 index 00000000000..26cd5959f62 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_count_tree/mod.rs @@ -0,0 +1,55 @@ +mod v0; + +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::storage_flags::StorageFlags; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::version::drive_versions::DriveVersion; + +impl Drive { + /// Pushes an "insert empty count tree" operation to `drive_operations`. + /// + /// A count tree maintains an O(1) total document count for the items it + /// contains. Used for primary key trees of document types that opted in + /// to `documentsCountable`. + /// + /// # Parameters + /// * `path`: The path to insert an empty count tree. + /// * `key_info`: The key information of the tree key. + /// * `storage_flags`: Storage options for the operation. + /// * `drive_operations`: The vector containing low-level drive operations. + /// * `drive_version`: The drive version to select the correct function version to run. + pub fn batch_insert_empty_count_tree<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + match drive_version + .grove_methods + .batch + .batch_insert_empty_count_tree + { + 0 => self.batch_insert_empty_count_tree_v0( + path, + key_info, + storage_flags, + drive_operations, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "batch_insert_empty_count_tree".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_count_tree/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_count_tree/v0/mod.rs new file mode 100644 index 00000000000..b4620889abe --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_count_tree/v0/mod.rs @@ -0,0 +1,108 @@ +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::object_size_info::DriveKeyInfo::{Key, KeyRef, KeySize}; +use crate::util::storage_flags::StorageFlags; +use grovedb::batch::KeyInfoPath; + +impl Drive { + /// Pushes an "insert empty count tree" operation to `drive_operations`. + pub(super) fn batch_insert_empty_count_tree_v0<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + match key_info { + KeyRef(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push(LowLevelDriveOperation::for_known_path_key_empty_count_tree( + path_items, + key.to_vec(), + storage_flags, + )); + Ok(()) + } + KeySize(key) => { + drive_operations.push( + LowLevelDriveOperation::for_estimated_path_key_empty_count_tree( + KeyInfoPath::from_known_path(path), + key, + storage_flags, + ), + ); + Ok(()) + } + Key(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push(LowLevelDriveOperation::for_known_path_key_empty_count_tree( + path_items, + key, + storage_flags, + )); + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::util::object_size_info::DriveKeyInfo; + use crate::util::test_helpers::setup::setup_drive; + use grovedb::batch::key_info::KeyInfo; + + #[test] + fn test_batch_insert_empty_count_tree_key_ref() { + let drive = setup_drive(None); + let mut ops = vec![]; + let path: &[&[u8]] = &[b"root"]; + drive + .batch_insert_empty_count_tree_v0( + path.iter().copied(), + DriveKeyInfo::KeyRef(b"child"), + None, + &mut ops, + ) + .expect("expected operation to succeed"); + assert_eq!(ops.len(), 1); + } + + #[test] + fn test_batch_insert_empty_count_tree_key_size() { + let drive = setup_drive(None); + let mut ops = vec![]; + let path: &[&[u8]] = &[b"root"]; + drive + .batch_insert_empty_count_tree_v0( + path.iter().copied(), + DriveKeyInfo::KeySize(KeyInfo::KnownKey(b"child".to_vec())), + None, + &mut ops, + ) + .expect("expected operation to succeed"); + assert_eq!(ops.len(), 1); + } + + #[test] + fn test_batch_insert_empty_count_tree_key() { + let drive = setup_drive(None); + let mut ops = vec![]; + let path: &[&[u8]] = &[b"root"]; + drive + .batch_insert_empty_count_tree_v0( + path.iter().copied(), + DriveKeyInfo::Key(b"child".to_vec()), + None, + &mut ops, + ) + .expect("expected operation to succeed"); + assert_eq!(ops.len(), 1); + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_tree/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_tree/mod.rs new file mode 100644 index 00000000000..23a8d0a2f42 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_tree/mod.rs @@ -0,0 +1,56 @@ +mod v0; + +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::storage_flags::StorageFlags; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::version::drive_versions::DriveVersion; + +impl Drive { + /// Pushes an "insert empty provable count tree" operation to `drive_operations`. + /// + /// A provable count tree maintains a verifiable per-subtree count and + /// supports range-countable queries. Implies `documentsCountable`. Used + /// for primary key trees of document types that opted in to + /// `rangeCountable`. + /// + /// # Parameters + /// * `path`: The path to insert an empty provable count tree. + /// * `key_info`: The key information of the tree key. + /// * `storage_flags`: Storage options for the operation. + /// * `drive_operations`: The vector containing low-level drive operations. + /// * `drive_version`: The drive version to select the correct function version to run. + pub fn batch_insert_empty_provable_count_tree<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + match drive_version + .grove_methods + .batch + .batch_insert_empty_provable_count_tree + { + 0 => self.batch_insert_empty_provable_count_tree_v0( + path, + key_info, + storage_flags, + drive_operations, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "batch_insert_empty_provable_count_tree".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_tree/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_tree/v0/mod.rs new file mode 100644 index 00000000000..a5eac303dd1 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_empty_provable_count_tree/v0/mod.rs @@ -0,0 +1,112 @@ +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::object_size_info::DriveKeyInfo; +use crate::util::object_size_info::DriveKeyInfo::{Key, KeyRef, KeySize}; +use crate::util::storage_flags::StorageFlags; +use grovedb::batch::KeyInfoPath; + +impl Drive { + /// Pushes an "insert empty provable count tree" operation to `drive_operations`. + pub(super) fn batch_insert_empty_provable_count_tree_v0<'a, 'c, P>( + &'a self, + path: P, + key_info: DriveKeyInfo<'c>, + storage_flags: Option<&StorageFlags>, + drive_operations: &mut Vec, + ) -> Result<(), Error> + where + P: IntoIterator, +

::IntoIter: ExactSizeIterator + DoubleEndedIterator + Clone, + { + match key_info { + KeyRef(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::for_known_path_key_empty_provable_count_tree( + path_items, + key.to_vec(), + storage_flags, + ), + ); + Ok(()) + } + KeySize(key) => { + drive_operations.push( + LowLevelDriveOperation::for_estimated_path_key_empty_provable_count_tree( + KeyInfoPath::from_known_path(path), + key, + storage_flags, + ), + ); + Ok(()) + } + Key(key) => { + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::for_known_path_key_empty_provable_count_tree( + path_items, + key, + storage_flags, + ), + ); + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::util::object_size_info::DriveKeyInfo; + use crate::util::test_helpers::setup::setup_drive; + use grovedb::batch::key_info::KeyInfo; + + #[test] + fn test_batch_insert_empty_provable_count_tree_key_ref() { + let drive = setup_drive(None); + let mut ops = vec![]; + let path: &[&[u8]] = &[b"root"]; + drive + .batch_insert_empty_provable_count_tree_v0( + path.iter().copied(), + DriveKeyInfo::KeyRef(b"child"), + None, + &mut ops, + ) + .expect("expected operation to succeed"); + assert_eq!(ops.len(), 1); + } + + #[test] + fn test_batch_insert_empty_provable_count_tree_key_size() { + let drive = setup_drive(None); + let mut ops = vec![]; + let path: &[&[u8]] = &[b"root"]; + drive + .batch_insert_empty_provable_count_tree_v0( + path.iter().copied(), + DriveKeyInfo::KeySize(KeyInfo::KnownKey(b"child".to_vec())), + None, + &mut ops, + ) + .expect("expected operation to succeed"); + assert_eq!(ops.len(), 1); + } + + #[test] + fn test_batch_insert_empty_provable_count_tree_key() { + let drive = setup_drive(None); + let mut ops = vec![]; + let path: &[&[u8]] = &[b"root"]; + drive + .batch_insert_empty_provable_count_tree_v0( + path.iter().copied(), + DriveKeyInfo::Key(b"child".to_vec()), + None, + &mut ops, + ) + .expect("expected operation to succeed"); + assert_eq!(ops.len(), 1); + } +} diff --git a/packages/rs-drive/src/util/grove_operations/mod.rs b/packages/rs-drive/src/util/grove_operations/mod.rs index 2a945ef4831..a6322ece6fa 100644 --- a/packages/rs-drive/src/util/grove_operations/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/mod.rs @@ -72,6 +72,12 @@ pub mod batch_insert_empty_tree; /// Batch insert operation into empty sum tree pub mod batch_insert_empty_sum_tree; +/// Batch insert operation into empty count tree (O(1) total count) +pub mod batch_insert_empty_count_tree; + +/// Batch insert operation into empty provable count tree (range-countable) +pub mod batch_insert_empty_provable_count_tree; + /// Batch insert operation into empty tree, but only if it doesn't already exist pub mod batch_insert_empty_tree_if_not_exists; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/mod.rs index 15937ae4bf3..012abdc5407 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/mod.rs @@ -60,6 +60,8 @@ pub struct DriveGroveBatchMethodVersions { pub batch_delete_up_tree_while_empty: FeatureVersion, pub batch_refresh_reference: FeatureVersion, pub batch_insert_empty_sum_tree: FeatureVersion, + pub batch_insert_empty_count_tree: FeatureVersion, + pub batch_insert_empty_provable_count_tree: FeatureVersion, pub batch_move: FeatureVersion, pub batch_insert_item_with_sum_item_if_not_exists: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/v1.rs index b0fd338ce2b..c6ecd135798 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_grove_method_versions/v1.rs @@ -51,6 +51,8 @@ pub const DRIVE_GROVE_METHOD_VERSIONS_V1: DriveGroveMethodVersions = DriveGroveM batch_delete_up_tree_while_empty: 0, batch_refresh_reference: 0, batch_insert_empty_sum_tree: 0, + batch_insert_empty_count_tree: 0, + batch_insert_empty_provable_count_tree: 0, batch_move: 0, batch_insert_item_with_sum_item_if_not_exists: 0, }, From e1ea11c2ff3de2595515d43a9865a373c004028e Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 19:01:41 +0700 Subject: [PATCH 10/14] test(drive): end-to-end coverage for documentsCountable / rangeCountable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `countable_e2e_tests` module in insert_contract/v0 that builds real v12 contracts via DataContractFactory, applies them to a Drive (setup_drive_with_initial_state_structure), reads the primary-key tree element back from grovedb, and asserts the concrete tree variant + count match what the schema requested. This closes the "feature actually works end-to-end" gap that was left after the unit-level dispatch tests. Six new tests: default_contract_creates_normal_tree_for_primary_key Sanity baseline: no flags → Element::Tree (NormalTree). documents_countable_contract_creates_count_tree_for_primary_key documentsCountable: true → Element::CountTree(_, 0, _) at insertion; DocumentTypeV2 reports documents_countable=true, range_countable=false. range_countable_contract_creates_provable_count_tree_for_primary_key rangeCountable: true → Element::ProvableCountTree(..); DocumentTypeV2 reports both flags true (range implies countable). count_tree_count_grows_and_shrinks_with_document_inserts_and_deletes Insert 3 documents → CountTree count == 3. Delete 1 → count == 2. provable_count_tree_count_grows_with_document_inserts Insert 5 documents into a rangeCountable contract → ProvableCountTree count == 5. count_tree_contract_supports_document_fetch Sanity: switching the primary-key tree variant doesn't break the existing all-items document query path. These exercise the previously-unverified positive feature path — schema → CountTree creation → count maintenance through inserts/deletes — and confirm DocumentTypeV2 invariants are observed end-to-end on a real grovedb instance. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../contract/insert/insert_contract/v0/mod.rs | 402 ++++++++++++++++++ 1 file changed, 402 insertions(+) diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs index 1afe813f21d..26023a6b965 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs @@ -345,3 +345,405 @@ impl Drive { Ok(batch_operations) } } + +#[cfg(test)] +mod countable_e2e_tests { + //! End-to-end coverage for `documentsCountable` / `rangeCountable`. + //! + //! These tests exercise the full feature path: + //! - Build a v12 contract with the flag set in the schema. + //! - Apply it to a real Drive (grovedb). + //! - Read the primary-key tree element back from grove and assert the + //! concrete tree variant (NormalTree / CountTree / ProvableCountTree) + //! matches what the schema requested. + //! - For the count variants, insert and delete documents and assert the + //! tree's internal count moves accordingly. + + use crate::drive::Drive; + use crate::util::grove_operations::DirectQueryType; + use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; + use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; + use crate::util::storage_flags::StorageFlags; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; + use dpp::data_contract::document_type::random_document::CreateRandomDocument; + use dpp::data_contract::DataContractFactory; + use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; + use dpp::document::DocumentV0Getters; + use dpp::platform_value::{platform_value, Value}; + use dpp::tests::utils::generate_random_identifier_struct; + use dpp::version::PlatformVersion; + use grovedb::Element; + + const PROTOCOL_VERSION_V12: u32 = 12; + + /// Builds a v12 `DataContract` whose single `widget` document type has + /// `documentsCountable` / `rangeCountable` set to the requested values. + fn build_widget_contract( + documents_countable: bool, + range_countable: bool, + ) -> dpp::prelude::DataContract { + let factory = + DataContractFactory::new(PROTOCOL_VERSION_V12).expect("expected to create factory"); + + let mut document_schema = platform_value!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "position": 0, + "maxLength": 64, + } + }, + "additionalProperties": false, + }); + if documents_countable { + document_schema.as_map_mut().unwrap().push(( + Value::Text("documentsCountable".to_string()), + Value::Bool(true), + )); + } + if range_countable { + document_schema + .as_map_mut() + .unwrap() + .push((Value::Text("rangeCountable".to_string()), Value::Bool(true))); + } + + let schemas = platform_value!({ "widget": document_schema }); + let owner_id = generate_random_identifier_struct(); + + factory + .create_with_value_config(owner_id, 0, schemas, None, None) + .expect("expected to create data contract") + .data_contract_owned() + } + + /// Reads the primary-key tree element directly from grove and returns it. + fn read_primary_key_tree( + drive: &Drive, + contract: &dpp::prelude::DataContract, + document_type_name: &str, + ) -> Element { + let pv = PlatformVersion::latest(); + let contract_id = contract.id(); + let path: [&[u8]; 4] = [ + &[crate::drive::RootTree::DataContractDocuments as u8], + contract_id.as_bytes(), + &[1], + document_type_name.as_bytes(), + ]; + drive + .grove_get_raw( + (&path).into(), + &[0], + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &pv.drive, + ) + .expect("expected grove_get_raw to succeed") + .expect("primary key tree element should exist") + } + + #[test] + fn default_contract_creates_normal_tree_for_primary_key() { + let drive = setup_drive_with_initial_state_structure(None); + let pv = PlatformVersion::latest(); + let contract = build_widget_contract(false, false); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply contract"); + + let elem = read_primary_key_tree(&drive, &contract, "widget"); + assert!( + matches!(elem, Element::Tree(..)), + "default (non-countable) contract should use a NormalTree primary key tree, got {:?}", + elem + ); + } + + #[test] + fn documents_countable_contract_creates_count_tree_for_primary_key() { + let drive = setup_drive_with_initial_state_structure(None); + let pv = PlatformVersion::latest(); + let contract = build_widget_contract(true, false); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply contract"); + + let elem = read_primary_key_tree(&drive, &contract, "widget"); + match &elem { + Element::CountTree(_, count, _) => { + assert_eq!(*count, 0, "freshly inserted CountTree should have count 0"); + } + other => panic!( + "documentsCountable contract should use a CountTree primary key tree, got {:?}", + other + ), + } + + // Sanity: the parsed DocumentTypeV2 also reports the flag. + let dt = contract + .document_type_for_name("widget") + .expect("widget exists"); + let dt_owned = dt.to_owned_document_type(); + match dt_owned { + dpp::data_contract::document_type::DocumentType::V2(v2) => { + assert!(v2.documents_countable()); + assert!(!v2.range_countable()); + } + other => panic!("expected DocumentType::V2 on protocol v12, got {:?}", other), + } + } + + #[test] + fn range_countable_contract_creates_provable_count_tree_for_primary_key() { + let drive = setup_drive_with_initial_state_structure(None); + let pv = PlatformVersion::latest(); + let contract = build_widget_contract(false, true); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply contract"); + + let elem = read_primary_key_tree(&drive, &contract, "widget"); + assert!( + matches!(elem, Element::ProvableCountTree(..)), + "rangeCountable contract should use a ProvableCountTree primary key tree, got {:?}", + elem + ); + + // rangeCountable implies documents_countable in the parser. + let dt = contract + .document_type_for_name("widget") + .expect("widget exists"); + let dt_owned = dt.to_owned_document_type(); + match dt_owned { + dpp::data_contract::document_type::DocumentType::V2(v2) => { + assert!(v2.range_countable()); + assert!(v2.documents_countable()); + } + other => panic!("expected DocumentType::V2 on protocol v12, got {:?}", other), + } + } + + #[test] + fn count_tree_count_grows_and_shrinks_with_document_inserts_and_deletes() { + let drive = setup_drive_with_initial_state_structure(None); + let pv = PlatformVersion::latest(); + let contract = build_widget_contract(true, false); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply contract"); + + let document_type = contract + .document_type_for_name("widget") + .expect("widget exists"); + + // Insert 3 documents. + let mut doc_ids = vec![]; + for seed in 1u64..=3 { + let document = document_type + .random_document(Some(seed), pv) + .expect("random document"); + doc_ids.push(document.id()); + + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + pv, + None, + ) + .expect("expected to insert document"); + } + + let elem_after_inserts = read_primary_key_tree(&drive, &contract, "widget"); + match elem_after_inserts { + Element::CountTree(_, count, _) => { + assert_eq!(count, 3, "count tree should track 3 inserted documents"); + } + other => panic!("expected CountTree, got {:?}", other), + } + + // Delete one. + drive + .delete_document_for_contract( + doc_ids[0], + &contract, + "widget", + BlockInfo::default(), + true, + None, + pv, + None, + ) + .expect("expected to delete document"); + + let elem_after_delete = read_primary_key_tree(&drive, &contract, "widget"); + match elem_after_delete { + Element::CountTree(_, count, _) => { + assert_eq!(count, 2, "count tree should drop to 2 after one delete"); + } + other => panic!("expected CountTree, got {:?}", other), + } + } + + #[test] + fn provable_count_tree_count_grows_with_document_inserts() { + let drive = setup_drive_with_initial_state_structure(None); + let pv = PlatformVersion::latest(); + let contract = build_widget_contract(false, true); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply contract"); + + let document_type = contract + .document_type_for_name("widget") + .expect("widget exists"); + + for seed in 1u64..=5 { + let document = document_type + .random_document(Some(seed), pv) + .expect("random document"); + + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + pv, + None, + ) + .expect("expected to insert document"); + } + + let elem = read_primary_key_tree(&drive, &contract, "widget"); + match elem { + Element::ProvableCountTree(_, count, _) => { + assert_eq!(count, 5, "provable count tree should track 5 documents"); + } + other => panic!("expected ProvableCountTree, got {:?}", other), + } + } + + /// Sanity: existing document fetch + count APIs still work for a CountTree + /// contract — i.e. switching the underlying primary-key tree variant + /// does not break document iteration. + #[test] + fn count_tree_contract_supports_document_fetch() { + let drive = setup_drive_with_initial_state_structure(None); + let pv = PlatformVersion::latest(); + let contract = build_widget_contract(true, false); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply contract"); + + let document_type = contract + .document_type_for_name("widget") + .expect("widget exists"); + + let document = document_type + .random_document(Some(42), pv) + .expect("random document"); + let inserted_id = document.id(); + + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + pv, + None, + ) + .expect("expected to insert document"); + + let query = + crate::query::DriveDocumentQuery::all_items_query(&contract, document_type, None); + let (docs, _, _) = query + .execute_raw_results_no_proof(&drive, None, None, pv) + .expect("expected query to succeed"); + assert_eq!(docs.len(), 1, "should fetch exactly the inserted document"); + let decoded = dpp::document::Document::from_bytes(&docs[0], document_type, pv) + .expect("expected to decode document"); + assert_eq!(decoded.id(), inserted_id); + } +} From fe6415a58e5f1103bf6697fe83512e0be231726a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 19:06:01 +0700 Subject: [PATCH 11/14] refactor(drive): route primary-key tree selection through primary_key_tree_type() The contract insert and update paths still inlined their own range_countable / documents_countable selection. Document insert, delete, and estimation paths route through the centralized DocumentTypeRef::primary_key_tree_type() helper, which participates in drive method version dispatch (platform_version.drive.methods.document.primary_key_tree_type). If that helper ever gets a v1 with different selection logic, contract creation and document operations would silently diverge. Match on the helper's TreeType output instead so all five paths (contract insert, contract update, document insert, document delete, estimation) agree on a single source of truth. Variants other than NormalTree / CountTree / ProvableCountTree return a CorruptedCodeExecution error because they're not valid for primary-key trees. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../contract/insert/insert_contract/v0/mod.rs | 39 +++++++++++-------- .../contract/update/update_contract/v0/mod.rs | 34 +++++++++------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs index 26023a6b965..70496324272 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs @@ -1,5 +1,6 @@ use crate::drive::contract::paths; +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; use crate::drive::{contract_documents_path, votes, Drive, RootTree}; use crate::util::object_size_info::DriveKeyInfo::{Key, KeyRef}; use crate::util::storage_flags::StorageFlags; @@ -12,7 +13,6 @@ use dpp::data_contract::config::v0::DataContractConfigGettersV0; use dpp::data_contract::DataContract; use dpp::fee::fee_result::FeeResult; -use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; use dpp::data_contract::document_type::methods::DocumentTypeBasicMethods; use dpp::serialization::PlatformSerializableWithPlatformVersion; @@ -22,7 +22,7 @@ use crate::drive::votes::paths::{ use crate::error::contract::DataContractError; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; -use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; use std::collections::{HashMap, HashSet}; impl Drive { @@ -284,36 +284,43 @@ impl Drive { type_key.as_bytes(), ]; - // primary key tree — pick the tree variant that matches the document - // type's countable flags. Pre-v12 contracts always have both flags - // false, so they fall through to the plain NormalTree path. + // primary key tree — route through the centralized + // primary_key_tree_type() so contract creation, document inserts, + // deletes, and estimation paths all see the same tree-variant + // selection (under whichever drive method version is active). let key_info = Key(vec![0]); - if document_type.range_countable() { - // ProvableCountTree supports range-countable queries (implies countable). - self.batch_insert_empty_provable_count_tree( + match document_type + .as_ref() + .primary_key_tree_type(platform_version)? + { + TreeType::ProvableCountTree => self.batch_insert_empty_provable_count_tree( type_path, key_info, storage_flags.as_ref(), &mut batch_operations, &platform_version.drive, - )?; - } else if document_type.documents_countable() { - // CountTree gives O(1) total document count. - self.batch_insert_empty_count_tree( + )?, + TreeType::CountTree => self.batch_insert_empty_count_tree( type_path, key_info, storage_flags.as_ref(), &mut batch_operations, &platform_version.drive, - )?; - } else { - self.batch_insert_empty_tree( + )?, + TreeType::NormalTree => self.batch_insert_empty_tree( type_path, key_info, storage_flags.as_ref(), &mut batch_operations, &platform_version.drive, - )?; + )?, + _ => { + return Err(Error::Drive( + crate::error::drive::DriveError::CorruptedCodeExecution( + "primary_key_tree_type returned a tree variant unsupported by contract insert (only NormalTree / CountTree / ProvableCountTree are valid for primary-key trees)", + ), + )); + } } let mut index_cache: HashSet<&[u8]> = HashSet::new(); diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs index 3a91cf5c7c7..a0c75b73c75 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs @@ -1,3 +1,4 @@ +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; use crate::drive::{contract_documents_path, Drive}; use crate::error::drive::DriveError; use crate::error::Error; @@ -9,7 +10,7 @@ use crate::util::storage_flags::StorageFlags; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::config::v0::DataContractConfigGettersV0; -use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::DataContract; use dpp::fee::fee_result::FeeResult; @@ -348,33 +349,40 @@ impl Drive { type_key.as_bytes(), ]; - // primary key tree — pick the tree variant that matches the - // document type's countable flags. Pre-v12 contracts always - // have both flags false, so they fall through to NormalTree. - if document_type.range_countable() { - self.batch_insert_empty_provable_count_tree( + // primary key tree — route through the centralized + // primary_key_tree_type() so contract update, document inserts, + // deletes, and estimation paths all see the same tree-variant + // selection (under whichever drive method version is active). + match document_type + .as_ref() + .primary_key_tree_type(platform_version)? + { + TreeType::ProvableCountTree => self.batch_insert_empty_provable_count_tree( type_path, KeyRef(&[0]), storage_flags.as_ref().map(|flags| flags.as_ref()), &mut batch_operations, drive_version, - )?; - } else if document_type.documents_countable() { - self.batch_insert_empty_count_tree( + )?, + TreeType::CountTree => self.batch_insert_empty_count_tree( type_path, KeyRef(&[0]), storage_flags.as_ref().map(|flags| flags.as_ref()), &mut batch_operations, drive_version, - )?; - } else { - self.batch_insert_empty_tree( + )?, + TreeType::NormalTree => self.batch_insert_empty_tree( type_path, KeyRef(&[0]), storage_flags.as_ref().map(|flags| flags.as_ref()), &mut batch_operations, drive_version, - )?; + )?, + _ => { + return Err(Error::Drive(DriveError::CorruptedCodeExecution( + "primary_key_tree_type returned a tree variant unsupported by contract update (only NormalTree / CountTree / ProvableCountTree are valid for primary-key trees)", + ))); + } } let mut index_cache: HashSet<&[u8]> = HashSet::new(); From 03e23a12b6744a1b2b6008541f156ceed1879a6d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 19:29:37 +0700 Subject: [PATCH 12/14] refactor(drive): default primary-key tree to NormalTree on unknown TreeType Collapse the explicit `TreeType::NormalTree` arm and the `CorruptedCodeExecution` fallback into a single `_ =>` catch-all that inserts a NormalTree. NormalTree is the safe default for any current or future TreeType variant the dispatcher might return that isn't specifically count/provable-count, and avoids a runtime error path for something that's effectively a code-evolution surface. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/drive/contract/insert/insert_contract/v0/mod.rs | 9 +-------- .../src/drive/contract/update/update_contract/v0/mod.rs | 7 +------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs index 70496324272..95cca5fc895 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs @@ -307,20 +307,13 @@ impl Drive { &mut batch_operations, &platform_version.drive, )?, - TreeType::NormalTree => self.batch_insert_empty_tree( + _ => self.batch_insert_empty_tree( type_path, key_info, storage_flags.as_ref(), &mut batch_operations, &platform_version.drive, )?, - _ => { - return Err(Error::Drive( - crate::error::drive::DriveError::CorruptedCodeExecution( - "primary_key_tree_type returned a tree variant unsupported by contract insert (only NormalTree / CountTree / ProvableCountTree are valid for primary-key trees)", - ), - )); - } } let mut index_cache: HashSet<&[u8]> = HashSet::new(); diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs index a0c75b73c75..61021bc9dde 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs @@ -371,18 +371,13 @@ impl Drive { &mut batch_operations, drive_version, )?, - TreeType::NormalTree => self.batch_insert_empty_tree( + _ => self.batch_insert_empty_tree( type_path, KeyRef(&[0]), storage_flags.as_ref().map(|flags| flags.as_ref()), &mut batch_operations, drive_version, )?, - _ => { - return Err(Error::Drive(DriveError::CorruptedCodeExecution( - "primary_key_tree_type returned a tree variant unsupported by contract update (only NormalTree / CountTree / ProvableCountTree are valid for primary-key trees)", - ))); - } } let mut index_cache: HashSet<&[u8]> = HashSet::new(); From 94e029f1032c07ac957ec5f2365156b141df225a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 19:37:42 +0700 Subject: [PATCH 13/14] test: fee-correctness + JS round-trip coverage for documentsCountable / rangeCountable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the two coverage gaps left after the e2e tests: Rust fee-correctness pinning (packages/rs-drive) Three new tests in countable_e2e_tests assert that switching the primary-key tree variant materially changes the fees Drive reports — if NormalTree / CountTree / ProvableCountTree contracts produced the same FeeResult, the differentiating code path was never exercised. Don't pin exact values (brittle under fee schedule changes); pin the directional invariant. - count_tree_contract_apply_produces_different_fees_than_normal_tree - provable_count_tree_contract_apply_produces_different_fees_than_count_tree - document_insert_into_count_tree_produces_positive_fees JS round-trip coverage (packages/wasm-dpp2) New spec file DataContractCountable.spec.ts asserts that the v12 flags survive the JS DataContract API: - schemas getter exposes documentsCountable / rangeCountable verbatim - toBytes / fromBytes round-trip preserves both flags - toObject / fromObject round-trip preserves documentsCountable - fullValidation accepts both flags together at v12 These pin the wire/serialization contract for SDK consumers — if a future change silently dropped the flags during JS-side serialization, contracts created via the SDK would land on disk with the wrong primary-key tree variant. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../contract/insert/insert_contract/v0/mod.rs | 117 ++++++++++++++++ .../tests/unit/DataContractCountable.spec.ts | 127 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 packages/wasm-dpp2/tests/unit/DataContractCountable.spec.ts diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs index 95cca5fc895..bea17db5214 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs @@ -746,4 +746,121 @@ mod countable_e2e_tests { .expect("expected to decode document"); assert_eq!(decoded.id(), inserted_id); } + + /// Apply a contract with the given countable flags and return the fees + /// reported by `insert_contract`. Used to compare fee profiles across + /// the three primary-key tree variants. + fn fees_for_contract_with( + documents_countable: bool, + range_countable: bool, + ) -> dpp::fee::fee_result::FeeResult { + let drive = setup_drive_with_initial_state_structure(None); + let pv = PlatformVersion::latest(); + let contract = build_widget_contract(documents_countable, range_countable); + drive + .insert_contract(&contract, BlockInfo::default(), true, None, pv) + .expect("expected insert_contract to succeed and return fees") + } + + /// Switching the primary-key tree variant from NormalTree to CountTree + /// changes the underlying grovedb element shape (CountTree carries an + /// extra count value). The reported fees must therefore differ — if they + /// don't, the contract insert path silently degraded back to the + /// NormalTree branch and the documentsCountable feature is dead. + #[test] + fn count_tree_contract_apply_produces_different_fees_than_normal_tree() { + let normal_fees = fees_for_contract_with(false, false); + let count_fees = fees_for_contract_with(true, false); + + assert!(normal_fees.storage_fee > 0, "normal tree storage fee"); + assert!(normal_fees.processing_fee > 0, "normal tree processing fee"); + assert!(count_fees.storage_fee > 0, "count tree storage fee"); + assert!(count_fees.processing_fee > 0, "count tree processing fee"); + + assert_ne!( + (normal_fees.storage_fee, normal_fees.processing_fee), + (count_fees.storage_fee, count_fees.processing_fee), + "documentsCountable: true must produce a different fee profile than the default \ + NormalTree contract — equal fees mean the count-tree branch was never exercised" + ); + } + + /// Same invariant for the rangeCountable / ProvableCountTree branch: + /// switching from CountTree to ProvableCountTree changes both the grove + /// element type and the proof shape, so fees must differ. + #[test] + fn provable_count_tree_contract_apply_produces_different_fees_than_count_tree() { + let count_fees = fees_for_contract_with(true, false); + let provable_fees = fees_for_contract_with(false, true); + + assert!(provable_fees.storage_fee > 0, "provable count storage fee"); + assert!( + provable_fees.processing_fee > 0, + "provable count processing fee" + ); + + assert_ne!( + (count_fees.storage_fee, count_fees.processing_fee), + (provable_fees.storage_fee, provable_fees.processing_fee,), + "rangeCountable: true must produce a different fee profile than documentsCountable: \ + true alone — equal fees mean the provable-count-tree branch was never exercised" + ); + } + + /// Document insert into a CountTree contract should produce positive fees + /// without error. This exercises the document-insert code paths + /// (add_document_for_contract_operations, primary-key-tree dispatch in + /// add_document_to_primary_storage) under the count-tree branch. + #[test] + fn document_insert_into_count_tree_produces_positive_fees() { + let drive = setup_drive_with_initial_state_structure(None); + let pv = PlatformVersion::latest(); + let contract = build_widget_contract(true, false); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply contract"); + + let document_type = contract + .document_type_for_name("widget") + .expect("widget exists"); + let document = document_type + .random_document(Some(7), pv) + .expect("random document"); + + let fee = drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + pv, + None, + ) + .expect("expected to insert document into count tree"); + + assert!( + fee.storage_fee > 0, + "document insert into a CountTree contract must produce a positive storage fee" + ); + assert!( + fee.processing_fee > 0, + "document insert into a CountTree contract must produce a positive processing fee" + ); + } } diff --git a/packages/wasm-dpp2/tests/unit/DataContractCountable.spec.ts b/packages/wasm-dpp2/tests/unit/DataContractCountable.spec.ts new file mode 100644 index 00000000000..9ab41c861cf --- /dev/null +++ b/packages/wasm-dpp2/tests/unit/DataContractCountable.spec.ts @@ -0,0 +1,127 @@ +/** + * Verifies that the v12-introduced `documentsCountable` / `rangeCountable` + * top-level document-schema flags survive round-tripping through the JS + * DataContract API. These flags select the primary-key tree variant in + * Drive (NormalTree / CountTree / ProvableCountTree); if the JS layer + * silently dropped them on serialize/deserialize, contracts created via + * the SDK would store with the wrong tree shape. + */ +import { expect } from './helpers/chai.ts'; +import { initWasm, wasm } from '../../dist/dpp.compressed.js'; + +let PlatformVersion: typeof wasm.PlatformVersion; + +before(async () => { + await initWasm(); + ({ PlatformVersion } = wasm); +}); + +const ownerId = '11111111111111111111111111111111'; + +function widgetSchemas(extras: Record): Record { + return { + widget: { + type: 'object', + properties: { + name: { type: 'string', position: 0, maxLength: 64 }, + }, + additionalProperties: false, + ...extras, + }, + }; +} + +describe('DataContract — countable flags (v12)', () => { + it('preserves documentsCountable on the schemas getter', () => { + const dataContract = new wasm.DataContract({ + ownerId, + identityNonce: BigInt(2), + schemas: widgetSchemas({ documentsCountable: true }), + definitions: null, + fullValidation: true, + platformVersion: new PlatformVersion(12), + }); + + const schemas = dataContract.schemas as Record; + expect(schemas.widget.documentsCountable).to.equal(true); + }); + + it('preserves rangeCountable on the schemas getter', () => { + const dataContract = new wasm.DataContract({ + ownerId, + identityNonce: BigInt(2), + schemas: widgetSchemas({ rangeCountable: true }), + definitions: null, + fullValidation: true, + platformVersion: new PlatformVersion(12), + }); + + const schemas = dataContract.schemas as Record; + expect(schemas.widget.rangeCountable).to.equal(true); + }); + + it('round-trips documentsCountable through toBytes / fromBytes', () => { + const original = new wasm.DataContract({ + ownerId, + identityNonce: BigInt(2), + schemas: widgetSchemas({ documentsCountable: true }), + definitions: null, + fullValidation: true, + platformVersion: new PlatformVersion(12), + }); + + const bytes = original.toBytes(new PlatformVersion(12)); + const restored = wasm.DataContract.fromBytes(bytes, true, new PlatformVersion(12)); + + const schemas = restored.schemas as Record; + expect(schemas.widget.documentsCountable).to.equal(true); + }); + + it('round-trips rangeCountable through toBytes / fromBytes', () => { + const original = new wasm.DataContract({ + ownerId, + identityNonce: BigInt(2), + schemas: widgetSchemas({ rangeCountable: true }), + definitions: null, + fullValidation: true, + platformVersion: new PlatformVersion(12), + }); + + const bytes = original.toBytes(new PlatformVersion(12)); + const restored = wasm.DataContract.fromBytes(bytes, true, new PlatformVersion(12)); + + const schemas = restored.schemas as Record; + expect(schemas.widget.rangeCountable).to.equal(true); + }); + + it('round-trips documentsCountable through toObject / fromObject', () => { + const original = new wasm.DataContract({ + ownerId, + identityNonce: BigInt(2), + schemas: widgetSchemas({ documentsCountable: true }), + definitions: null, + fullValidation: true, + platformVersion: new PlatformVersion(12), + }); + + const obj = original.toObject(new PlatformVersion(12)); + const restored = wasm.DataContract.fromObject(obj, true, new PlatformVersion(12)); + + const schemas = restored.schemas as Record; + expect(schemas.widget.documentsCountable).to.equal(true); + }); + + it('full validation accepts documentsCountable + rangeCountable together at v12', () => { + expect(() => { + // eslint-disable-next-line no-new + new wasm.DataContract({ + ownerId, + identityNonce: BigInt(2), + schemas: widgetSchemas({ documentsCountable: true, rangeCountable: true }), + definitions: null, + fullValidation: true, + platformVersion: new PlatformVersion(12), + }); + }).to.not.throw(); + }); +}); From a43d434b056ed64094381d8423253ccc13f0a176 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 8 May 2026 20:24:39 +0700 Subject: [PATCH 14/14] test(drive): cover countable contract update paths --- .../contract/insert/insert_contract/v0/mod.rs | 93 +++++++- .../contract/update/update_contract/v0/mod.rs | 212 +++++++++++++----- 2 files changed, 250 insertions(+), 55 deletions(-) diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs index bea17db5214..05c57682037 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs @@ -375,7 +375,7 @@ mod countable_e2e_tests { use dpp::platform_value::{platform_value, Value}; use dpp::tests::utils::generate_random_identifier_struct; use dpp::version::PlatformVersion; - use grovedb::Element; + use grovedb::{Element, GroveDb, PathTrunkChunkQuery}; const PROTOCOL_VERSION_V12: u32 = 12; @@ -448,6 +448,19 @@ mod countable_e2e_tests { .expect("primary key tree element should exist") } + fn primary_key_tree_path( + contract: &dpp::prelude::DataContract, + document_type_name: &str, + ) -> Vec> { + vec![ + vec![crate::drive::RootTree::DataContractDocuments as u8], + contract.id().as_bytes().to_vec(), + vec![1], + document_type_name.as_bytes().to_vec(), + vec![0], + ] + } + #[test] fn default_contract_creates_normal_tree_for_primary_key() { let drive = setup_drive_with_initial_state_structure(None); @@ -688,6 +701,84 @@ mod countable_e2e_tests { } } + #[test] + fn range_countable_primary_key_tree_supports_trunk_proof() { + let drive = setup_drive_with_initial_state_structure(None); + let pv = PlatformVersion::latest(); + let contract = build_widget_contract(false, true); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + pv, + ) + .expect("expected to apply contract"); + + let document_type = contract + .document_type_for_name("widget") + .expect("widget exists"); + + for seed in 1u64..=20 { + let document = document_type + .random_document(Some(seed), pv) + .expect("random document"); + + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + pv, + None, + ) + .expect("expected to insert document"); + } + + let elem = read_primary_key_tree(&drive, &contract, "widget"); + match elem { + Element::ProvableCountTree(_, count, _) => { + assert_eq!(count, 20, "provable count tree should track inserted docs"); + } + other => panic!("expected ProvableCountTree, got {:?}", other), + } + + let query = PathTrunkChunkQuery::new(primary_key_tree_path(&contract, "widget"), 3); + let proof = drive + .grove + .prove_trunk_chunk(&query, &pv.drive.grove_version) + .value + .expect("expected trunk proof call to succeed"); + let (root_hash, result) = + GroveDb::verify_trunk_chunk_proof(&proof, &query, &pv.drive.grove_version) + .expect("expected trunk proof to verify"); + + assert_ne!(root_hash, [0u8; 32], "root hash should not be zero"); + assert!( + !result.elements.is_empty(), + "trunk proof should return primary-key tree elements" + ); + assert!( + result + .leaf_keys + .values() + .any(|leaf_info| leaf_info.count.is_some()), + "rangeCountable trunk proof should expose subtree counts" + ); + } + /// Sanity: existing document fetch + count APIs still work for a CountTree /// contract — i.e. switching the underlying primary-key tree variant /// does not break document iteration. diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs index 61021bc9dde..a453ee86e3f 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v0/mod.rs @@ -404,17 +404,122 @@ impl Drive { #[cfg(test)] mod tests { + use crate::drive::{Drive, RootTree}; use crate::error::drive::DriveError; use crate::error::Error; + use crate::util::grove_operations::DirectQueryType; use crate::util::storage_flags::StorageFlags; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; use dpp::data_contract::config::v0::DataContractConfigSettersV0; use dpp::data_contract::schema::DataContractSchemaMethodsV0; - use dpp::platform_value::platform_value; + use dpp::platform_value::{platform_value, Value}; use dpp::tests::fixtures::get_dashpay_contract_fixture; use dpp::version::PlatformVersion; + use grovedb::Element; + + fn label_document_schema(documents_countable: bool, range_countable: bool) -> Value { + let mut schema = platform_value!({ + "type": "object", + "properties": { + "label": { + "type": "string", + "position": 0, + "maxLength": 50, + } + }, + "additionalProperties": false, + }); + + let schema_map = schema.as_map_mut().expect("schema should be a map"); + if documents_countable { + schema_map.push(( + Value::Text("documentsCountable".to_string()), + Value::Bool(true), + )); + } + if range_countable { + schema_map.push((Value::Text("rangeCountable".to_string()), Value::Bool(true))); + } + + schema + } + + fn update_contract_with_new_document_type( + document_type_name: &str, + new_schema: Value, + ) -> (Drive, dpp::prelude::DataContract, usize) { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let mut contract = get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("initial insert"); + + let original_type_count = contract.document_types().len(); + + contract + .set_document_schema( + document_type_name, + new_schema, + true, + &mut vec![], + platform_version, + ) + .expect("set new schema"); + contract.increment_version(); + + drive + .update_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("update with new doc type should succeed"); + + (drive, contract, original_type_count) + } + + fn read_primary_key_tree( + drive: &Drive, + contract: &dpp::prelude::DataContract, + document_type_name: &str, + ) -> Element { + let platform_version = PlatformVersion::latest(); + let contract_id = contract.id(); + let path: [&[u8]; 4] = [ + &[RootTree::DataContractDocuments as u8], + contract_id.as_bytes(), + &[1], + document_type_name.as_bytes(), + ]; + + drive + .grove_get_raw( + (&path).into(), + &[0], + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected grove_get_raw to succeed") + .expect("primary key tree element should exist") + } /// Exercises the `if original_contract.config().readonly() { ... }` branch /// inside `update_contract_operations_v0`. Note that the earlier readonly @@ -485,60 +590,11 @@ mod tests { /// exercises adding an entirely new document type. #[test] fn test_update_contract_v0_adds_new_document_type_creates_trees() { - let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); - - let mut contract = get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - - drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("initial insert"); - - let original_type_count = contract.document_types().len(); - - // Add a brand-new document type that did NOT exist before. - let new_schema = platform_value!({ - "type": "object", - "properties": { - "label": { - "type": "string", - "position": 0, - "maxLength": 50, - } - }, - "additionalProperties": false, - }); - contract - .set_document_schema( - "brandNewDocType", - new_schema, - true, - &mut vec![], - platform_version, - ) - .expect("set new schema"); - contract.increment_version(); - - // The update path will hit the `else` branch because "brandNewDocType" - // is not in the original's document_types map. - drive - .update_contract( - &contract, - BlockInfo::default(), - true, - None, - platform_version, - None, - ) - .expect("update with new doc type should succeed"); + let (drive, contract, original_type_count) = update_contract_with_new_document_type( + "brandNewDocType", + label_document_schema(false, false), + ); // Verify the new type is now present. let fetched = drive @@ -557,6 +613,54 @@ mod tests { .contains_key("brandNewDocType"), "new document type must be present" ); + + let elem = read_primary_key_tree(&drive, &contract, "brandNewDocType"); + assert!( + matches!(elem, Element::Tree(..)), + "new non-countable document type should use a NormalTree primary key tree, got {:?}", + elem + ); + } + + #[test] + fn test_update_contract_v0_adds_new_documents_countable_type_creates_count_tree() { + let (drive, contract, _) = update_contract_with_new_document_type( + "brandNewCountedDocType", + label_document_schema(true, false), + ); + + let elem = read_primary_key_tree(&drive, &contract, "brandNewCountedDocType"); + match elem { + Element::CountTree(_, count, _) => { + assert_eq!(count, 0, "freshly created CountTree should have count 0"); + } + other => panic!( + "new documentsCountable document type should use a CountTree primary key tree, got {:?}", + other + ), + } + } + + #[test] + fn test_update_contract_v0_adds_new_range_countable_type_creates_provable_count_tree() { + let (drive, contract, _) = update_contract_with_new_document_type( + "brandNewRangeCountedDocType", + label_document_schema(false, true), + ); + + let elem = read_primary_key_tree(&drive, &contract, "brandNewRangeCountedDocType"); + match elem { + Element::ProvableCountTree(_, count, _) => { + assert_eq!( + count, 0, + "freshly created ProvableCountTree should have count 0" + ); + } + other => panic!( + "new rangeCountable document type should use a ProvableCountTree primary key tree, got {:?}", + other + ), + } } /// Exercises the `update_contract_v0/v1` apply=false path where the