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..cbe3941ceb8 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 @@ -588,17 +588,6 @@ "type": "string" } }, - "keywords": { - "type": "array", - "description": "List of up to 20 descriptive keywords for the contract, used in the Keyword Search contract", - "items": { - "type": "string", - "minLength": 3, - "maxLength": 50 - }, - "maxItems": 20, - "uniqueItems": true - }, "additionalProperties": { "type": "boolean", "const": false 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..424fb087269 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 @@ -23,7 +23,6 @@ pub const ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[ "tokenCost", "properties", "transient", - "keywords", "additionalProperties", "required", "$comment", @@ -79,6 +78,31 @@ mod tests { assert!(keys.contains(&"additionalProperties")); } + #[test] + fn strips_keywords_from_document_schema() { + // `keywords` was erroneously placed on the document-type meta schema + // by PR #2523 — the intended location is contract-level + // (`DataContractV1.keywords`). This test guards the v12 migration + // path that removes any `keywords` key that slipped onto a + // document-type schema in stored state. + let mut schema = platform_value!({ + "type": "object", + "properties": {}, + "additionalProperties": false, + "keywords": ["one", "two"] + }); + + let changed = strip_unknown_properties_from_document_schema(&mut schema); + assert!(changed); + + let map = schema.as_map().unwrap(); + let keys: Vec<&str> = map.iter().filter_map(|(k, _)| k.as_text()).collect(); + assert!(!keys.contains(&"keywords")); + assert!(keys.contains(&"type")); + assert!(keys.contains(&"properties")); + assert!(keys.contains(&"additionalProperties")); + } + #[test] fn no_change_when_all_properties_are_known() { let mut schema = platform_value!({ 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..e39b4fe8198 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 @@ -268,7 +268,7 @@ impl DataContract { } if self.keywords() != new_data_contract.keywords() { - // Validate there are no more than 50 keywords + // Validate there are no more than 50 contract keywords if new_data_contract.keywords().len() > 50 { return Ok(SimpleConsensusValidationResult::new_with_error( TooManyKeywordsError::new(self.id(), new_data_contract.keywords().len() as u8) diff --git a/packages/rs-dpp/src/data_contract/v1/data_contract.rs b/packages/rs-dpp/src/data_contract/v1/data_contract.rs index a3c51868bcb..271cfa7615c 100644 --- a/packages/rs-dpp/src/data_contract/v1/data_contract.rs +++ b/packages/rs-dpp/src/data_contract/v1/data_contract.rs @@ -64,7 +64,7 @@ use platform_value::Value; /// ## 4. **Keywords** (`keywords: Vec`) /// - Keywords which contracts can be searched for via the new `search` system contract. /// - This vector can be left empty, but if populated, it must contain unique keywords. -/// - The maximum number of keywords is limited to 20. +/// - The maximum number of keywords is limited to 50. /// /// ## 5. **Description** (`description: Option`) /// - A human-readable description of the contract. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs index 8decf78865c..79ba1b8496f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs @@ -141,7 +141,7 @@ impl DataContractCreateStateTransitionBasicStructureValidationV0 for DataContrac } } - // Validate there are no more than 50 keywords + // Validate there are no more than 50 contract keywords if self.data_contract().keywords().len() > 50 { return Ok(SimpleConsensusValidationResult::new_with_error( ConsensusError::BasicError(BasicError::TooManyKeywordsError( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs index 03bb9a21c96..0970e17a129 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs @@ -4315,6 +4315,69 @@ mod tests { valid_keywords_for_verification.retain(|&x| x != keyword); } } + + #[test] + fn test_document_type_keywords_rejected_by_v1_meta_schema() { + use dpp::ProtocolError; + + // `keywords` is a contract-level field only. The v1 document-type + // meta schema (active as of protocol v12) must reject it on any + // document type via its root-level `additionalProperties: false`. + // Pinned to v12 because this is the specific version that introduced + // v1 meta schema enforcement. + let platform_version = PlatformVersion::get(12).expect("expected v12"); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let _platform_state = platform.state.load(); + let (_identity, _signer, _key) = + setup_identity(&mut platform, 958, dash_to_credits!(1.0)); + + let data_contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/keyword_test/keyword_base_contract.json", + None, + None, + false, + platform_version, + ) + .expect("expected to load contract"); + + let mut contract_value = data_contract + .to_value(platform_version) + .expect("to_value failed"); + + // Inject `keywords` onto the `preorder` document type schema — the + // wrong place for it. This should be rejected by the v1 meta + // schema during `DataContract::from_value` full validation. + contract_value["documentSchemas"]["preorder"]["keywords"] = + Value::Array(vec![Value::Text("invalid".to_string())]); + + let err = DataContract::from_value(contract_value, true, platform_version) + .expect_err("meta schema validation must reject document-type keywords"); + + // Assert the failure is specifically a JSON schema validation error + // (i.e. the meta schema rejected the unknown `keywords` property), + // not an unrelated error such as a serialization or structural issue. + match err { + ProtocolError::ConsensusError(consensus_err) => match *consensus_err { + ConsensusError::BasicError(BasicError::JsonSchemaError(js_err)) => { + // The rejection should be driven by `additionalProperties` + // / `unevaluatedProperties` at the meta-schema root, and + // the offending property name must be `keywords`. + let summary = js_err.error_summary(); + assert!( + summary.contains("keywords"), + "expected JSON schema error to reference `keywords`, got: {summary}" + ); + } + other => panic!( + "expected BasicError::JsonSchemaError, got ConsensusError: {other:?}" + ), + }, + other => panic!("expected ProtocolError::ConsensusError, got: {other:?}"), + } + } } mod descriptions {