From ca0c6117de21711ac6386fa32bf9e65deabc4d8d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 21 Sep 2026 09:45:33 +0700 Subject: [PATCH] fix(dpp): a doctype schema that is not an object is refused as an invalid contract structure again `parse_can_be_deleted_by_moderators_for_keyword` (#4864) read the `canBeDeletedByModeratorsFor` window with `get_optional_integer` straight on the schema value, which converts the value to a map first. On a document type schema that is not an object it therefore failed with a raw `ValueError("structure error: value is not a map")` before the core parser reached its "document schema must be an object" check, so the refusal changed from `InvalidContractStructure` to `ValueError`. Two drive-abci tests that inject a `null` schema pin the old refusal and have been red on v4.2-dev since the merge. The reader now tolerates a schema that is no object, like every other doctype-level keyword read before the core parser, and a dpp test pins the refusal for null, string and array schemas on both the validated and the stored path. Co-Authored-By: Claude Fable 5.1 --- .../try_from_schema/common/mod.rs | 12 ++++++-- .../v3/moderators_delete_tests.rs | 30 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index caf79778b86..57de57dbcd1 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -2073,8 +2073,16 @@ pub(super) fn apply_can_be_deleted_by_moderators( pub(super) fn parse_can_be_deleted_by_moderators_for_keyword( schema: &Value, ) -> Result, ProtocolError> { - schema - .get_optional_integer::(CAN_BE_DELETED_BY_MODERATORS_FOR) + // A schema that is not an object carries no keyword. Like every other + // doctype-level keyword read before the core parser, this one must not be + // the first to fail on such a schema: the core parser refuses it as an + // invalid contract structure, and a raw value error here would replace + // that refusal. + let Ok(schema_map) = schema.to_map() else { + return Ok(None); + }; + + Value::inner_optional_integer_value::(schema_map, CAN_BE_DELETED_BY_MODERATORS_FOR) .map_err(consensus_or_protocol_value_error) } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/moderators_delete_tests.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/moderators_delete_tests.rs index 36f8ab58762..e019653d73d 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/moderators_delete_tests.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/moderators_delete_tests.rs @@ -320,3 +320,33 @@ fn should_refuse_a_window_that_is_not_a_positive_number_of_seconds_on_the_stored } } } + +#[test] +fn should_refuse_a_schema_that_is_not_an_object_as_an_invalid_contract_structure() { + // The window is read off the raw schema before the core parser checks that + // the schema is an object. It must not be the reader that fails first: a + // schema that is no object carries no keyword, and the core parser is the + // one that names the real problem. + let platform_version = PlatformVersion::latest(); + for full_validation in [true, false] { + for schema in [ + platform_value!(null), + platform_value!("post"), + platform_value!([]), + ] { + let error = parse_with_config( + schema.clone(), + &moderated_config(platform_version), + platform_version.protocol_version, + full_validation, + ) + .expect_err("a schema that is not an object must be refused"); + let message = format!("{error:?}"); + assert!( + message.contains("InvalidContractStructure"), + "schema {schema:?} must be refused as an invalid contract structure (full \ + validation: {full_validation}), got {message}" + ); + } + } +}