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..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": { 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 9dfb6ad7499..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": { @@ -505,6 +506,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 adf85180244..bc3b58832a8 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 @@ -683,6 +683,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/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs index ca52f02ec6a..f771b3d916a 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs @@ -619,6 +619,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V1(v1) => { v1.requires_revision() } + crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { + v2.requires_revision() + } } } @@ -630,6 +633,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V1(v1) => { v1.initial_revision() } + crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { + v2.initial_revision() + } } } @@ -641,6 +647,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V1(v1) => { v1.top_level_indices() } + crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { + v2.top_level_indices() + } } } @@ -652,6 +661,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V1(v1) => { v1.top_level_indices_of_contested_unique_indexes() } + crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { + v2.top_level_indices_of_contested_unique_indexes() + } } } @@ -664,6 +676,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V1(v1) => { v1.index_structure() } + crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { + v2.index_structure() + } } } @@ -679,6 +694,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V1(v1) => { v1.unique_id_for_document_field(index_level, base_event) } + crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { + v2.unique_id_for_document_field(index_level, base_event) + } } } @@ -690,6 +708,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V1(v1) => { v1.sanitize_document_properties(properties) } + crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { + v2.sanitize_document_properties(properties) + } } } } 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..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 @@ -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() } @@ -949,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 { 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/schema/allowed_top_level_properties.rs b/packages/rs-dpp/src/data_contract/document_type/schema/allowed_top_level_properties.rs index 2d3f6ced6df..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 @@ -1,12 +1,22 @@ 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. /// -/// 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. -pub const ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[ +/// This is intentionally a *subset* of v1 meta-schema's `properties`: keys +/// 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", "$defs", @@ -47,7 +57,7 @@ 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_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES.contains(&key_str) }); map.len() != before } @@ -100,40 +110,109 @@ 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_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("v0 document meta-schema JSON must be valid"); + + let v0_properties: std::collections::BTreeSet<&str> = v0_schema + .get("properties") + .and_then(|p| p.as_object()) + .expect("v0 meta-schema must have a 'properties' object") + .keys() + .map(|k| k.as_str()) + .collect(); + + let allowlist: std::collections::BTreeSet<&str> = + ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES + .iter() + .copied() + .collect(); + + let known_v1_additions: std::collections::BTreeSet<&str> = + STANDARD_V1_JSON_SCHEMA_KEYWORDS_KEPT_IN_TRANSITION + .iter() + .copied() + .collect(); + + 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 allowlist_matches_v1_meta_schema_properties() { + 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 schema_properties: std::collections::BTreeSet<&str> = v1_schema + let v0_props: std::collections::BTreeSet<&str> = v0_schema .get("properties") .and_then(|p| p.as_object()) - .expect("v1 meta-schema must have a 'properties' object") + .unwrap() .keys() .map(|k| k.as_str()) .collect(); - - let allowlist: std::collections::BTreeSet<&str> = ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES - .iter() - .copied() + 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(); - 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: {:?}", - 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 - ); + 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)" + ); + } } } diff --git a/packages/rs-dpp/src/data_contract/document_type/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/v1/mod.rs index 97586c95973..c00386359b9 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v1/mod.rs @@ -185,7 +185,8 @@ mod tests { match dt { crate::data_contract::document_type::DocumentType::V0(v0) => v0, - crate::data_contract::document_type::DocumentType::V1(_) => { + crate::data_contract::document_type::DocumentType::V1(_) + | crate::data_contract::document_type::DocumentType::V2(_) => { panic!("expected V0 from first() version routing") } } 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-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/insert/insert_contract/v0/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/mod.rs index 2dc73f217df..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 @@ -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; @@ -21,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 { @@ -283,15 +284,37 @@ impl Drive { type_key.as_bytes(), ]; - // primary key tree + // 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]); - self.batch_insert_empty_tree( - type_path, - key_info, - storage_flags.as_ref(), - &mut batch_operations, - &platform_version.drive, - )?; + 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, + )?, + TreeType::CountTree => self.batch_insert_empty_count_tree( + type_path, + key_info, + storage_flags.as_ref(), + &mut batch_operations, + &platform_version.drive, + )?, + _ => 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 @@ -322,3 +345,613 @@ 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, GroveDb, PathTrunkChunkQuery}; + + 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") + } + + 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); + 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), + } + } + + #[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. + #[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); + } + + /// 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/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..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 @@ -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). /// 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 1c25939590a..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 @@ -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; @@ -348,14 +349,36 @@ impl Drive { type_key.as_bytes(), ]; - // 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, - )?; + // 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, + )?, + 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, + )?, + _ => 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 @@ -381,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 @@ -462,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 @@ -534,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 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 5613d134371..ebcaf8844c9 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, @@ -475,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/dpp_versions/dpp_contract_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs index a36bbeccc26..eaba292f1a3 100644 --- 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 @@ -5,8 +5,8 @@ use crate::version::dpp_versions::dpp_contract_versions::{ }; use versioned_feature_core::FeatureVersionBounds; -// Introduced in protocol version 12, document_type_schema is changed to v1 -// which adds additionalProperties: false at the top level of document meta-schema +// Introduced in protocol version 12. Adds documents_countable feature via try_from_schema v2, +// and uses v1 document meta-schema with additionalProperties: false at the top level. pub const CONTRACT_VERSIONS_V4: DPPContractVersions = DPPContractVersions { max_serialized_size: 65000, contract_serialization_version: FeatureVersionBounds { @@ -34,7 +34,7 @@ pub const CONTRACT_VERSIONS_V4: DPPContractVersions = DPPContractVersions { index_levels_from_indices: 0, }, class_method_versions: DocumentTypeClassMethodVersions { - try_from_schema: 1, + try_from_schema: 2, // changed: supports documentsCountable create_document_types_from_document_schemas: 1, }, structure_version: 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/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, }, diff --git a/packages/rs-platform-version/src/version/v12.rs b/packages/rs-platform-version/src/version/v12.rs index 901dae3905c..10fbec6e054 100644 --- a/packages/rs-platform-version/src/version/v12.rs +++ b/packages/rs-platform-version/src/version/v12.rs @@ -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_V4, // changed: use v1 document meta-schema with additionalProperties: false + contract_versions: CONTRACT_VERSIONS_V4, // changed: try_from_schema v2 supports documentsCountable + v1 document meta-schema with additionalProperties: false document_versions: DOCUMENT_VERSIONS_V3, identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, 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) 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(); + }); +});