Skip to content

fix(dpp): refuse encryptedFor paths inside a transient object (PV14) - #4948

Merged
QuantumExplorer merged 1 commit into
v4.2-devfrom
fix/encrypted-for-transient-object
Sep 23, 2026
Merged

QuantumExplorer merged 1 commit into
v4.2-devfrom
fix/encrypted-for-transient-object

Conversation

@QuantumExplorer

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

encryptedFor (#4919, protocol version 14, unreleased) refuses a transient recipient, recipient key or sender key, because a transient value is stripped before storage and the stored ciphertext would lose its recipe. The check compared the named path against transient_fields as declared, so a path inside a transient object got through: transient: ["meta"] with recipient: "meta.recipientId" passed, and Drive then drops the whole meta object from the stored document (data.retain(|key, _| !transient_fields.contains(key)) in the create transition action). A reader of the stored document could not tell whom the bytes are for or which key decrypts them, which is what the rule exists to guarantee.

It has to be fixed before 4.2 ships: the check runs on every parse (try_from_schema generation 3, called unconditionally), so tightening it once protocol version 14 is live would make stored contracts unparseable and would need a version gate.

What was done?

  • validate_encrypted_for_declarations checks the path and every dotted prefix of it against the transient set, for the recipient and for both key paths. The message now says "is transient or inside a transient object".
  • The prefix check is one pub(crate) fn is_transient(document_type: DocumentTypeRef, path: &str) -> bool in document_type/property/mod.rs. The same fix for refersTo lookup key sources (#4943) and listElement references (#4940) each carry an identical private copy; neither has merged, so whichever lands after this one swaps its copy for use super::is_transient; (same signature, no call site changes).
  • The encryptedFor description in meta-schema v3 now states the rule (it did not mention transient at all), and the book's Encrypted Properties section adds "or sits inside a transient object".

Only DPNS declares transient among system contracts and test fixtures, and it has no encryptedFor; no contract in the repository declares encryptedFor.

Before / after

A document type whose recipient and key ids live in a transient object:

{
  "type": "object",
  "properties": {
    "meta": {
      "type": "object",
      "position": 0,
      "properties": {
        "recipientId": {
          "type": "array", "byteArray": true, "minItems": 32, "maxItems": 32,
          "contentMediaType": "application/x.dash.dpp.identifier", "position": 0
        },
        "recipientKeyId": { "type": "integer", "minimum": 0, "maximum": 4294967295, "position": 1 },
        "senderKeyId": { "type": "integer", "minimum": 0, "maximum": 4294967295, "position": 2 }
      },
      "required": ["recipientId", "recipientKeyId", "senderKeyId"],
      "additionalProperties": false
    },
    "encryptedMessage": {
      "type": "array", "byteArray": true, "minItems": 32, "maxItems": 1040, "position": 1,
      "encryptedFor": {
        "recipient": "meta.recipientId",
        "recipientKey": "meta.recipientKeyId",
        "senderKey": "meta.senderKeyId",
        "scheme": "ecdh-secp256k1-aes256-cbc"
      }
    }
  },
  "required": ["meta", "encryptedMessage"],
  "transient": ["meta"],
  "additionalProperties": false
}

Before: the contract registers. Every stored document keeps encryptedMessage and loses meta, so no reader can tell whom the bytes are for or which keys to use.

After: the contract is refused with InvalidContractStructure (10231):

document type "message" property "encryptedMessage" encryptedFor recipient "meta.recipientId" is transient or inside a transient object: a transient value is never stored, so a reader could not tell whom the bytes are for

(and the same for recipientKey / senderKey when the recipient is outside the object). The same schema without "transient": ["meta"] still registers, as does a top-level transient entry that only shares a text prefix with the path (meta does not cover metadata.recipientId).

In-place changes to shipped generations

None. The edited check belongs to try_from_schema generation 3, selected only by CONTRACT_VERSIONS_V6, which only protocol version 14 (unreleased) uses. Meta-schema v3 is likewise protocol version 14 only, and the change there is a description.

How Has This Been Tested?

New rs-dpp tests in try_from_schema/mod.rs, next to should_reject_encrypted_for_naming_a_transient_recipient_or_key:

  • should_reject_encrypted_for_naming_a_recipient_or_key_inside_a_transient_object: recipient, recipientKey and senderKey each pointed into a required object whose leaves are required; the schema parses under full validation without transient, and is refused with the new message on both the validating and the non-validating parse once the object is transient.
  • should_find_a_path_transient_through_itself_or_an_enclosing_object_only: the helper on a top-level transient property, the transient object itself, one and two levels inside it, and a look-alike name (metadata.authorId with meta transient) that must not match.

Run locally:

cargo test -p dpp --all-features --lib -- encrypted_for transient
cargo clippy -p dpp --all-features --tests
cargo fmt --all

All 389 tests matching try_from_schema, encrypted_for, transient or meta_schema pass; clippy is clean. With the old contains check restored, the new rejection test fails and the example schema above registers under full validation.

Breaking Changes

None for any released protocol version. At protocol version 14 (unreleased), a contract declaring encryptedFor with a recipient or key path inside a transient object is now refused.

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

validate_encrypted_for_declarations compared the recipient, recipientKey
and senderKey paths with transient_fields as declared, so a path inside a
transient object (transient: ["meta"], recipient: "meta.recipientId")
passed while Drive strips the whole object before storage. The check now
covers the path and every dotted prefix of it.

The prefix check is one pub(crate) is_transient(DocumentTypeRef, path) in
document_type/property/mod.rs, the same signature as the private copies
in the open lookup-source and listElement PRs, which can switch to it.

Generation 3 of try_from_schema and meta-schema v3 are selected only by
protocol version 14 (unreleased), so this is an in-place edit.

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 20 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: 7c4abbc7-cff1-4979-b589-c7d936943e03

📥 Commits

Reviewing files that changed from the base of the PR and between 85328a5 and 645d807.

📒 Files selected for processing (4)
  • 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/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/property/mod.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.

@QuantumExplorer
QuantumExplorer merged commit c114ce4 into v4.2-dev Sep 23, 2026
9 checks passed
@QuantumExplorer
QuantumExplorer deleted the fix/encrypted-for-transient-object branch September 23, 2026 14:00
@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 23, 2026
@github-actions

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-23T14:10:54.392Z

QuantumExplorer added a commit that referenced this pull request Sep 23, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant