Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -4357,6 +4357,93 @@ 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.
//
// 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 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 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!(
unexpected.iter().any(|v| v.as_str() == Some("keywords")),
"expected `keywords` in rejected properties, got {unexpected:?}"
);
Comment on lines +4403 to +4438
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: The assertion is weaker than the error type allows and can pass on unrelated failures mentioning keywords

The test only checks js_err.error_summary().contains("keywords"). In this codebase JsonSchemaError exposes structured fields such as keyword(), instance_path(), and property_name(), so the test can assert that the failure came from the document-type schema rejecting an unexpected property rather than from any future validation error whose summary also happens to mention keywords. As written, the test proves that some schema error referenced the string, not that the intended additionalProperties or unevaluatedProperties rejection occurred at the injected document-type path.

source: ['claude']

Comment on lines +4404 to +4438
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Test couples to validator's internal params shape across two keywords

The test branches on keyword == "additionalProperties" vs "unevaluatedProperties" and reads two different param keys ("additionalProperties" vs "unexpected") out of js_err.params(). This is intentionally precise — it guards against ambiguity in which keyword the JSON-schema validator emits — but it ties the assertion tightly to internal validator structure that has changed historically. If the underlying validator switches keyword/param-key naming, this test will fail for unrelated reasons. A simpler assertion (e.g. matching "keywords" in the rendered error summary while still asserting the keyword is one of additional/unevaluatedProperties) would be more resilient. Not blocking — current form does work today.

source: ['claude']

}
other => panic!(
"expected BasicError::JsonSchemaError, got ConsensusError: {other:?}"
),
},
other => panic!("expected ProtocolError::ConsensusError, got: {other:?}"),
}
}
}

mod descriptions {
Expand Down
Loading