feat(platform)!: anyOf and allOf reference expressions (PV14) - #4942
Conversation
A refersTo may be { "anyOf": [target, ...] } in place of one target, on
an identifier property or on the elements of a typed array, and holds if
at least one target holds. Targets are identity or permanentDocument (by
id or through a lookup); contract, token, deletableDocument,
identityPublicKey, a nested anyOf, keys beside anyOf and a repeated
target are refused on every parse. Registration caps a list at
SystemLimits::max_any_of_reference_targets (4) and counts every target
against max_references_per_document, and checks each target as the same
declaration alone.
At write time the targets of each value are checked in declared order
and the first that holds ends the check; every read is billed, and when
none holds the write is refused with the last target's error, so no new
StateError exists. A propertyAgreement belongs to its target.
DocumentPropertyReferenceTarget::AnyOf is appended (existing encodings
unchanged) and decodes through a guard that refuses an anyOf inside an
anyOf, so consensus error bytes cannot drive unbounded recursion. Joins
refuse an anyOf join property and preallocated indexes never bind one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedNext included review available in 54 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: dashpay/platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (35)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-09-23T13:25:03.148Z |
|
🕓 Queued for automated review — 11th in line, estimated start in ~1.4 h (commit 853cd59)
|
refersTo expressions: besides anyOf, an allOf holds if every operand holds for the same value, and the two nest (an operand is a leaf or an expression of the other combinator) up to SystemLimits::max_reference_expression_depth (4) combinators, each list holding at most max_reference_operands (4). Registration refuses two alike operands of one list, a leaf naming the declaring contract explicitly counting as one omitting it, and counts every leaf against max_references_per_document. At write time an expression is evaluated operand by operand in declared order: anyOf stops at the first that holds and otherwise refuses with the last operand's error, allOf stops at the first that fails and refuses with its error; every read is billed. AllOf is appended as variant 8. The decode guard now bounds nesting at MAX_REFERENCE_EXPRESSION_DECODE_DEPTH (16) instead of refusing it. Errors name a leaf by its path (anyOf[1].allOf[0]) in the parse and at registration. The meta-schema picks the form with if/then/else so a single target missing its type keeps a precise error. The ABCI tests use the shared reference_test_setup helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lpers
wasm-dpp2 reports a reference expression as { type: 'anyOf', anyOf: [...] }
or { type: 'allOf', allOf: [...] }, so the DocumentPropertyReferenceTarget
union stays internally tagged by `type` (CONVENTIONS.md, "Tagged unions")
and client code switching on `type` sees the combinators.
The lookup and expression parser suites share reference_test_helpers.rs,
and the drive join refusals for lookups and expressions run on one
fixture in reference_join_tests.rs. The malformed-leaf test asserts the
parser's message, and a shadowed name in the JS spec is renamed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment Merges anyOf/allOf (#4942) and the transient-object fixes (#4943, #4948), and reworks listElement to the shape Sam proposed: "refersTo": { "type": "listElement", "documentType": "electedCharter", "propertyAgreement": { "electedCharterId": "$id" }, "inList": "members" } - The list's document is found by the agreement pair with `$id` on the referenced side (exactly one, read from a stored identifier property, never $ownerId); `documentProperty`/`list` are gone. `$id` joins $ownerId and $creatorId as a referenced-side agreement name for every document reference. - A list element is a document reference: contractId allowed, `as_any_document_reference` carries it with `in_list`, so registration checks its contract, type and pairs through the shared code; the $id property needs no refersTo of its own. - Write time: the document is fetched by id through a per-write memo shared with every by-id reference (one fetch for the charter and its list elements); lists are collected once into a set. Replace triggers are the agreement's (binds_a_changed_property). - listElement is a combinable leaf of anyOf/allOf (target variant 9). - Tests, fixtures, meta-schema, changelog item 34, book and wasm-dpp2 updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the leader Built on #4940 (listElement), #4941 (ownerRefersTo) and #4942 (anyOf), merged in from v4.2-dev. The resignation request: - may be filed only by a member of the seated team: ownerRefersTo holds an anyOf of a listElement (the writer is in the elected charter's members, the charter found through the electedCharterId $id pair) and a lookup of an addedModerator for the charter keyed by the writer; the leader is in neither list; - carries a message encrypted to the leader, with the leader's decryption key bound to submittedCharter and the member's encryption key bound to joinRequest, the keys join requests already use; - is deletable (Sam), so it is a request the leader acts on with a removal and the member withdraws by deleting it. It changes the team by itself no longer: ElectedCharter::active_members drops its resignations input. The charter changelog item is renumbered 36 after the three new items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Issue being fixed or feature implemented
A
refersTonames exactly one target today. The moderation charters contract (#4898) needs a reference that holds when one of several targets does: a resignation must come from a member who is either listed in the elected charter or was added later through anaddedModerator. This PR lets arefersTobe a small expression at protocol version 14:{ "anyOf": [...] }holds if at least one operand holds,{ "allOf": [...] }if every operand holds for the same value, and the two nest up to 4 combinators deep. The charters contract itself is not edited here.What was done?
The declaration
refersTois one target, as before, or an object whose only key isanyOforallOf, a list of operands. An operand is a leaf (an ordinary target with its own keys) or an expression of the other combinator. Allowed on an identifier property and on theitemsof a typed array of identifiers (#4928).Shape rules, refused on every parse by meta-schema v3 and the parser:
{ "anyOf": [ { "type": "identity" } ] }{ "anyOf": [ ..., { "anyOf": [...] } ] }(same combinator directly inside){ "anyOf": [...], "type": "identity" }, apropertyAgreementbeside a combinator, or bothanyOfandallOfcontractleaf (at any depth)token,deletableDocument,identityPublicKey(either form) leavesRegistration limits, full validation only (like the other reference limits, so a stored contract is never re-judged):
SystemLimits::max_reference_expression_depth)max_reference_operands)max_references_per_document)Why only
identityandpermanentDocumentleaves (also in the meta-schema'sreferenceOperandsdescription): both are existence checks against entities that are never deleted, so an expression of them holds for good once it holds and is re-validated on replace only when its value or a property one of its leaves binds changed, exactly like a single target. The other types do not compose with other operands:deletableDocumentis re-validated on every replace and may be cleared once its document is deleted (the immutable-property exception assumes the property refers to that one target);identityPublicKeypairs the value with a key id property no other operand reads; acontracttarget's requirements are gates judged against the block time and the writer rather than an existence check, and a contract or token id is never also an identity or document id. Admitting another leaf type after 4.2 ships takes a newapply_property_referencegeneration and meta-schema, not an edit toCOMBINABLE_REFERENCE_TARGET_TYPES.Registration
Every leaf is checked exactly as the same declaration alone, at the same places: the contract parse for a lookup into the declaring contract (
create_document_types_from_document_schemasv1), the registration state validation for everything that needs state (data_contract_reference_validationv0). Every leaf must pass, since each has to be a declaration that could hold. Errors name the leaf by where it sits:Write time
validate_reference_v0(document reference validation v0) evaluates an expression operand by operand in declared order, a nested expression the same way; a leaf goes to the per-target check it always ran (validate_reference_target_v0, unchanged).anyOfstops at the first operand that holds; when none does, the result is the last operand's.allOfstops at the first operand that fails, with that operand's result.allOfwhose first operand fails reads nothing more.Decision: a refusal is always a leaf's own error, no new state error. Each leaf's failure already has a precise error with the path and the target embedded; a "no operand held" error would have to nest one consensus error per leaf or drop their reasons; and the author controls which failure a writer sees by ordering the lists (the most general
anyOfoperand last, the most tellingallOfoperand first).StateErrorand the JS error-code surface are unchanged, and a reference error never carries a combinator.A
propertyAgreementbelongs to its leaf and is judged only against that leaf's document:On replace, the bound-property rule moved into
binds_a_changed_property(same arms, same comments); an expression is re-validated as a whole when any of its leaves would be. A replace changingsubmittedCharterId(a key part the lookups read) re-validatesmemberId; one changing onlytitledoes not.Model and encoding
DocumentPropertyReferenceTarget::AnyOf(ReferenceOperands)(variant 7) andAllOf(ReferenceOperands)(8) are appended, so every existing target keeps its variant and its bytes (should_keep_the_encoding_of_a_reference_error_for_an_id_referencestill pins them). Serde writes{"anyOf": [...]}/{"allOf": [...]}.ReferenceOperandsdecodes through a depth guard refusing a nesting deeper thanMAX_REFERENCE_EXPRESSION_DECODE_DEPTH(16), tested with 100,000 claimed levels on every decoder; a test holds every protocol version'smax_reference_expression_depthbelow it so every registrable declaration decodes.leaves()/leaves_with_paths()walk the leaves (a single target is its own leaf at an empty path),expression_depth()andcombinator()describe the tree. Every consumer ofreference()/as_document_reference()/as_any_document_reference()was audited:validate_reference_lookup_sources(every parse), same-contract lookup check, registration validatorleaves_with_paths(): each leaf checked as alone, errors name the leafvalidate_reference_expressions(new, full validation)validate_reference_countPropertyReference::max_references)validate_no_immutable_deletable_element_references,deletable_document_reference_target_is_goneIdentifierWithReference(_)matches (identifier paths, query conditions, typed array element width, random documents)documentTypeReferences{ path, type: 'anyOf', anyOf: [...] }/{ path, type: 'allOf', allOf: [...] }, operands recursive: the list sits under the schema's own key andtypetags the union like every other member (wasm-dpp2 CONVENTIONS.md, "Tagged unions")Update rules
A changed expression is an incompatible schema change: the compatibility rule for
refersToalready freezes everything beneath it (the rule walker never descends intoanyOf/allOfas JSON Schema keywords). Tests cover single to expression and back, adding, removing, reordering and changing an operand, swappinganyOfforallOf, and nesting deeper, on a property and on typed array elements.Meta-schema
refersTopicks its form withif/then/else(anyOfpresent: only that key; elseallOfpresent: only that key; elsetyperequired), so an invalid single target keeps its precise error ("type is a required property") rather than an opaqueoneOffailure. Both combinators reference a shared$defs/referenceOperands(two or more unique operands, leaf types restricted), and each forbids its own combinator directly inside.Docs
v14 changelog item 33,
book/src/data-model/documents.md("Reference expressions (anyOf,allOf)"), the meta-schema descriptions, and the wasm-dpp2 TypeScript types (DocumentPropertyReferenceExpression,DocumentPropertyReferenceOperand, taggedtype: 'anyOf' | 'allOf').Test helpers
The parser suites for lookups and expressions share
reference_test_helpers.rs(the charter'sjoinRequesttype, identifier properties, the parse and the refusal check), and the drive join refusals for both kinds live in onereference_join_tests.rson one fixture. The ABCI suite uses the sharedreference_test_setupharness.Siblings
listElement(#4940) andownerRefersTo(#4941) are open, not merged (v4.2-dev fetched before starting and before pushing, still at af350a9), so there is no composing test for them here. Whichever of these lands after another:DocumentPropertyReferenceTargetat position 7; the second to merge renumbers (this PR pins anyOf 7 / allOf 8 inreference_expression.rs). AdmittinglistElementas a leaf before 4.2 ships means adding it toCOMBINABLE_REFERENCE_TARGET_TYPES, to the meta-schema'sreferenceOperandsleaftypeenum, an arm inbinds_a_changed_property, and a composing test.ownerRefersTotakes an expression by walkingleaves_with_paths()wherever it checks its declaration and evaluating throughvalidate_reference_v0; the charter'sownerRefersTo: { anyOf: [listElement, lookup] }is then one declaration.In-place changes to shipped generations
create_document_types_from_document_schemasv1 (same-contract lookup check)CONTRACT_VERSIONS_V2)leaves_with_paths(), which for a single declaration is the declaration itself at an empty path, so the walk and the error text are identical wherever no expression exists; an expression parses only whereapply_property_referenceisSome(0), PV14. Comment at the edit.data_contract_reference_validationv0validate_reference_target_declaration_v0unchanged (same order, errors, billing, memo); a single declaration is one leaf at the declaration's own path. Comment at the edit.document_reference_validationv0validate_reference_target_v0is the old per-target body, unchanged;validate_reference_v0sends a single target straight to it; the bound-propertymatchmoved verbatim intobinds_a_changed_property.should_validate_no_expression_at_protocol_version_13runs this generation at protocol version 13 on the fixture: the PV13 parse holds no reference, so it reads and bills nothing. Comments at the edits.Meta-schema v3, the parser (
apply_property_reference0) andtry_from_schemageneration 3 are PV14-only and unreleased (edited in place per the book).How Has This Been Tested?
SystemLimits(depth, operands at the top and nested, alike operands including the own-contract-id spelling, leaf count), each leaf checked as alone with its path in the error, PV13 gate, round trip including a depth-4 expression, model (leaves_with_paths,expression_depth,Display,max_references), decode guard (every decoder, the bound itself, one past it, 100,000 claimed levels, depth restored after a refusal, every version's limit within the bound), preallocation for both combinators, update incompatibility.reference-validation-contract-reference-expression.json): anyOf first/second/none, elements, identity-or-document, agreement per leaf, allOf all/first-fails/second-fails, nestedallOf(identity, anyOf(...)), depth-4 evaluation, replace re-validation on a changed key part, billing of failed operands and allOf short-circuit, the PV13 run of generation 0, registration success, registration failure atanyOf[1].allOf[1]. Mutation-checked: makingallOfstop on success fails four tests; making an expression never re-validate on replace fails the replace test.{ path, type: 'anyOf', anyOf }form, anallOfnested in ananyOf, a refused leaf type and a same-combinator nesting, each with its message; a TypeScript probe compiled withtsc --strictagainst the generateddpp.d.tsshows the recursive types narrow ontypeand refuse acontractoperand.cargo test -p dpp --all-features --lib: 4750 passed, 0 failedcargo test -p drive-abci --lib -- reference refers_to: 137 passed (the reference suites, including the 14 new tests)cargo test -p drive --lib -- reference_join_tests preallocat: all passed (the 4 join refusals on the shared fixture)cargo test -p json-schema-compatibility-validator,cargo test -p platform-version: all passedcargo clippy -p dpp --all-features --all-targets,cargo clippy -p drive -p drive-abci --all-targets,cargo clippy -p wasm-dpp2 --target wasm32-unknown-unknown, all-D warnings: cleancargo check -p wasm-dpp2 --target wasm32-unknown-unknown: clean;cargo fmt --all --check: cleanyarn workspace @dashevo/wasm-dpp2 build, then the wasm-dpp2 mocha unit suite: 1316 passing, 7 pendingyarn eslint tests/unit/DocumentPropertyReference.spec.ts: cleanBreaking Changes
Consensus-breaking at protocol version 14 (unreleased): meta-schema v3 and the parser admit reference expressions, and
DocumentPropertyReferenceTargetgains two appended variants (existing encodings unchanged). Rust code matching the enum exhaustively needsAnyOfandAllOfarms.SystemLimitsgainsmax_reference_operandsandmax_reference_expression_depth.Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code
PR Hygiene ·
853cd59/skip-botsproceeds without the ones not yet reported/self-reviewedonce the bots are doneWhen every box is checked the
PR Hygienecheck passes and this can merge.