Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ pub const ALLOWED_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[
"tokenCost",
"properties",
"transient",
"keywords",
"additionalProperties",
"required",
"$comment",
Expand Down Expand Up @@ -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!({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/rs-dpp/src/data_contract/v1/data_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ use platform_value::Value;
/// ## 4. **Keywords** (`keywords: Vec<String>`)
/// - 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<String>`)
/// - A human-readable description of the contract.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading