feat(platform)!: encryptedFor envelope declaration on byte properties (PV14) - #4919
Conversation
|
Warning Review limit reachedNext included review available in 58 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 (39)
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-22T23:22:24.552Z |
… (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>
1dea2fe to
234e17b
Compare
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
encryptedMessageonjoinRequestin the moderation charters contract (#4898).This adds the
encryptedFordocument 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
secretdocument 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:Writing one: the sender takes its own identity key
senderKeyId, the recipient's identity keyrecipientKeyId, derives the ECDH shared key, draws a 16-byte IV and storesIV || 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:
encryptedMessageInvalidEncryptedPropertyShapeError(10420): not a multiple of 16minItems: 32first; with a lowerminItemsit is 10420 (IV alone, no block)Reading one, a wallet asks the contract instead of guessing:
In Rust the same comes from
document_type.encrypted_properties(). For a message the writer encrypts to themself,recipientis"$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 aretransient, or whose byte arraymaxItemsis below 32.What was done?
Schema and model (rs-dpp)
encryptedForon byte array properties that are not identifiers:recipient(the path of an identifier property of the same document type, or$ownerId),recipientKeyandsenderKey(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), withEncryptedForRecipient(OwnerorProperty(path)) andEncryptionScheme, which knows its block length and minimum ciphertext length.apply_encrypted_forslot in the document type schema versions (Nonebefore PV14, so older parses ignore the keyword exactly as they didrefersTo;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 declaresminimumat least 0 andmaximumat most 4294967295 (read from the schema, so the rule holds undersizedIntegerTypes: false), that none of the three istransient, and that the byte array's ownmaxItemscan hold the scheme's shortest ciphertext.DocumentTypeV0Getters::encrypted_properties()lists the declared properties of a document type by dotted path, for the Rust SDK.encryptedForon 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_shapeschecks 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 newvalidate_encrypted_property_shapesmethod slot (Nonebefore PV14,Some(0)in the v6 table).InvalidEncryptedPropertyShapeError(10420, the next code in the basic document band afterdistinctFrom's 10419), which names the property, the scheme and the lengths.Clients and docs
contract.documentTypeEncryptedProperties(name)and thedocumentEncryptedPropertiesmap expose the declarations in TypeScript (DocumentPropertyEncryption), andDocumentEncryptionErrorCode.InvalidEncryptedPropertyShapemirrors the code. wasm-dpp (legacy) maps the new error. js-evo-sdk README section.ecdh-secp256k1-aes256-cbcbyte 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 ownminItems/maxItemsrun before the shape check; the error code table row; changelog item 27 in the v14 doc comment.Scheme layout, confirmed against
platform-encryptionand 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
platform-encryption).keyRequirementssession nor theidentityPropertysession has landed on v4.2-dev, so there is no fixture showingencryptedFornext torefersTo: identityPublicKeywithkeyIdPropertyon the recipient. Where a schema declares those, they do the key existence and purpose checks;encryptedForneither duplicates nor requires them. Add that fixture once either lands.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 tovalidate_encrypted_property_shapesafter thedistinctFromcall 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 refuseencryptedFor, their parser ignores it (apply_encrypted_forisNone, so no parsed property carries a declaration), and the dpp method's own gatevalidate_encrypted_property_shapesisNonethere, so the call returns an empty result.should_not_check_the_ciphertext_shape_on_replace_before_protocol_version_14runs 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 --libfor the newencrypted_formodule, thetry_from_schematests (parse, including$ownerIdand 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 whosemaxItemsis below 32; accept the key ids undersizedIntegerTypes: false; refuse below PV14 under full validation, ignore below PV14 without it, accept at 14 both ways), thevalidate_updatetests (added, removed or changed declaration is incompatible, unchanged is fine), the platform serialization round trip with and without the keyword, and theBasicErrordiscriminant pin.cargo test -p drive-abcifor the newencrypted_forbatch document tests: a create with a 48-byte ciphertext succeeds and is stored, 47 bytes and 16 bytes are refused withInvalidEncryptedPropertyShapeErrornaming the property and lengths and store nothing, a nestedmeta.blobdeclaration 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.DocumentPropertyEncryption.spec.ts(run against a rebuiltyarn 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:
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 ·
f03c0ce/skip-botsproceeds without the ones not yet reported/self-reviewedonce the bots are donejs-wasm-sdk(packages/js-evo-sdk/README.md) — shumkovdpp— you own itrs-drive-abci— you own itWhen every box is checked the
PR Hygienecheck passes and this can merge.