From e47d531c833c05c92d03b70b056e9bee08a1a752 Mon Sep 17 00:00:00 2001 From: thephez Date: Thu, 9 Apr 2026 16:25:59 -0400 Subject: [PATCH 1/7] docs(dpp): clarify contract-level keywords scope and fix max limit in comments Co-Authored-By: Claude Sonnet 4.6 --- .../src/data_contract/methods/validate_update/v0/mod.rs | 2 +- packages/rs-dpp/src/data_contract/v1/data_contract.rs | 4 ++-- .../data_contract_create/basic_structure/v0/mod.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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..faab2918b14 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. @@ -113,7 +113,7 @@ pub struct DataContractV1 { /// The tokens on the contract. pub tokens: BTreeMap, - /// The contract's keywords for searching + /// Contract-level keywords for searching (distinct from per-document-type keywords in document-meta schema) pub keywords: Vec, /// The contract's description 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( From 44d7038aa1078480e5238e84aa520215d3adf64a Mon Sep 17 00:00:00 2001 From: thephez Date: Thu, 9 Apr 2026 16:26:34 -0400 Subject: [PATCH 2/7] fix(dpp): remove keywords from document-meta schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field was added by mistake in #2523 — keywords belong to the contract level (DataContractV1), not document types. DocumentTypeV0/V1 have no keywords field and try_from_schema never reads it, so the schema entry was silently accepted then discarded. Co-Authored-By: Claude Sonnet 4.6 --- .../meta_schemas/document/v0/document-meta.json | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v0/document-meta.json index 3747878cf17..4f66bf117bf 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 @@ -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 From 6d234e912fad93e75f90cd2cd89b4662911f93b0 Mon Sep 17 00:00:00 2001 From: thephez Date: Thu, 9 Apr 2026 16:45:11 -0400 Subject: [PATCH 3/7] docs(dpp): revert DataContractV1 keywords field comment Co-Authored-By: Claude Sonnet 4.6 --- packages/rs-dpp/src/data_contract/v1/data_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 faab2918b14..271cfa7615c 100644 --- a/packages/rs-dpp/src/data_contract/v1/data_contract.rs +++ b/packages/rs-dpp/src/data_contract/v1/data_contract.rs @@ -113,7 +113,7 @@ pub struct DataContractV1 { /// The tokens on the contract. pub tokens: BTreeMap, - /// Contract-level keywords for searching (distinct from per-document-type keywords in document-meta schema) + /// The contract's keywords for searching pub keywords: Vec, /// The contract's description From 8c4244c175167fc8e94076b92487185fb15fd6a9 Mon Sep 17 00:00:00 2001 From: thephez Date: Thu, 16 Apr 2026 10:41:48 -0400 Subject: [PATCH 4/7] revert(dpp): restore keywords in v0 document-meta schema v0 is a frozen historical meta schema no longer on the active validation path (protocol v12 switched to v1). Removing it from v0 had no runtime effect but risked diverging from what was validated when legacy contracts were created. The real fix belongs on the v1 schema, which is applied in a follow-up commit. Reverts the schema portion of 9e56d4d. Co-Authored-By: Claude Sonnet 4.6 --- .../meta_schemas/document/v0/document-meta.json | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 4f66bf117bf..3747878cf17 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 @@ -588,6 +588,17 @@ "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 From 85081a85e8f5f42cdd9aae0ca94378756dbbcc2a Mon Sep 17 00:00:00 2001 From: thephez Date: Thu, 16 Apr 2026 10:43:56 -0400 Subject: [PATCH 5/7] fix(dpp): remove keywords from v1 document-meta schema and allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2523 mistakenly placed keywords in the document-type meta schema (correct location is contract-level, on DataContractV1). Neither DocumentTypeV0 nor DocumentTypeV1 has a keywords field, and try_from_schema never reads it. v1 is the active meta schema (protocol v12 sets document_type_schema to 1), so document-type keywords were still silently accepted in new contracts after the earlier v0 fix. Removing the field from the v1 schema and its v12-migration allowlist in lockstep — the parity test (allowlist_matches_v1_meta_schema_properties) requires both to change together. Any existing stored contract that happens to have document-type keywords will be cleanly stripped by the v12 migration in strip_unknown_document_schema_properties; safe because keywords on document types has never been read by Rust. Co-Authored-By: Claude Sonnet 4.6 --- .../meta_schemas/document/v1/document-meta.json | 11 ----------- .../schema/allowed_top_level_properties.rs | 1 - 2 files changed, 12 deletions(-) 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..a154b015d4d 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", From 3df26c16594e70ff90b8318f872daf7740a56ed9 Mon Sep 17 00:00:00 2001 From: thephez Date: Thu, 16 Apr 2026 12:16:33 -0400 Subject: [PATCH 6/7] test(dpp,drive-abci): add coverage for document-type keywords rejection and stripping Adds two tests targeting the behavior change of removing `keywords` from the v1 document-type meta schema and its allowlist: - `strips_keywords_from_document_schema` (rs-dpp): unit test that verifies the v12 migration `strip_unknown_properties_from_document_schema` removes a `keywords` key from a document-type schema. - `test_document_type_keywords_rejected_by_v1_meta_schema` (drive-abci): integration test pinned to protocol v12 that injects `keywords` onto a document-type schema, calls `DataContract::from_value` with full validation, and asserts the failure is specifically a `BasicError::JsonSchemaError` whose summary references `keywords` (not any unrelated error). Co-Authored-By: Claude Sonnet 4.6 --- .../schema/allowed_top_level_properties.rs | 25 ++++++++ .../data_contract_create/mod.rs | 63 +++++++++++++++++++ 2 files changed, 88 insertions(+) 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 a154b015d4d..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 @@ -78,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-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 { From 1fad422c6da584a2b2ff1ba590ab9a3153bf7b33 Mon Sep 17 00:00:00 2001 From: thephez Date: Thu, 23 Apr 2026 09:09:10 -0400 Subject: [PATCH 7/7] test(drive-abci): tighten meta-schema keyword rejection assertion Assert structured `JsonSchemaError` fields (keyword + offending property name in params) instead of a substring match on `error_summary`, and drop the unused platform/identity setup since `DataContract::from_value` is a pure DPP call. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../data_contract_create/mod.rs | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) 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 0970e17a129..9dba6ec1bd7 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 @@ -4325,14 +4325,11 @@ mod tests { // document type via its root-level `additionalProperties: false`. // Pinned to v12 because this is the specific version that introduced // v1 meta schema enforcement. + // + // No platform/identity setup: this test exercises meta-schema + // validation inside `DataContract::from_value`, which is a pure DPP + // call and never reaches Drive or the state-transition pipeline. 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", @@ -4362,13 +4359,40 @@ mod tests { 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(); + // The rejection must be driven by `additionalProperties` + // / `unevaluatedProperties`, and the offending property + // name must be `keywords` — not just any schema error + // whose summary happens to mention the string. + let keyword = js_err.keyword(); + assert!( + matches!( + keyword, + "additionalProperties" | "unevaluatedProperties" + ), + "expected additionalProperties/unevaluatedProperties rejection, got keyword={keyword:?}, summary={}", + js_err.error_summary() + ); + + let param_key = if keyword == "additionalProperties" { + "additionalProperties" + } else { + "unexpected" + }; + let unexpected = js_err + .params() + .get(param_key) + .ok() + .flatten() + .and_then(|v| v.as_array()) + .unwrap_or_else(|| { + panic!( + "expected params[{param_key:?}] array, got params={:?}", + js_err.params() + ) + }); assert!( - summary.contains("keywords"), - "expected JSON schema error to reference `keywords`, got: {summary}" + unexpected.iter().any(|v| v.as_str() == Some("keywords")), + "expected `keywords` in rejected properties, got {unexpected:?}" ); } other => panic!(