Skip to content

fix(platform)!: drop transient values on replace and refuse declarations that read them (PV14) - #4950

Merged
QuantumExplorer merged 2 commits into
v4.2-devfrom
fix/transient-property-gaps
Sep 23, 2026
Merged

QuantumExplorer merged 2 commits into
v4.2-devfrom
fix/transient-property-gaps

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

A transient property is validated on the transition and not stored. An audit of how transient is handled after #4943 and #4948 found seven gaps:

  1. Drive drops transient values only on a create. A replace stores whatever it carries, so a replaced document keeps values its create had dropped.
  2. An index may read a transient property. Every document then sits in the index's null branch: a query by the value finds nothing, and a unique index enforces nothing.
  3. A propertyAgreement may name a transient property on its referenced side. No stored document carries that value, so a referring type whose side is required can never be written.
  4. A refersTo lookup may go through an index keyed by a transient property, and can then never find a document.
  5. A key reference may store its key id while the identity it belongs to is transient, leaving a key id that names no key.
  6. Changing the transient list on a contract update fails as an unsupported keyword in the schema compatibility check. That is an internal error: the transition is dropped unpaid and the client gets no consensus error.
  7. transient entries are never validated. A dotted path such as "meta.tag" marks the property transient in the parsed type, but Drive drops values by top-level name, so the value is stored anyway.

All of this is protocol version 14, unreleased. Before it ships, each fix is an in-place edit of an unreleased generation. Afterwards, the parser rules would need a gate so stored contracts stay readable.

Census (2026-09-23). I pulled the schema of every contract from the platform explorer: 52 on mainnet and 3,890 on testnet. The ones without a schema are token-only contracts with an empty {}. transient appears only on DPNS-shaped domain types: DPNS on mainnet, DPNS and five copies on testnet. All of them list preorderSalt only, a top-level property that no index reads, on an immutable type. No existing contract is refused by the new rules, and since no mutable type declares transient, the replace change alters no stored document.

What was done?

Every rule reads a path through is_transient (the path and every enclosing object's). It is now pub so drive-abci can use it.

1. A replace drops transient values (document_from_replace_transition_action 1, rs-drive). The conversion that builds the stored document from a replace action drops the document type's transient values by top-level name. It goes through the same drop_transient_values helper the create action now uses, so the two cannot drift apart. Validation still sees the transition's values, as it does for a create.

Before, with a mutable type that declares "transient": ["code"]:

create  { code: "x", body: "a" }  ->  stored { body: "a" }
replace { code: "y", body: "b" }  ->  stored { code: "y", body: "b" }

After:

replace { code: "y", body: "b" }  ->  stored { body: "b" }

2. No index reads a transient property (parser generation 3, full validation only).

"transient": ["code"],
"indices": [{ "name": "byCode", "properties": [{ "code": "asc" }], "unique": true }]

Before: registered, and the unique index enforced nothing. After: refused with index "byCode" of document type "note" reads "code", which is transient or inside a transient object: its value is never stored, so the index would never hold it. An index over meta.tag with "transient": ["meta"] is refused the same way.

3. A propertyAgreement names no transient property on its referenced side (data_contract_reference_validation 0, drive-abci). Take a note type with "transient": ["topic"], and a message.noteId declaring "propertyAgreement": { "topic": "topic" }.

  • Before: it registered. Afterwards every write of a message with a topic was refused with ReferencedDocumentPropertyMismatchError.
  • After: registration fails with ReferencedDocumentPropertyAgreementInvalidError: the referenced property is transient or inside a transient object: no stored document carries its value, so none could be agreed with.
  • A transient referring side still registers. It is judged on the transition, a write gate like the writer's $ownerId.

4. A lookup's index keys documents by no transient property (DocumentReferenceLookup::referenced_side_error). Registration already refuses such an index (2), but a type parsed without full validation still reaches this check. The refusal reads index "bySubmittedCharter" of "joinRequest" keys documents by "submittedCharterId", which is transient or inside a transient object: its value is never stored, so the lookup could never find a document.

5. A stored key id never pairs with a transient identity (data_contract_reference_validation 0), in both forms:

"transient": ["toUserId"],
"recipientKeyId": { "type": "integer", "minimum": 0, "maximum": 4294967295,
                    "refersTo": { "type": "identityPublicKey", "identityProperty": "toUserId" } }
  • Before: registered. After: refused with ReferencedKeyIdPropertyInvalidError: the key id is stored but the identity property toUserId is transient or inside a transient object: a reader could not tell whose key it is.
  • The keyIdProperty form (the reference on a transient identity property, pointing at a stored key id) is refused the same way.
  • With both sides transient nothing is stored, so the pair registers.

6. A changed transient list is an incompatible schema change (validate_schema_compatibility 1). The list gets the frozen rule refersTo has, as ownerRefersTo and creatorRefersTo already do. The parse reads it as a set, so it is sorted and deduplicated before the diff: reordering or repeating a name is not a change. Adding "transient": ["name"] to an existing type:

  • Before: InternalError("... schema keyword 'transient' at path '/transient' is not supported"), dropped unpaid.
  • After: IncompatibleDocumentTypeSchemaError for add /transient, a paid consensus error.

Generation 0 (protocol version 13) keeps the internal error, pinned by a test.

7. Every transient entry names a top-level property (parser generation 3, full validation only). Before: "transient": ["meta.tag"], ["ghost"] and ["$ownerId"] all registered. After: each is refused with document type "note" lists "meta.tag" as transient, but it is not a top-level property of the document type: transient values are dropped by top-level name, so list the object around a nested property.

Known limitation. Rules 2 and 7 run under full validation, and an update re-parses the whole contract that way. Neither the transient list nor an index can change on update, so a contract registered before protocol version 14 with one of these shapes could no longer be updated. The census found none on mainnet or testnet. This is the same trade-off the protocol 14 word-characters-only name rule made.

Docs. Added a "Transient Properties" section to book/src/data-model/documents.md, a description on the meta-schema v3 transient keyword (description text only), and item 35 of the protocol version 14 change list in v14.rs.

In-place changes to shipped generations

  • data_contract_reference_validation 0 (drive-abci; item 3 and item 5). Every table selects it, but only contract create and update state validation 1 call it, and only protocol version 14 selects those. Earlier versions never run it. Its module comment already records the same argument for the reference expressions added in place.
  • DocumentReferenceLookup::referenced_side_error (dpp, unversioned; item 4). A lookup only parses at protocol version 14, so no earlier version reaches it.
  • The create action's conversion to a document (rs-drive, every version). A pure refactor with identical output: its two inline data.retain(...) strips now call the shared drop_transient_values, which runs the same retain.

Everything else is edited in generations that only protocol version 14 selects: document_from_replace_transition_action 1 (DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4, only DRIVE_VERSION_V9), parser generation 3 (CONTRACT_VERSIONS_V6) and validate_schema_compatibility 1.

How Has This Been Tested?

  • dpp, new tests:
    • transient_tests.rs (generation 3): top-level entries and objects are accepted; a bad entry and an index over a transient property are refused under full validation, still parse without it, and still register at protocol version 13.
    • should_refuse_a_lookup_into_an_index_reading_a_transient_property.
    • should_report_every_transient_list_change_as_incompatible (add, remove, append, replace), should_accept_a_reordered_or_repeated_transient_list, and should_hard_error_on_a_transient_diff_at_protocol_version_13 pinning protocol version 13.
  • rs-drive: should_drop_transient_values_on_replace_conversion_from_protocol_version_14. The DPNS preorderSalt is dropped by a replace at protocol version 14 and kept at 13; a create drops it at both.
  • drive-abci end to end: should_store_a_replaced_document_without_its_transient_values creates and replaces a document of a mutable type through process_raw_state_transitions and reads the stored document back. should_store_the_transient_values_of_a_replace_at_protocol_version_13 pins the old behaviour.
  • drive-abci, through process_raw_state_transitions with new fixtures:
    • should_reject_agreement_on_a_transient_referenced_property (the property itself, and one inside a transient object) and should_register_agreement_on_a_transient_referring_property.
    • should_reject_a_stored_key_id_paired_with_a_transient_identity (both forms, plus an identity inside a transient object) and should_register_a_key_reference_whose_key_id_and_identity_are_both_transient (both forms).
    • should_refuse_an_update_changing_the_transient_list_as_an_incompatible_schema (contract update state validation).
  • Every new refusal is shown to catch its gap: with each fix neutralized in place, all eight new refuse/drop tests fail, while the positive tests and the protocol version 13 pins still pass.
  • cargo test -p dpp --all-features --lib: 4775 passed.
  • cargo test -p drive -p drive-abci --lib -- transient replace_conversion data_contract_create data_contract_update replacement dpns reference_validation: drive 54 passed, drive-abci 256 passed.
  • cargo clippy -p dpp -p drive -p drive-abci --all-features --all-targets -- -D warnings: clean. cargo fmt --all -- --check: clean.

Breaking Changes

Consensus-breaking at protocol version 14 only, which is unreleased. Nodes running it (devnets) must run the same binary:

  • a replace no longer stores transient values;
  • registrations with the shapes above are refused;
  • a transient change on update becomes a paid consensus error instead of an internal error.

The census found no mainnet or testnet contract affected. No released protocol version changes.

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

PR Hygiene · d17a1ae

  • Bots — coderabbitai not yet · thepastaclaw not yet — /skip-bots proceeds without the ones not yet reported
  • Self-review — post /self-reviewed once the bots are done
  • Within your 5 open PRs
  • Build running
  • Approvals — you own every area touched; none needed

When every box is checked the PR Hygiene check passes and this can merge.

…ons that read them (PV14)

A transient property is judged on the transition and never stored, but:
a replace stored whatever it carried (only a create dropped the values);
an index, a lookup's index, a propertyAgreement's referenced side and a
key reference's identity could all read a transient value no stored
document holds; transient entries naming nested paths, system or unknown
properties were accepted and not dropped; and changing the transient list
on update failed as an unsupported keyword, an internal error.

- document_from_replace_transition_action 1 drops transient values by
  top-level name, as the create action does
- parser generation 3 (full validation) refuses a transient entry that is
  not a top-level property and an index reading a transient property
- referenced_side_error refuses a lookup index keyed by one
- data_contract_reference_validation 0 refuses a transient referenced
  agreement property and a stored key id paired with a transient identity
- validate_schema_compatibility 1 freezes the transient list

Every generation touched is only selected by, or only reached from,
protocol version 14, which is unreleased. A census of all mainnet and
testnet contracts found transient only on immutable DPNS-shaped domain
types, which none of the new rules refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

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

Review profile: CHILL

Plan: Advanced

Run ID: 78921823-9619-4999-8aa4-0b04c32fde47

📥 Commits

Reviewing files that changed from the base of the PR and between 3044153 and d17a1ae.

📒 Files selected for processing (26)
  • book/src/data-model/documents.md
  • packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/immutable_tests.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/transient_tests.rs
  • packages/rs-dpp/src/data_contract/document_type/property/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/property/reference_lookup.rs
  • packages/rs-dpp/src/data_contract/document_type/schema/validate_schema_compatibility/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs
  • packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-agreement-transient-referenced-object.json
  • packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-agreement-transient-referenced.json
  • packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-agreement-transient-referring.json
  • packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-identity-key-registration-transient-identity.json
  • packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-identity-key-registration-transient-pair.json
  • packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-identity-property-key-transient-identity.json
  • packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-identity-property-key-transient-object.json
  • packages/rs-drive-abci/tests/supporting_files/contract/reference-validation/reference-validation-contract-identity-property-key-transient-pair.json
  • packages/rs-drive-abci/tests/supporting_files/contract/transient/transient-note-contract.json
  • packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/v0/mod.rs
  • packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_replace_transition_action/v1/mod.rs
  • packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs
  • packages/rs-drive/src/state_transition_action/batch/tests.rs
  • packages/rs-platform-version/src/version/v14.rs

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 the waiting-bots Waiting for the review bots to report on this head label Sep 23, 2026
@thepastaclaw

thepastaclaw commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

🕓 Queued for automated review — 8th in line, estimated start in ~50 min (commit d17a1ae)
Estimated review time once started: ~15 min (two-phase automated review; median of recent runs).

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

@github-actions

github-actions Bot commented Sep 23, 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-23T15:56:14.211Z

@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 23, 2026
- the frozen transient list is compared as a set: sorted and deduplicated
  before the schema diff, so a reordering is no change
- one drop_transient_values helper for the create action (pure refactor)
  and the replace conversion, which now drops before moving the data
  instead of cloning the transient set
- end-to-end replace test through process_raw_state_transitions at
  protocol versions 14 and 13
- drive-abci fixtures for a transient object around an agreement's
  referenced property or a key reference's identity, and the
  keyIdProperty pair
- inertness comments at the in-place edits of
  data_contract_reference_validation 0, a should_ test name, shared v3
  test helpers, a hint only for dotted transient entries, and the
  update limitation documented at the parser check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit 411590b into v4.2-dev Sep 23, 2026
36 of 37 checks passed
@QuantumExplorer
QuantumExplorer deleted the fix/transient-property-gaps branch September 23, 2026 16:48
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