From 9961b898329369b4987f127b006b8937b7a212c9 Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Wed, 4 Nov 2026 19:57:15 +0700 Subject: [PATCH 1/2] fix(dpp)!: refuse a contest keyed by a nested or system property (PV15) A contested create keys its vote poll by looking each index property up by name among the document's top-level properties. A property path such as `records.identity` or a system property such as `$ownerId` is never found there, so the poll gets a null value and Drive fails every contested create of the type: the contest can never start. From protocol version 15 (`check_document_type_schema` 1), full validation refuses a contested index holding such a property with `InvalidContractStructure`. Stored contracts still load, and protocol version 14 and earlier are unchanged. Co-Authored-By: Claude Opus 5.5 --- book/src/contract-keywords/contested.md | 1 + .../try_from_schema/common/mod.rs | 48 ++++++ ...est_keyed_by_top_level_properties_tests.rs | 148 ++++++++++++++++++ .../class_methods/try_from_schema/v3/mod.rs | 2 + .../contest_keyed_by_top_level_properties.rs | 24 +++ .../data_contract/document_type/index/mod.rs | 1 + .../rs-platform-version/src/version/v15.rs | 13 +- 7 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/contest_keyed_by_top_level_properties_tests.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/index/contest_keyed_by_top_level_properties.rs diff --git a/book/src/contract-keywords/contested.md b/book/src/contract-keywords/contested.md index d54c64a66b0..831f70d9dda 100644 --- a/book/src/contract-keywords/contested.md +++ b/book/src/contract-keywords/contested.md @@ -72,6 +72,7 @@ The fund, the windows, the tallies and the special case of moderation elections - `resolution` is present and is `0` or `1`; `1` is refused before protocol version 14. - `fieldMatches`, when present, holds at least one entry, and each `regexPattern` is a valid regular expression (`RegexError`, 10247). - From protocol version 15, the contest is decided by the index values, so that two documents with the same values are both contested or both ordinary: every `fieldMatches` field is a property of the index, and a match on a string property that may be empty (no `minLength` of at least 1) treats `""` and `"\0"` alike, since the two share one index key (`InvalidContractStructure`, 10231). Otherwise an ordinary document could take the values of a contested one, and the contest would have nothing left to award. A contract registered earlier keeps its contest, but can no longer be updated, since an update is checked like a registration; while such a contest is live, a create outside it with the same index values is refused (`DuplicateUniqueIndexError`). +- From protocol version 15, every property of the index is a top-level property of the document: no property path such as `records.identity` and no system property such as `$ownerId` (`InvalidContractStructure`, 10231). A contested create finds the contest by reading each index value from the document's top-level properties, where such a property is never found, so no contested create could open or join the contest. A contract registered earlier keeps such an index, but can no longer be updated. - A contested index cannot carry a [`timeRange`](time-range.md), an [`integerRange`](integer-range.md) or a ranking (a ranking needs a non-unique index), and an [index-only type](index-only.md) cannot have one. - A document type with a contested index cannot set [`ttl`](ttl.md), nor `moderatorAbilities.delete` (see [Deletion](deletion.md)): a moderator's restore puts a document back by an ordinary insert, and a contested value is only awarded through a vote. - From protocol version 14, a document type with a contested index that sums a property ([`summable`, `averageable`, `documentsSummable` or `documentsAverageable`](aggregates.md)) declares that property with a `minimum` of at least -134217728 and a `maximum` of at most 134217728 (±2^27, `max_contested_summed_value_magnitude`). The end of a contest adds the winner's value to the type's sums, after any number of other documents were written, and a sum that left the signed 64-bit range there could not be stored. Values this small keep the sums in range short of 2^36 documents. Checked when a contract is registered or updated, so contracts registered earlier keep their bounds. diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index f4d034b78c9..9c9b33c5f72 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -800,6 +800,47 @@ fn refuse_a_contest_not_decided_by_index_values( } } +/// A rule of full validation under `check_document_type_schema` 1 (protocol version 15): the +/// contested `index` of the type `name` must be keyed by top-level properties of the document +/// ([`Index::contest_is_keyed_by_top_level_properties`]), or no contested create could open or +/// join its contest. Version 0 refuses nothing, so a contract registered earlier keeps such an +/// index. +#[cfg(feature = "validation")] +fn refuse_a_contest_keyed_by_a_nested_or_system_property( + index: &Index, + name: &str, + platform_version: &PlatformVersion, +) -> Result<(), ProtocolError> { + match platform_version + .dpp + .contract_versions + .document_type_versions + .schema + .check_document_type_schema + { + 0 => Ok(()), + 1 => { + if index.contest_is_keyed_by_top_level_properties() { + Ok(()) + } else { + Err(consensus_or_protocol_data_contract_error( + DataContractError::InvalidContractStructure(format!( + "the contest of index \"{}\" on document type \"{name}\" must be keyed \ + by top-level properties of the document: a property path or a system \ + property (starting with `$`) can not key a contest", + index.name + )), + )) + } + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentType::check_document_type_schema".to_string(), + known_versions: vec![0, 1], + received: version, + }), + } +} + /// The schema check of full validation, versioned on `check_document_type_schema`: 0 checks /// the enriched schema against the document meta-schema through the `jsonschema` crate and /// compiles it; 1 (protocol version 15) checks it natively ([`native_schema`]), with the @@ -1539,6 +1580,13 @@ fn parse_indices( ctx.platform_version, )?; + // A contest keyed by a nested or system property (protocol version 15) + refuse_a_contest_keyed_by_a_nested_or_system_property( + &index, + ctx.name, + ctx.platform_version, + )?; + if ctx.generation.admit_indexed_arrays { validate_indexed_array_index( ctx, diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/contest_keyed_by_top_level_properties_tests.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/contest_keyed_by_top_level_properties_tests.rs new file mode 100644 index 00000000000..61836aeb08e --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/contest_keyed_by_top_level_properties_tests.rs @@ -0,0 +1,148 @@ +//! A contested index keyed by a property path or a system property, which registration +//! refuses from protocol version 15 (`check_document_type_schema` 1): no contested create can +//! open or join such a contest. A registration rule: a stored contract still parses, and +//! protocol version 14 keeps registering such an index. +use super::*; +use crate::consensus::basic::BasicError; +use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; +use platform_value::platform_value; + +fn parse_at( + schema: Value, + protocol_version: u32, + full_validation: bool, +) -> Result { + let platform_version = + PlatformVersion::get(protocol_version).expect("expected platform version"); + let config = DataContractConfig::default_for_version(platform_version) + .expect("default config available"); + DocumentType::try_from_schema( + Identifier::new([1; 32]), + 1, + config.version(), + "name", + schema, + None, + &BTreeMap::new(), + &config, + full_validation, + &mut vec![], + platform_version, + ) +} + +fn parse(schema: Value) -> Result { + parse_at(schema, PlatformVersion::latest().protocol_version, true) +} + +/// An immutable `name` type of `normalizedLabel` and a `records` object holding an `identity`, +/// whose unique `byKeyAndLabel` index on `key` and `normalizedLabel` is contested on +/// `normalizedLabel`. +fn name_schema(key: &str) -> Value { + platform_value!({ + "type": "object", + "documentsMutable": false, + "canBeDeleted": false, + "properties": { + "normalizedLabel": { "type": "string", "maxLength": 20, "position": 0 }, + "records": { + "type": "object", + "properties": { + "identity": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "contentMediaType": "application/x.dash.dpp.identifier", + "position": 0, + }, + }, + "required": ["identity"], + "additionalProperties": false, + "position": 1, + }, + "group": { "type": "string", "maxLength": 20, "position": 2 }, + }, + "required": ["normalizedLabel", "records", "group"], + "indices": [{ + "name": "byKeyAndLabel", + "properties": [{ key: "asc" }, { "normalizedLabel": "asc" }], + "unique": true, + "contested": { + "fieldMatches": [ + { "field": "normalizedLabel", "regexPattern": "^[a-z]{3,5}$" } + ], + "resolution": 0, + }, + }], + "additionalProperties": false, + }) +} + +fn assert_refused(result: Result) { + let error = result.expect_err("the document type must be refused"); + let ProtocolError::ConsensusError(consensus_error) = &error else { + panic!("expected a paid consensus error, got {error:?}"); + }; + let ConsensusError::BasicError(BasicError::ContractError( + DataContractError::InvalidContractStructure(message), + )) = consensus_error.as_ref() + else { + panic!("expected InvalidContractStructure, got {consensus_error:?}"); + }; + assert!( + message.contains( + "the contest of index \"byKeyAndLabel\" on document type \"name\" must be keyed by \ + top-level properties" + ), + "{message}" + ); +} + +#[test] +fn should_refuse_a_contest_keyed_by_a_property_path() { + assert_refused(parse(name_schema("records.identity"))); +} + +#[test] +fn should_refuse_a_contest_keyed_by_a_system_property() { + assert_refused(parse(name_schema("$ownerId"))); +} + +#[test] +fn should_register_a_contest_keyed_by_top_level_properties() { + let document_type = + parse(name_schema("group")).expect("a contest on top-level properties registers"); + let index = document_type + .find_contested_index() + .expect("expected the contested index"); + assert!(index.contest_is_keyed_by_top_level_properties()); +} + +#[test] +fn should_register_a_unique_index_without_a_contest_keyed_by_a_property_path() { + let mut schema = name_schema("records.identity"); + let Some(Value::Array(indices)) = schema.get_mut("indices").expect("indices") else { + panic!("expected the indices"); + }; + indices[0] + .remove("contested") + .expect("expected to remove the contest"); + parse(schema).expect("only a contest must be keyed by top-level properties"); +} + +#[test] +fn should_register_a_contest_keyed_by_a_property_path_at_protocol_version_14() { + parse_at(name_schema("records.identity"), 14, true) + .expect("protocol version 14 keeps registering it"); + parse_at(name_schema("$ownerId"), 14, true).expect("protocol version 14 keeps registering it"); +} + +#[test] +fn should_load_a_stored_contest_keyed_by_a_property_path() { + let latest = PlatformVersion::latest().protocol_version; + parse_at(name_schema("records.identity"), latest, false) + .expect("a stored contract runs no registration rule"); + parse_at(name_schema("$ownerId"), latest, false) + .expect("a stored contract runs no registration rule"); +} diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs index 0abae10018f..0dbec5f6a71 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs @@ -2089,6 +2089,8 @@ mod commit_reveal_lookup_tests; #[cfg(all(test, feature = "validation"))] mod contest_decided_by_index_values_tests; #[cfg(all(test, feature = "validation"))] +mod contest_keyed_by_top_level_properties_tests; +#[cfg(all(test, feature = "validation"))] mod contested_summed_value_bounds_tests; #[cfg(all(test, feature = "validation"))] mod delete_constraints_tests; diff --git a/packages/rs-dpp/src/data_contract/document_type/index/contest_keyed_by_top_level_properties.rs b/packages/rs-dpp/src/data_contract/document_type/index/contest_keyed_by_top_level_properties.rs new file mode 100644 index 00000000000..f207eddaf2d --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/index/contest_keyed_by_top_level_properties.rs @@ -0,0 +1,24 @@ +//! Whether every property of a contested index is a top-level property of the document. The +//! vote poll of a contest takes each index value from the document's top-level properties by +//! name, so a nested or system property gives the poll a null value, and no contested create +//! can open or join the contest. + +use crate::data_contract::document_type::Index; + +impl Index { + /// Whether this index's contest, if it has one, is keyed only by top-level properties of + /// the document: no property path (`records.identity`) and no system property + /// (`$ownerId`). A contested create keys its vote poll by looking each index property up + /// by name among the document's top-level properties ([`Index::extract_values`]), where a + /// property path or a system property is never found. The poll then holds a null value + /// where the contender's index entry holds the real one, and Drive fails every contested + /// create of the type without storing it, so the contest can never start. An index without + /// a contest is keyed by any of its properties. + pub fn contest_is_keyed_by_top_level_properties(&self) -> bool { + self.contested_index.is_none() + || self + .properties + .iter() + .all(|property| !property.name.starts_with('$') && !property.name.contains('.')) + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/index/mod.rs b/packages/rs-dpp/src/data_contract/document_type/index/mod.rs index d310375c939..8f37704ea6c 100644 --- a/packages/rs-dpp/src/data_contract/document_type/index/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/index/mod.rs @@ -32,6 +32,7 @@ use std::{collections::BTreeMap, convert::TryFrom}; mod bucketing; mod contest_decided_by_values; +mod contest_keyed_by_top_level_properties; mod derived_index_property; mod extract_contested_values; pub mod integer_range; diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 1a0874a0dcd..9250fe74d94 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -162,7 +162,8 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// (`InvalidContractStructure`; a stored contract with one, which no JSON /// could have written, must drop it to be updated), /// refuses a contested index whose contest its index values do not decide -/// (`InvalidContractStructure`, note 19), +/// (`InvalidContractStructure`, note 19) or that is keyed by a property path or a +/// system property (`InvalidContractStructure`, note 20), /// and refuses a document type the typed validation schema does not represent /// (`InvalidContractStructure`), so every type registered from this version /// has its documents checked natively (note 6). The parser is generation 3 @@ -428,6 +429,16 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// registered earlier with such a contest keeps it, but an update, which carries the /// whole contract, is refused like a registration. Protocol v14 and earlier keep /// `check_document_type_schema` 0 and document create state validation 2. +/// 20. **A contest is keyed by top-level properties**: a contested create keys its vote poll +/// by looking each index property up by name among the document's top-level properties +/// (`Index::extract_values`), where a property path (`records.identity`) or a system +/// property (`$ownerId`) is never found, so the poll holds a null value and Drive fails +/// every contested create of the type without storing it: the contest can never start. +/// Registration under `check_document_type_schema` 1 (note 7) refuses a contested index +/// holding one (`Index::contest_is_keyed_by_top_level_properties`, +/// `InvalidContractStructure`). A contract registered earlier with one keeps it, but an +/// update is refused like a registration. Protocol v14 and earlier keep +/// `check_document_type_schema` 0. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 10) and verify_private_document_entries Some(0) a page of a private type's store (note 13); state transition method versions v5: document_create_transition 1 deletes a consumed document from what its lookup read (note 11); document method versions v5: insert_empty_private_document_store_operation Some(0) creates a private document type's store, and fetch_private_document_entries and prove_private_document_entries Some(0) read and prove a page of it (note 13); apply_drive_operations 2 resolves a batch's estimate, remove_reference_for_index_level_for_contract_operations 2, stateless_delete_of_non_tree_for_costs 1 and for_total_system_credits_update 1 describe their layers as they are (note 14); create_initial_state_structure 5 adds the token marketplace escrow trees; calculate_total_tokens_balance 2 counts escrowed tokens; the escrow primitives; the identity balance, token balance, prefunded specialized balance and contract fee pot estimations (for_balances 1, for_token_balances 1, estimated_cost_for_prefunded_specialized_balance_update 2, add_estimation_costs_for_contract_fee_pot_update 1) keep a deeper layer description (note 17); fetch_core_fork_activation_height and store_core_fork_activation_height Some(0) keep the Core fork activations (note 18) From 2b68e4fddb002395ac746236fda9e5d03e12d0f6 Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Wed, 4 Nov 2026 20:13:23 +0700 Subject: [PATCH 2/2] test(drive-abci): refuse a contract update keeping a contest keyed by a nested property The refusal of a contested index keyed by a property path or a system property is a full validation rule, so an update, which carries the whole contract, must hit it too. Two DPNS fixtures key the contest by `records.identity` and by `$ownerId`: registration refuses both at the latest version and accepts both at protocol version 14, and an update of a stored contract keeping the nested key is refused with `InvalidContractStructure`. Co-Authored-By: Claude Opus 5.5 --- .../state_transition/state_transitions/mod.rs | 151 ++++++++++++++++ ...ct-contested-keyed-by-nested-property.json | 169 ++++++++++++++++++ ...-contract-contested-keyed-by-owner-id.json | 169 ++++++++++++++++++ 3 files changed, 489 insertions(+) create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-nested-property.json create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-owner-id.json diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs index eff3ea6b211..1854fa60e93 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs @@ -5078,4 +5078,155 @@ pub(in crate::execution) mod tests { assert!(result.is_valid(), "{:?}", result.errors); } } + + /// A contested index keyed by a property path or a system property. A contested create + /// keys its vote poll by reading each index value from the document's top-level + /// properties, where such a property is never found, so no contested create can open or + /// join the contest. From protocol version 15 registration, and an update, which carries + /// the whole contract, refuse such an index. + mod contest_keyed_by_top_level_properties { + use super::*; + use crate::config::PlatformConfig; + use dpp::consensus::basic::BasicError; + use dpp::dashcore::Network; + use dpp::data_contract::config::DataContractConfig; + use dpp::data_contract::errors::DataContractError; + use dpp::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; + use dpp::ProtocolError; + + /// DPNS's contested index with `records.identity` in place of the parent domain. + const NESTED_PROPERTY_CONTRACT: &str = + "tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-nested-property.json"; + + /// DPNS's contested index with `$ownerId` in place of the parent domain. + const OWNER_ID_CONTRACT: &str = + "tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-owner-id.json"; + + /// From protocol version 15 registration refuses both shapes. + #[test] + fn should_refuse_registering_a_contest_keyed_by_a_nested_or_system_property_at_the_latest_protocol_version( + ) { + for contract_path in [NESTED_PROPERTY_CONTRACT, OWNER_ID_CONTRACT] { + let error = json_document_to_contract_with_ids( + contract_path, + None, + None, + true, + PlatformVersion::latest(), + ) + .expect_err("registration must refuse the contest"); + assert_matches!( + error, + ProtocolError::ConsensusError(consensus_error) + if matches!( + consensus_error.as_ref(), + ConsensusError::BasicError(BasicError::ContractError( + DataContractError::InvalidContractStructure(_) + )) + ), + "{contract_path}" + ); + } + } + + /// Protocol version 14 keeps registering both shapes. + #[test] + fn should_register_a_contest_keyed_by_a_nested_or_system_property_at_protocol_version_14() { + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + for contract_path in [NESTED_PROPERTY_CONTRACT, OWNER_ID_CONTRACT] { + json_document_to_contract_with_ids( + contract_path, + None, + None, + true, + platform_version, + ) + .unwrap_or_else(|error| { + panic!("protocol version 14 must register {contract_path}: {error:?}") + }); + } + } + + /// From protocol version 15 a contract registered earlier with such an index can no + /// longer be updated: an update carries the whole contract, which full validation + /// checks like a registration. + #[tokio::test] + async fn should_refuse_updating_a_contract_keeping_a_contest_keyed_by_a_property_path_at_the_latest_protocol_version( + ) { + let platform_config = PlatformConfig { + network: Network::Mainnet, + ..Default::default() + }; + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .with_config(platform_config) + .build_with_mock_rpc() + .set_initial_state_structure(); + let platform_version = PlatformVersion::latest(); + + let mut rng = StdRng::seed_from_u64(0x0F1E_1D10); + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(2.0)); + + // The fixture's config is version 0, which an update can no longer keep + let contract = setup_contract( + &platform.drive, + NESTED_PROPERTY_CONTRACT, + None, + Some(identity.id().to_buffer()), + Some(|contract: &mut DataContract| { + contract.set_config( + DataContractConfig::default_for_version(platform_version) + .expect("expected the default config"), + ) + }), + None, + Some(platform_version), + ); + let mut updated_contract = contract.clone(); + updated_contract.set_version(2); + + let transition = DataContractUpdateTransition::new_from_data_contract( + updated_contract, + &identity.into_partial_identity_info(), + key.id(), + 1, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expected to create the update transition"); + let serialized_transition = transition + .serialize_to_bytes() + .expect("expected to serialize the transition"); + + let platform_state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized_transition], + &platform_state, + &BlockInfo::default_with_time(3000), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process the state transition"); + + assert_matches!( + processing_result.into_execution_results().remove(0), + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError(BasicError::ContractError( + DataContractError::InvalidContractStructure(message) + )), + .. + } if message.contains("must be keyed by top-level properties") + ); + } + } } diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-nested-property.json b/packages/rs-drive-abci/tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-nested-property.json new file mode 100644 index 00000000000..0287528a72a --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-nested-property.json @@ -0,0 +1,169 @@ +{ + "$formatVersion": "0", + "id": "DWBXe9EXFPHxvbArQgT45uQR5gMmi8dfMpLhR5KSbwnZ", + "ownerId": "2QjL594djCH2NyDsn45vd6yQjEDHupMKo7CEGVTHtQxU", + "version": 1, + "documentSchemas": { + "domain": { + "documentsMutable": false, + "canBeDeleted": true, + "transferable": 1, + "tradeMode": 1, + "type": "object", + "indices": [ + { + "name": "parentNameAndLabel", + "properties": [ + { + "records.identity": "asc" + }, + { + "normalizedLabel": "asc" + } + ], + "unique": true, + "contested": { + "fieldMatches": [ + { + "field": "normalizedLabel", + "regexPattern": "^[a-zA-Z01]{3,19}$" + } + ], + "resolution": 0, + "description": "If the normalized label part of this index is less than 20 characters (all alphabet a-z and 0 and 1) then this index is non unique while contest resolution takes place." + } + }, + { + "name": "identityId", + "nullSearchable": false, + "properties": [ + { + "records.identity": "asc" + } + ] + } + ], + "properties": { + "label": { + "type": "string", + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$", + "minLength": 3, + "maxLength": 63, + "position": 0, + "description": "Domain label. e.g. 'Bob'." + }, + "normalizedLabel": { + "type": "string", + "pattern": "^[a-hj-km-np-z0-9][a-hj-km-np-z0-9-]{0,61}[a-hj-km-np-z0-9]$", + "maxLength": 63, + "position": 1, + "description": "Domain label converted to lowercase for case-insensitive uniqueness validation. \"o\", \"i\" and \"l\" replaced with \"0\" and \"1\" to mitigate homograph attack. e.g. 'b0b'", + "$comment": "Must be equal to the label in lowercase. \"o\", \"i\" and \"l\" must be replaced with \"0\" and \"1\"." + }, + "parentDomainName": { + "type": "string", + "pattern": "^$|^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$", + "minLength": 0, + "maxLength": 63, + "position": 2, + "description": "A full parent domain name. e.g. 'dash'." + }, + "normalizedParentDomainName": { + "type": "string", + "pattern": "^$|^[a-hj-km-np-z0-9][a-hj-km-np-z0-9-\\.]{0,61}[a-hj-km-np-z0-9]$", + "minLength": 0, + "maxLength": 63, + "position": 3, + "description": "A parent domain name in lowercase for case-insensitive uniqueness validation. \"o\", \"i\" and \"l\" replaced with \"0\" and \"1\" to mitigate homograph attack. e.g. 'dash'", + "$comment": "Must either be equal to an existing domain or empty to create a top level domain. \"o\", \"i\" and \"l\" must be replaced with \"0\" and \"1\". Only the data contract owner can create top level domains." + }, + "preorderSalt": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "position": 4, + "description": "Salt used in the preorder document" + }, + "records": { + "type": "object", + "properties": { + "identity": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "position": 1, + "contentMediaType": "application/x.dash.dpp.identifier", + "description": "Identifier name record that refers to an Identity" + } + }, + "minProperties": 1, + "position": 5, + "additionalProperties": false + }, + "subdomainRules": { + "type": "object", + "properties": { + "allowSubdomains": { + "type": "boolean", + "description": "This option defines who can create subdomains: true - anyone; false - only the domain owner", + "$comment": "Only the domain owner is allowed to create subdomains for non top-level domains", + "position": 0 + } + }, + "position": 6, + "description": "Subdomain rules allow domain owners to define rules for subdomains", + "additionalProperties": false, + "required": [ + "allowSubdomains" + ] + } + }, + "required": [ + "$createdAt", + "$updatedAt", + "$transferredAt", + "label", + "normalizedLabel", + "normalizedParentDomainName", + "preorderSalt", + "records", + "subdomainRules" + ], + "additionalProperties": false, + "$comment": "In order to register a domain you need to create a preorder. The preorder step is needed to prevent man-in-the-middle attacks. normalizedLabel + '.' + normalizedParentDomain must not be longer than 253 chars length as defined by RFC 1035. Domain documents are immutable: modification and deletion are restricted" + }, + "preorder": { + "documentsMutable": false, + "canBeDeleted": true, + "type": "object", + "indices": [ + { + "name": "saltedHash", + "properties": [ + { + "saltedDomainHash": "asc" + } + ], + "unique": true + } + ], + "properties": { + "saltedDomainHash": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "position": 0, + "description": "Double sha-256 of the concatenation of a 32 byte random salt and a normalized domain name" + } + }, + "required": [ + "saltedDomainHash" + ], + "additionalProperties": false, + "$comment": "Preorder documents are immutable: modification and deletion are restricted" + } + } +} diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-owner-id.json b/packages/rs-drive-abci/tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-owner-id.json new file mode 100644 index 00000000000..6ba5eb8ce5f --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/dpns/dpns-contract-contested-keyed-by-owner-id.json @@ -0,0 +1,169 @@ +{ + "$formatVersion": "0", + "id": "DWBXe9EXFPHxvbArQgT45uQR5gMmi8dfMpLhR5KSbwnZ", + "ownerId": "2QjL594djCH2NyDsn45vd6yQjEDHupMKo7CEGVTHtQxU", + "version": 1, + "documentSchemas": { + "domain": { + "documentsMutable": false, + "canBeDeleted": true, + "transferable": 1, + "tradeMode": 1, + "type": "object", + "indices": [ + { + "name": "parentNameAndLabel", + "properties": [ + { + "$ownerId": "asc" + }, + { + "normalizedLabel": "asc" + } + ], + "unique": true, + "contested": { + "fieldMatches": [ + { + "field": "normalizedLabel", + "regexPattern": "^[a-zA-Z01]{3,19}$" + } + ], + "resolution": 0, + "description": "If the normalized label part of this index is less than 20 characters (all alphabet a-z and 0 and 1) then this index is non unique while contest resolution takes place." + } + }, + { + "name": "identityId", + "nullSearchable": false, + "properties": [ + { + "records.identity": "asc" + } + ] + } + ], + "properties": { + "label": { + "type": "string", + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$", + "minLength": 3, + "maxLength": 63, + "position": 0, + "description": "Domain label. e.g. 'Bob'." + }, + "normalizedLabel": { + "type": "string", + "pattern": "^[a-hj-km-np-z0-9][a-hj-km-np-z0-9-]{0,61}[a-hj-km-np-z0-9]$", + "maxLength": 63, + "position": 1, + "description": "Domain label converted to lowercase for case-insensitive uniqueness validation. \"o\", \"i\" and \"l\" replaced with \"0\" and \"1\" to mitigate homograph attack. e.g. 'b0b'", + "$comment": "Must be equal to the label in lowercase. \"o\", \"i\" and \"l\" must be replaced with \"0\" and \"1\"." + }, + "parentDomainName": { + "type": "string", + "pattern": "^$|^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$", + "minLength": 0, + "maxLength": 63, + "position": 2, + "description": "A full parent domain name. e.g. 'dash'." + }, + "normalizedParentDomainName": { + "type": "string", + "pattern": "^$|^[a-hj-km-np-z0-9][a-hj-km-np-z0-9-\\.]{0,61}[a-hj-km-np-z0-9]$", + "minLength": 0, + "maxLength": 63, + "position": 3, + "description": "A parent domain name in lowercase for case-insensitive uniqueness validation. \"o\", \"i\" and \"l\" replaced with \"0\" and \"1\" to mitigate homograph attack. e.g. 'dash'", + "$comment": "Must either be equal to an existing domain or empty to create a top level domain. \"o\", \"i\" and \"l\" must be replaced with \"0\" and \"1\". Only the data contract owner can create top level domains." + }, + "preorderSalt": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "position": 4, + "description": "Salt used in the preorder document" + }, + "records": { + "type": "object", + "properties": { + "identity": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "position": 1, + "contentMediaType": "application/x.dash.dpp.identifier", + "description": "Identifier name record that refers to an Identity" + } + }, + "minProperties": 1, + "position": 5, + "additionalProperties": false + }, + "subdomainRules": { + "type": "object", + "properties": { + "allowSubdomains": { + "type": "boolean", + "description": "This option defines who can create subdomains: true - anyone; false - only the domain owner", + "$comment": "Only the domain owner is allowed to create subdomains for non top-level domains", + "position": 0 + } + }, + "position": 6, + "description": "Subdomain rules allow domain owners to define rules for subdomains", + "additionalProperties": false, + "required": [ + "allowSubdomains" + ] + } + }, + "required": [ + "$createdAt", + "$updatedAt", + "$transferredAt", + "label", + "normalizedLabel", + "normalizedParentDomainName", + "preorderSalt", + "records", + "subdomainRules" + ], + "additionalProperties": false, + "$comment": "In order to register a domain you need to create a preorder. The preorder step is needed to prevent man-in-the-middle attacks. normalizedLabel + '.' + normalizedParentDomain must not be longer than 253 chars length as defined by RFC 1035. Domain documents are immutable: modification and deletion are restricted" + }, + "preorder": { + "documentsMutable": false, + "canBeDeleted": true, + "type": "object", + "indices": [ + { + "name": "saltedHash", + "properties": [ + { + "saltedDomainHash": "asc" + } + ], + "unique": true + } + ], + "properties": { + "saltedDomainHash": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "position": 0, + "description": "Double sha-256 of the concatenation of a 32 byte random salt and a normalized domain name" + } + }, + "required": [ + "saltedDomainHash" + ], + "additionalProperties": false, + "$comment": "Preorder documents are immutable: modification and deletion are restricted" + } + } +}