Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e47d531
docs(dpp): clarify contract-level keywords scope and fix max limit in…
thephez Apr 9, 2026
44d7038
fix(dpp): remove keywords from document-meta schema
thephez Apr 9, 2026
6d234e9
docs(dpp): revert DataContractV1 keywords field comment
thephez Apr 9, 2026
8c4244c
revert(dpp): restore keywords in v0 document-meta schema
thephez Apr 16, 2026
85081a8
fix(dpp): remove keywords from v1 document-meta schema and allowlist
thephez Apr 16, 2026
3df26c1
test(dpp,drive-abci): add coverage for document-type keywords rejecti…
thephez Apr 16, 2026
f1ef834
Merge branch 'v3.1-dev' into docs/keyword-clarification
thephez Apr 21, 2026
e89e0fc
Merge branch 'v3.1-dev' into docs/keyword-clarification
thephez Apr 22, 2026
1fad422
test(drive-abci): tighten meta-schema keyword rejection assertion
thephez Apr 23, 2026
73b972a
Merge branch 'v3.1-dev' into docs/keyword-clarification
thephez Apr 23, 2026
aa60910
Merge branch 'v3.1-dev' into docs/keyword-clarification
thephez Apr 27, 2026
4249c3a
Merge branch 'v3.1-dev' into docs/keyword-clarification
thephez Apr 28, 2026
6aa00c9
Merge branch 'v3.1-dev' into docs/keyword-clarification
thephez May 4, 2026
1239e2d
Merge branch 'v3.1-dev' into docs/keyword-clarification
thephez May 5, 2026
7b07054
Merge branch 'v3.1-dev' into docs/keyword-clarification
thephez May 19, 2026
ffc0e9d
test(dpp): exclude `keywords` from v0 baseline in transition allowlis…
thephez May 19, 2026
080bc89
Merge branch 'v3.1-dev' into docs/keyword-clarification
thephez May 20, 2026
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
11 changes: 0 additions & 11 deletions packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -656,17 +656,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 @@ -33,7 +33,6 @@ pub const ALLOWED_TRANSITION_TO_DOCUMENT_SCHEMA_V1_PROPERTIES: &[&str] = &[
"tokenCost",
"properties",
"transient",
"keywords",
"additionalProperties",
"required",
"$comment",
Expand Down Expand Up @@ -89,6 +88,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 Expand Up @@ -140,6 +164,13 @@ mod tests {
// other v1 addition (e.g. `documentsCountable` / `rangeCountable`)
// is excluded so the v2 parser cannot revive it on pre-v12 contracts
// and reinterpret a `NormalTree` as a count tree.
//
// `keywords` is a special case: it was erroneously placed on the v0
// document-type meta-schema by PR #2523 (its intended home is
// contract-level, `DataContractV1.keywords`). The v11→v12 migration
// deliberately strips it from stored bytes — see
// `strips_keywords_from_document_schema` — so it is excluded from
// the expected allowlist here even though it appears in v0.
let v0_schema: serde_json::Value = serde_json::from_str(include_str!(
"../../../../schema/meta_schemas/document/v0/document-meta.json"
))
Expand All @@ -151,6 +182,7 @@ mod tests {
.expect("v0 meta-schema must have a 'properties' object")
.keys()
.map(|k| k.as_str())
.filter(|k| *k != "keywords")
.collect();

let allowlist: std::collections::BTreeSet<&str> =
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 thread
thephez marked this conversation as resolved.
Comment thread
thephez marked this conversation as resolved.
}
other => panic!(
"expected BasicError::JsonSchemaError, got ConsensusError: {other:?}"
),
},
other => panic!("expected ProtocolError::ConsensusError, got: {other:?}"),
}
}
}

mod descriptions {
Expand Down
Loading