Skip to content

feat(platform)!: distinctFrom on identifier properties (PV14) - #4917

Merged
QuantumExplorer merged 13 commits into
v4.2-devfrom
feat/distinct-from-identifier-properties
Sep 22, 2026
Merged

QuantumExplorer merged 13 commits into
v4.2-devfrom
feat/distinct-from-identifier-properties

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Document schemas had no way to say that an identifier property must not equal another value of the same document. The first user is the members items of electedCharter in the moderation charters contract (#4898), where a charter's members must not include the owner. This adds the distinctFrom property keyword at protocol version 14: a pure structure rule, an identifier property's value must differ from the value of a named property of the same document, or from the document's $ownerId. No state is read.

"delegateId": {
  "type": "array", "byteArray": true, "minItems": 32, "maxItems": 32,
  "contentMediaType": "application/x.dash.dpp.identifier",
  "distinctFrom": "$ownerId",
  "position": 0
}

What was done?

Meta-schema v3 (edited in place, the unreleased generation): distinctFrom on the property keyword list, a string of 1 to 256 characters, restricted to identifier properties through the same dependentSchemas shape that restricts refersTo.

Parser (rs-dpp): DocumentProperty gains distinct_from: Option<DistinctFrom> (OwnerId or Property(path)), skipped in serde when absent, so contracts that do not declare it serialize exactly as before. The keyword is read by apply_distinct_from, gated on a new apply_distinct_from: OptionalFeatureVersion in the document type schema versions (None before protocol version 14, where it is ignored like refersTo and requiredSince; Some(0) in CONTRACT_VERSIONS_V6). Generation 3 then validates every target once all properties are parsed: it must be $ownerId or an existing identifier property of the same document type, and not the declaring property; any other $ system property is refused. Contract registration and update both go through this parse, so a bad target is a clear contract structure error on either.

Write time: DocumentTypeV0Methods::validate_distinct_from_properties (new validate_distinct_from: OptionalFeatureVersion in the document type method versions, Some(0) at protocol version 14) walks the flattened properties and judges each declared value with DistinctFrom::violation, which takes the property's own value as a parameter so that a typed array item can be judged by the same rule with the item's value. Document create structure validation 1 (protocol version 14, extended in place) and document replace structure validation 0 (shipped, extended in place, see the section below) call it inline after the document's schema validation, with the owner id the batch already carries; no Drive read, no new processor trait, no table bump. The replace validator's validate_structure now takes the owner id, so the two batch call sites pass identity.id. An equal pair is refused with the new basic error DocumentPropertyNotDistinctError (10419, the next code in the basic document band 10400-10449; BasicError discriminant 194, appended), which names the document type, the property and what it collided with. An absent named property passes. Transfer and purchase structure validation judge the stored document's $ownerId declarations against the new owner the action carries, so a transfer to, or a purchase by, the identity such a property names is refused the same way; both checks were added to the shipped v0 in place (see the section below). The update-price dispatcher read the transfer slot instead of its own (both 0 in every released table); it now reads document_update_price_transition_structure_validation. The parser records the declaring properties on the V2 document type (distinct_from_fields) so the write-time check reads a list instead of walking every property.

Typed array items: with typed scalar arrays (#4922) on the base, an identifier typed array carries distinctFrom on its items and every element must differ from the named value, which is what the charters contract's members needs. The parser refuses the keyword on the array itself and on elements of any other type, the meta-schema's array-item definition admits it on identifier elements only, and the write-time check judges each element with DistinctFrom::violation.

Update rules: distinctFrom gets a compatibility rule in json-schema-compatibility-validator like refersTo and requiredSince: adding, removing or changing it is an incompatible schema change on contract update.

Clients: wasm-dpp2 exposes the declarations through DataContract.documentTypeDistinctFrom(name) and the documentDistinctFrom map ({ path, distinctFrom } per declaration, mirroring the reference and immutability getters), plus a DocumentDistinctFromErrorCode enum with the new code; wasm-dpp mirrors the error. Swift and Kotlin parsers are out of scope for this PR.

Docs: item 26 of the v14 changelog, a "Distinct Identifier Properties" section in the book's documents chapter, and the conventions rule on shipped generations rewritten: frozen unless the change provably cannot modify consensus, in which case the PR description carries an "In-place changes to shipped generations" section (also in the versioned-dispatch chapter and the CLAUDE.md summary).

In-place changes to shipped generations

Per the conventions rule updated in this PR (book/src/contributing/coding-conventions.md, "Shipped generations are frozen unless the change cannot modify consensus"), these shipped modules were edited in place rather than copied into a new generation:

  • document_replace_transition_action/advanced_structure_v0 (now taking the owner id), document_transfer_transition_action/advanced_structure_v0 and document_purchase_transition_action/advanced_structure_v0, selected by every protocol version (their table slots stay 0). Each gained one call to validate_distinct_from_properties on the stored document and its new owner. It cannot modify consensus below protocol version 14: the meta-schemas of those versions refuse distinctFrom, their parser ignores it (apply_distinct_from is None, so no parsed property carries a declaration), and the dpp method's own gate validate_distinct_from is None there, so the call returns an empty result. should_not_judge_distinct_from_on_replace_before_protocol_version_14 and should_not_judge_distinct_from_on_transfer_or_purchase_before_protocol_version_14 run the three modules at 13 and 14 through their dispatchers. The batch advanced-structure v0 call site passes identity.id to the replace validator, which is the only edit to that shipped module; the value is unread below 14.
  • document_update_price_transition_action/mod.rs (the dispatcher, not a generation) read the transfer table slot instead of document_update_price_transition_structure_validation; both are 0 in every table, so the fix changes no outcome.

How Has This Been Tested?

  • cargo test -p dpp --lib -- data_contract::document_type errors::consensus: parses distinctFrom: "$ownerId" and top-level and nested property paths, and on the items of an identifier typed array (refused on the array itself and on non-identifier items), judging every element; refuses it on a non-identifier property (parser and meta-schema), on a missing path, on a non-identifier target, on itself, on another $ system property and on a non-string; refuses it at protocol version 13 under full validation and ignores it there without; accepts it at 14; a contract round-trips through serialize_to_bytes_with_platform_version with and without it; a contract update that adds, changes or removes it is refused as incompatible while an unchanged one passes; the BasicError discriminant test pins the new variant.
  • cargo test -p json-schema-compatibility-validator: the new rule's examples.
  • cargo test -p drive-abci --lib -- batch::tests::document::distinct_from: a create whose value equals $ownerId is refused with the new error and nothing is stored, a create with a different value succeeds, a create whose value equals the named sibling is refused, an absent sibling passes, a nested pair is judged by its dotted paths, an array element equal to $ownerId is refused while distinct elements pass, a replace that makes the two equal (or sets the property to the owner) is refused and leaves the stored document untouched, a replace that keeps them distinct succeeds, a transfer to (or a purchase by) the identity a $ownerId declaration names is refused and leaves the owner unchanged while a transfer to (or purchase by) another identity succeeds, and the replace, transfer and purchase dispatchers are exercised at protocol versions 13 and 14. The immutable, creation, transfer, nft and create-structure suites around the touched validators still pass.
  • cargo check -p wasm-dpp2 --target wasm32-unknown-unknown, cargo check -p wasm-dpp --target wasm32-unknown-unknown, clippy on dpp and drive-abci, cargo fmt --all.

Breaking Changes

Protocol version 14 only: contracts may carry distinctFrom; document create and replace refuse a write that violates it, and transfer and purchase refuse a change of owner that would (new consensus error 10419). Earlier protocol versions are unchanged. DocumentReplaceTransitionActionValidation::validate_structure now takes the owner id.

Follow-ups

  • Swift and Kotlin DataContractParser do not surface the keyword yet.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

An identifier property may declare `distinctFrom`: its value must differ
from a named property of the same document, or from the document's
`$ownerId`. A pure structure rule at protocol version 14: the parser
(meta-schema v3, `apply_distinct_from` 0) checks the target at contract
registration and update, and document create and replace structure
validation 1 refuse an equal pair with `DocumentPropertyNotDistinctError`
(10419) after the document's schema validation, reading the transition
alone. An absent named property passes. A changed `distinctFrom` is an
incompatible schema change on update. `DistinctFrom::violation` judges a
single value so that a typed array item can be judged by the same rule.

wasm-dpp2 exposes the declarations and the error code; the book and the
v14 changelog describe the keyword.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository: dashpay/platform/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ce587dc0-d936-487b-a2e5-a5303a6dc325

📥 Commits

Reviewing files that changed from the base of the PR and between 1d6497e and 456a934.

📒 Files selected for processing (47)
  • AGENTS.md
  • book/src/contributing/coding-conventions.md
  • book/src/data-model/documents.md
  • book/src/error-handling/error-codes.md
  • book/src/versioning/versioned-dispatch.md
  • packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/index/preallocation.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs
  • packages/rs-dpp/src/data_contract/document_type/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/property/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/v0/random_document_type.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs
  • packages/rs-dpp/src/data_contract/document_type/v2/mod.rs
  • packages/rs-dpp/src/errors/consensus/basic/basic_error.rs
  • packages/rs-dpp/src/errors/consensus/basic/document/document_property_not_distinct_error.rs
  • packages/rs-dpp/src/errors/consensus/basic/document/mod.rs
  • packages/rs-dpp/src/errors/consensus/codes.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_purchase_transition_action/advanced_structure_v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_replace_transition_action/advanced_structure_v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_replace_transition_action/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_transfer_transition_action/advanced_structure_v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_update_price_transition_action/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/distinct_from.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs
  • packages/rs-json-schema-compatibility-validator/src/rules/rule_set.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v5.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs
  • packages/rs-platform-version/src/version/v14.rs
  • packages/wasm-dpp/src/errors/consensus/consensus_error.rs
  • packages/wasm-dpp2/src/consensus_error.rs
  • packages/wasm-dpp2/src/data_contract/document_type_distinct_from.rs
  • packages/wasm-dpp2/src/data_contract/mod.rs
  • packages/wasm-dpp2/src/data_contract/model.rs
 ______________________________________________________________________________________________________________________________________________
< Eliminate effects between unrelated things. Design components that are self-contained, independent, and have a single, well-defined purpose. >
 ----------------------------------------------------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 22, 2026
@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Sep 22, 2026
@thepastaclaw

thepastaclaw commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

⚠️ DEGRADED — Queued for automated review — 34th in line, estimated start in ~32 h (commit 21705dc)
Estimated review time once started: ~2.0 h (two-phase automated review; median of recent runs).
The primary review models are currently out of quota; this review will run on stand-in models and be marked as degraded.

  • Request priority review — click to move this review to the front of the queue.

QuantumExplorer and others added 2 commits September 23, 2026 01:28
…ctFrom tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-09-22T23:01:49.698Z

QuantumExplorer and others added 7 commits September 23, 2026 03:22
Keep the shipped batch advanced-structure generation 0 byte-identical: the
replace validator gains `validate_structure_with_owner`, which only the
generation 1 batch caller uses, and `validate_structure` keeps its original
signature. Move the distinctFrom target check into the shared property
parser, gated on the same `apply_distinct_from` version that parses the
declarations. Name an object target as such instead of "no property".
Drop the unused Deserialize, TryFrom and Display on DistinctFrom. Document
that transfers and purchases are not judged, and extend the error code
catalog to 10419. Tests: the meta-schema, not the parser, refuses the
keyword on a non-identifier; refersTo and distinctFrom on one property;
the dpp and replace dispatchers on both sides of the protocol version 14
gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, precompute the declaring properties

Transfer and purchase structure validation move to generation 1 at
protocol version 14: the stored document's `distinctFrom: $ownerId`
properties are judged against the new owner the action already carries,
so a transfer to, or a purchase by, the identity such a property names is
refused with DocumentPropertyNotDistinctError like a create or replace.
The update-price dispatcher read the transfer slot instead of its own
(both were 0 everywhere, so nothing changes for released tables); it now
reads `document_update_price_transition_structure_validation`.

The parser records the declaring properties on the V2 document type
(`distinct_from_fields`, in schema order) and the write-time check reads
that list, so a type without declarations costs nothing per write.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Typed scalar arrays landed on v4.2-dev (#4922), so the declaration the
charters contract needs, on the `members` items, is covered: an identifier
typed array carries `distinctFrom` on its `items` and every element must
differ from the named value. The parser refuses the keyword on the array
itself and on elements of any other type, the meta-schema admits it on
identifier elements only, and the write-time check judges each element
with `DistinctFrom::violation`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…m parse

Typed array elements are their scalar property type since #4923.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Approved

QuantumExplorer and others added 3 commits September 23, 2026 05:46
…he shipped v0, and let shipped generations change in place when consensus cannot

The conventions rule becomes "shipped generations are frozen unless the
change cannot modify consensus": an in-place edit is allowed when the new
code is unreachable by construction, or output-identical, at every
protocol version that selects the module, with the reason at the edited
lines and an "In-place changes to shipped generations" section in the
PR description. Written into the coding conventions, the versioned
dispatch chapter and the CLAUDE.md summary.

Under that rule the transfer and purchase distinctFrom judgement moves
from a copied v1 into v0, with the table slots back at 0: below
protocol version 14 no parsed property carries the keyword and the dpp
gate is None, so the call returns an empty result there. A dispatcher
test runs both modules at 13 and 14.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…operties' into feat/distinct-from-identifier-properties
Replace structure v0 takes the writer's owner id and judges the
transition's distinctFrom declarations after the schema validation, in
place under the revised conventions rule: below protocol version 14 no
parsed property carries the keyword and the dpp gate is None, so the
call returns an empty result. The copied v1, the second trait method and
the table bump go away; both batch call sites pass identity.id.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit afa67e6 into v4.2-dev Sep 22, 2026
10 checks passed
@QuantumExplorer
QuantumExplorer deleted the feat/distinct-from-identifier-properties branch September 22, 2026 23:02
QuantumExplorer added a commit that referenced this pull request Sep 22, 2026
Keeps the distinctFrom changelog entry as item 26 and moves the
word-characters-only name rule to item 27.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-bots Waiting for the review bots to report on this head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants