Skip to content

feat(platform)!: encryptedFor envelope declaration on byte properties (PV14) - #4919

Merged
QuantumExplorer merged 1 commit into
v4.2-devfrom
claude/optimistic-lehmann-926fd1
Sep 22, 2026
Merged

QuantumExplorer merged 1 commit into
v4.2-devfrom
claude/optimistic-lehmann-926fd1

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

A byte array property that holds ciphertext said nothing about how the ciphertext was made, so every wallet had to learn the recipe (whose keys, which key ids, which scheme, where the IV sits) from documentation or a side channel. The first user is encryptedMessage on joinRequest in the moderation charters contract (#4898).

This adds the encryptedFor document schema keyword at protocol version 14. A byte array property declares for which recipient, with which recipient key and sender key, and under which scheme its bytes were produced, and wallets and SDKs read the recipe from the contract.

Example

A secret document type whose message only the named recipient can read. The three companion properties carry the recipient identity and the two key ids; the byte array declares how its bytes were made:

"secret": {
  "type": "object",
  "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 },
    "encryptedMessage": {
      "type": "array", "byteArray": true, "minItems": 32, "maxItems": 1040,
      "encryptedFor": {
        "recipient": "recipientId",
        "recipientKey": "recipientKeyId",
        "senderKey": "senderKeyId",
        "scheme": "ecdh-secp256k1-aes256-cbc"
      },
      "position": 3
    }
  },
  "required": ["recipientId", "recipientKeyId", "senderKeyId", "encryptedMessage"],
  "additionalProperties": false
}

Writing one: the sender takes its own identity key senderKeyId, the recipient's identity key recipientKeyId, derives the ECDH shared key, draws a 16-byte IV and stores IV || AES-256-CBC(plaintext):

{
  "recipientId": "8eTDkBhpQjHeqgbVeriwLeZr1tCa6yBGw76SckvD1cwc",
  "recipientKeyId": 3,
  "senderKeyId": 1,
  "encryptedMessage": "<48 bytes: 16-byte IV followed by two AES blocks>"
}

What consensus does with it, at create and replace:

encryptedMessage Result
48 bytes accepted (IV plus two whole blocks)
47 bytes refused, InvalidEncryptedPropertyShapeError (10420): not a multiple of 16
16 bytes refused by the schema's minItems: 32 first; with a lower minItems it is 10420 (IV alone, no block)

Reading one, a wallet asks the contract instead of guessing:

const contract = await sdk.contracts.fetch(contractId);
contract.documentTypeEncryptedProperties('secret');
// [{ path: 'encryptedMessage', recipient: 'recipientId',
//    recipientKey: 'recipientKeyId', senderKey: 'senderKeyId',
//    scheme: 'ecdh-secp256k1-aes256-cbc' }]

In Rust the same comes from document_type.encrypted_properties(). For a message the writer encrypts to themself, recipient is "$ownerId" and no recipient property is needed. Registration refuses a declaration whose recipient is not an identifier property, whose key properties are not integers bounded to a key id, whose named properties are transient, or whose byte array maxItems is below 32.

What was done?

Schema and model (rs-dpp)

  • Meta-schema v3 (edited in place, unreleased) admits encryptedFor on byte array properties that are not identifiers: recipient (the path of an identifier property of the same document type, or $ownerId), recipientKey and senderKey (paths of integer properties carrying key ids), scheme (closed enum, one value: ecdh-secp256k1-aes256-cbc). All four keys required, additionalProperties: false. The description spells out the byte layout.
  • DocumentProperty::encrypted_for: Option<EncryptedFor> (appended, skip_serializing_if), with EncryptedForRecipient (Owner or Property(path)) and EncryptionScheme, which knows its block length and minimum ciphertext length.
  • The parser reads the keyword per property under a new apply_encrypted_for slot in the document type schema versions (None before PV14, so older parses ignore the keyword exactly as they did refersTo; Some(0) in the v6 contract table). Once every property is parsed, generation 3 checks that the recipient names an identifier property, that both key paths name integer properties whose schema declares minimum at least 0 and maximum at most 4294967295 (read from the schema, so the rule holds under sizedIntegerTypes: false), that none of the three is transient, and that the byte array's own maxItems can hold the scheme's shortest ciphertext.
  • DocumentTypeV0Getters::encrypted_properties() lists the declared properties of a document type by dotted path, for the Rust SDK.
  • A changed, added or removed encryptedFor on a contract update is an incompatible schema change (the existing differ catches it; a test pins it).

Consensus (rs-drive-abci)

  • DocumentTypeBasicMethods::validate_encrypted_property_shapes checks every declared property a document supplies: the length must be at least the IV plus one block and a multiple of the block (32 and 16 for AES-CBC). No state read. Versioned on a new validate_encrypted_property_shapes method slot (None before PV14, Some(0) in the v6 table).
  • Document create structure validation 1 (PV14, unreleased, extended in place) and replace structure validation 0 (shipped, extended in place; see the section below) call it after the JSON schema validation of the properties, and refuse with the new basic error InvalidEncryptedPropertyShapeError (10420, the next code in the basic document band after distinctFrom's 10419), which names the property, the scheme and the lengths.

Clients and docs

  • wasm-dpp2: contract.documentTypeEncryptedProperties(name) and the documentEncryptedProperties map expose the declarations in TypeScript (DocumentPropertyEncryption), and DocumentEncryptionErrorCode.InvalidEncryptedPropertyShape mirrors the code. wasm-dpp (legacy) maps the new error. js-evo-sdk README section.
  • Book: a section in the documents chapter with the ecdh-secp256k1-aes256-cbc byte layout (libsecp256k1 ECDH shared key, 16-byte IV prefix, AES-256-CBC with PKCS7) and what consensus checks and cannot check, including that the schema's own minItems/maxItems run before the shape check; the error code table row; changelog item 27 in the v14 doc comment.

Scheme layout, confirmed against platform-encryption and the dashpay contact request: shared key = libsecp256k1 ECDH of the sender's private key and the recipient's public key (SHA256(parity || x)), value = random 16-byte IV followed by AES-256-CBC with PKCS7 padding under that key and IV. So a ciphertext is at least 32 bytes and a multiple of 16.

Out of scope, follow-ups

  • Encrypt and decrypt helpers keyed off the declaration in js-evo-sdk and wasm-sdk (the primitives are in platform-encryption).
  • Swift and Kotlin contract parsers do not read the keyword yet.
  • Neither the keyRequirements session nor the identityProperty session has landed on v4.2-dev, so there is no fixture showing encryptedFor next to refersTo: identityPublicKey with keyIdProperty on the recipient. Where a schema declares those, they do the key existence and purpose checks; encryptedFor neither duplicates nor requires them. Add that fixture once either lands.
  • The moderation charters contract (feat(platform)!: moderation charters system data contract #4898) is a draft and is not edited here.

In-place changes to shipped generations

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

  • document_replace_transition_action/advanced_structure_v0, selected by every protocol version (its table slot stays 0). It gained one call to validate_encrypted_property_shapes after the distinctFrom call feat(platform)!: distinctFrom on identifier properties (PV14) #4917 added the same way. It cannot modify consensus below protocol version 14: the meta-schemas of those versions refuse encryptedFor, their parser ignores it (apply_encrypted_for is None, so no parsed property carries a declaration), and the dpp method's own gate validate_encrypted_property_shapes is None there, so the call returns an empty result. should_not_check_the_ciphertext_shape_on_replace_before_protocol_version_14 runs the module at 13 and 14 through its dispatcher against a contract that does carry the declaration.

How Has This Been Tested?

  • cargo test -p dpp --lib for the new encrypted_for module, the try_from_schema tests (parse, including $ownerId and a nested recipient; refuse on a string and on an identifier property, with a missing key, an unknown key, an unknown scheme, a non-string path, a system recipient or key path; refuse a recipient that is not an identifier and a key path that is missing, a string, an unbounded or max-only integer or an identifier; refuse a transient recipient or key and a byte array whose maxItems is below 32; accept the key ids under sizedIntegerTypes: false; refuse below PV14 under full validation, ignore below PV14 without it, accept at 14 both ways), the validate_update tests (added, removed or changed declaration is incompatible, unchanged is fine), the platform serialization round trip with and without the keyword, and the BasicError discriminant pin.
  • cargo test -p drive-abci for the new encrypted_for batch document tests: a create with a 48-byte ciphertext succeeds and is stored, 47 bytes and 16 bytes are refused with InvalidEncryptedPropertyShapeError naming the property and lengths and store nothing, a nested meta.blob declaration is checked through its dotted path while an omitted optional declared property is not, a replace that shrinks the ciphertext to 16 bytes is refused and leaves the stored document untouched while one of the right shape goes through, and the replace structure dispatcher is exercised at protocol versions 13 and 14. The rest of the batch document suite (creation, replacement, immutable, distinct_from among them) still passes.
  • cargo check -p wasm-dpp2 --target wasm32-unknown-unknown, clippy on dpp and drive-abci, cargo fmt --all.
  • wasm-dpp2 unit spec DocumentPropertyEncryption.spec.ts (run against a rebuilt yarn workspace @dashevo/wasm-dpp2 build).

Breaking Changes

Protocol version 14 (unreleased): a new document schema keyword, a new basic consensus error (10420), a new document type schema slot and method slot, and the shape check added to the create and replace structure validations. Contracts and documents without the keyword are unaffected; contract serialization is unchanged for contracts without it.

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 · f03c0ce

  • 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 — this one is beyond the limit; it waits until one merges
  • Build running
  • Approvals
    • files with no dedicated owner — you own it
    • js-wasm-sdk (packages/js-evo-sdk/README.md) — shumkov
    • dpp — you own it
    • rs-drive-abci — you own it

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

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 58 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: 11a1d04c-7709-42b2-a99f-6fabe3299979

📥 Commits

Reviewing files that changed from the base of the PR and between afa67e6 and f03c0ce.

📒 Files selected for processing (39)
  • book/src/data-model/documents.md
  • book/src/error-handling/error-codes.md
  • packages/js-evo-sdk/README.md
  • packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/accessors/v0/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/class_methods/try_from_schema/v3/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/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/property/encrypted_for.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/v1/serialization/mod.rs
  • packages/rs-dpp/src/errors/consensus/basic/basic_error.rs
  • packages/rs-dpp/src/errors/consensus/basic/document/invalid_encrypted_property_shape_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_replace_transition_action/advanced_structure_v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/encrypted_for.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_encryption.rs
  • packages/wasm-dpp2/src/data_contract/mod.rs
  • packages/wasm-dpp2/src/data_contract/model.rs
  • packages/wasm-dpp2/tests/unit/DocumentPropertyEncryption.spec.ts

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
@thepastaclaw

thepastaclaw commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

⚠️ DEGRADED — Queued for automated review — 29th in line, estimated start in ~28 h (commit f03c0ce)
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.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Sep 22, 2026
@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:22:24.552Z

@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

… (PV14)

A byte array property may declare how its ciphertext was produced
(recipient, recipient and sender key ids, scheme), so wallets and SDKs
read the recipe from the contract instead of a side channel. Meta-schema
v3 admits it on non-identifier byte arrays; parser generation 3 checks
the named properties at registration (identifier recipient, key ids
bounded in the schema, none transient, maxItems above the scheme's
shortest ciphertext); document create structure validation 1 and the
shipped replace structure validation 0, extended in place and inert
before this version, check the ciphertext shape (IV plus whole blocks)
through the versioned validate_encrypted_property_shapes and refuse
with InvalidEncryptedPropertyShapeError (10420). wasm-dpp2 exposes the
declarations and the error code; the book states the scheme layout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer force-pushed the claude/optimistic-lehmann-926fd1 branch 2 times, most recently from 1dea2fe to 234e17b Compare September 22, 2026 23:22

@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

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