Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
fd216b7
feat(sdk): refuse a private document type too small for its payload
QuantumExplorer Nov 7, 2026
63e3159
fix(sdk): check only the private types a contract update adds or chan…
QuantumExplorer Nov 7, 2026
839bfbe
feat(sdk): seal and open private document entries in the standard env…
QuantumExplorer Nov 7, 2026
e3e7707
fix(sdk): check private entry sizes on native wallet contract updates…
QuantumExplorer Nov 7, 2026
3d4d67d
Merge branch 'feat/private-document-minimum-entry-size' into feat/pri…
QuantumExplorer Nov 7, 2026
d130880
test(dpp): count a private type's integers at the width the contract …
QuantumExplorer Nov 7, 2026
450ffbd
Merge branch 'feat/private-document-minimum-entry-size' into feat/pri…
QuantumExplorer Nov 7, 2026
49473c9
fix(dpp): encode a private document payload whatever the version of t…
QuantumExplorer Nov 7, 2026
b0c97cf
fix(sdk): constant-time ECDH, readable old and empty-object payloads,…
QuantumExplorer Nov 7, 2026
fad9c53
fix(dpp): refuse a private payload whose objects hold bytes their mem…
QuantumExplorer Nov 7, 2026
e13eb1b
test(dpp): isolate a private payload's unread object bytes from the t…
QuantumExplorer Nov 7, 2026
94a400c
Merge remote-tracking branch 'origin/v5.1-dev' into feat/private-docu…
QuantumExplorer Nov 7, 2026
bcc9b6a
fix(sdk): build the envelope without getrandom, and check decoded val…
QuantumExplorer Nov 7, 2026
488066d
feat(sdk): verify a signed entry without a recipient key, and prepare…
QuantumExplorer Nov 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion book/src/contract-keywords.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ Every key a contract can write, grouped by where it goes. **Since** is the proto
| `rangeCountable`, `rangeSummable`, `rangeAverageable` | boolean | Provable counts, sums or averages over ranges of document ids. | 12 | [Document type range keys](contract-keywords/aggregates.md#document-type-rangecountable-rangesummable-rangeaverageable) |
| `indexOnly` | boolean | Documents are never stored whole: the index entries are the rows. | 14 | [indexOnly](contract-keywords/index-only.md#indexonly) Β· [internals](drive/index-only-document-types.md) |
| `entryPayload` | array of 1 to 16 names | On an index-only type, properties carried in each entry's value instead of a key. | 14 | [entryPayload](contract-keywords/index-only.md#entrypayload) |
| `private` | object: `entrySize`, 1 to 4096 | A private type: its documents are encrypted entries of `entrySize` bytes, which the chain never reads. Its `properties` describe the plaintext. It declares only `type`, `properties`, `required`, `additionalProperties`, `minProperties`, `maxProperties`, `dependentRequired`, `$comment`, `description`, the two bounded key keys, and `documentsMutable`, `canBeDeleted` and `documentsKeepHistory` as `false`, each at most once; neither its properties nor `required` nor `dependentRequired` name a system field, no property at any depth declares `refersTo`, `distinctFrom`, `generatedFrom`, `encryptedFor` or `requiredSince`, and the contract's elected moderators may not moderate it. No reference, `countOf` or `sumOf` may name it, an update cannot change it, and a Batch refuses every document transition on it (`DocumentTypePrivateError`, 10425). Entries are appended only by `PrivateDocumentCreateFromShieldedPool` (type 29), from 1 to 16 distinct entries of exactly `entrySize` bytes, paid from the credit shielded pool. | 15 | |
| `private` | object: `entrySize`, 1 to 4096 | A private type: its documents are encrypted entries of `entrySize` bytes, which the chain never reads. Its `properties` describe the plaintext. It declares only `type`, `properties`, `required`, `additionalProperties`, `minProperties`, `maxProperties`, `dependentRequired`, `$comment`, `description`, the two bounded key keys, and `documentsMutable`, `canBeDeleted` and `documentsKeepHistory` as `false`, each at most once; neither its properties nor `required` nor `dependentRequired` name a system field, no property at any depth declares `refersTo`, `distinctFrom`, `generatedFrom`, `encryptedFor` or `requiredSince`, and the contract's elected moderators may not moderate it. No reference, `countOf` or `sumOf` may name it, an update cannot change it, and a Batch refuses every document transition on it (`DocumentTypePrivateError`, 10425). Entries are appended only by `PrivateDocumentCreateFromShieldedPool` (type 29), from 1 to 16 distinct entries of exactly `entrySize` bytes, paid from the credit shielded pool. The chain does not compare `entrySize` with the properties; the SDK refuses to register a private type whose `entrySize` is below its largest payload plus the private document envelope's 299 bytes, or whose properties are not all bounded (`minimum_private_document_entry_size` in dpp). The largest payload is read from the property types alone: an `enum` or a `maxProperties` that would make it smaller is not counted. | 15 | |

### Property

Expand Down
8 changes: 7 additions & 1 deletion packages/rs-dpp/src/data_contract/document_type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ pub mod property_constraints;
pub use index_level::IndexLevel;
pub use index_level::IndexLevelTypeInfo;
pub use index_level::IndexType;
pub use private_document::PrivateDocumentConfig;
pub(crate) use private_document::PRIVATE_DOCUMENT_TYPE_KEYWORDS;
pub use private_document::{
decode_private_document_payload, encode_private_document_payload,
largest_private_document_payload_size, minimum_private_document_entry_size,
validate_private_document_entry_size, PrivateDocumentConfig,
PRIVATE_DOCUMENT_ENVELOPE_V1_BODY_HEADER_BYTES, PRIVATE_DOCUMENT_ENVELOPE_V1_OVERHEAD_BYTES,
PRIVATE_DOCUMENT_ENVELOPE_V1_WRAPPER_BYTES,
};

#[cfg(feature = "random-documents")]
pub mod random_document;
Expand Down
886 changes: 885 additions & 1 deletion packages/rs-dpp/src/data_contract/document_type/private_document.rs

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions packages/rs-dpp/src/data_contract/document_type/property/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3050,6 +3050,77 @@ impl DocumentPropertyType {
}
}

/// The most bytes [`Self::encode_value_ref_with_size`] writes for a value of this type, `None`
/// when the type sets no largest value: a string with neither `maxLength` nor `maxBytes`, a byte
/// array without `maxItems`, an untyped array, or a typed array that may hold an element of
/// such a type. `required` is the flag the encoding is called
/// with (an optional `Date` carries a marker byte); the presence byte a document writes before
/// an optional property is not counted.
///
/// Each arm is the largest output of the matching arm of the encoder, which this must follow
/// when that changes.
pub fn max_encoded_value_size(&self, required: bool) -> Option<u32> {
// A varint length, then that many bytes.
let length_prefixed = |bytes: u32| bytes.saturating_add(bytes.required_space() as u32);
match self {
DocumentPropertyType::String(sizes) => {
// A character takes at most 4 bytes in UTF-8.
let max_bytes = match (sizes.max_length, sizes.max_bytes) {
(max_length, Some(max_bytes)) => max_length
.map_or(u32::from(max_bytes), |length| {
(u32::from(length) * 4).min(u32::from(max_bytes))
}),
(Some(max_length), None) => u32::from(max_length) * 4,
(None, None) => return None,
};
Some(length_prefixed(max_bytes))
}
DocumentPropertyType::Date => Some(if required { 8 } else { 9 }),
DocumentPropertyType::U128 | DocumentPropertyType::I128 => Some(16),
DocumentPropertyType::U64 | DocumentPropertyType::I64 | DocumentPropertyType::F64 => {
Some(8)
}
DocumentPropertyType::U32
| DocumentPropertyType::I32
| DocumentPropertyType::KeyIdWithReference(_) => Some(4),
DocumentPropertyType::U16 | DocumentPropertyType::I16 => Some(2),
DocumentPropertyType::U8 | DocumentPropertyType::I8 | DocumentPropertyType::Boolean => {
Some(1)
}
DocumentPropertyType::ByteArray(sizes) => match (sizes.min_size, sizes.max_size) {
(Some(min), Some(max)) if min == max => Some(u32::from(max)),
(_, Some(max)) => Some(length_prefixed(u32::from(max))),
(_, None) => None,
},
DocumentPropertyType::Identifier | DocumentPropertyType::IdentifierWithReference(_) => {
Some(32)
}
DocumentPropertyType::Object(inner_fields) => {
let mut inner = 0u32;
for field in inner_fields.values() {
let field_size = field.property_type.max_encoded_value_size(field.required)?;
inner = inner
.saturating_add(field_size)
.saturating_add(u32::from(!field.required));
}
Some(length_prefixed(inner))
}
DocumentPropertyType::TypedArray(typed_array) => {
// A varint count, then each element encoded as a required property of its type.
// Counted here rather than by `max_encoded_size`, which saturates at `u16::MAX`.
let max_items = u32::from(typed_array.max_items);
let elements = if max_items == 0 {
0
} else {
let element = typed_array.item_type.max_encoded_value_size(true)?;
max_items.saturating_mul(element)
};
Some(elements.saturating_add(max_items.required_space() as u32))
}
DocumentPropertyType::Array(_) | DocumentPropertyType::VariableTypeArray(_) => None,
}
}

pub fn encode_value_ref_with_size(
&self,
value: &Value,
Expand Down
16 changes: 16 additions & 0 deletions packages/rs-dpp/src/document/v0/serialize/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ impl DocumentPlatformSerializationMethodsV0 for DocumentV0 {
}
}

impl DocumentV0 {
/// This document in serialization format 3, whatever the version of its contract or of the
/// contract's config. A private document payload holds a type's properties as format 3
/// writes them (`encode_private_document_payload`), including for a type whose contract keeps
/// a version 0 config, whose documents [`DocumentPlatformConversionMethodsV0::serialize`]
/// writes in format 0. Format 3 writes each property at its type's width, and a version 0
/// config parses every integer as an `I64`, so the properties come out as format 0 writes
/// them too.
pub(crate) fn serialize_in_format_3(
&self,
document_type: DocumentTypeRef,
) -> Result<Vec<u8>, ProtocolError> {
DocumentV0::serialize_v3(self, document_type)
}
}

impl DocumentPlatformDeserializationMethodsV0 for DocumentV0 {
/// Format 0 β€” implementation in [`v0`].
fn from_bytes_v0(
Expand Down
23 changes: 23 additions & 0 deletions packages/rs-dpp/src/errors/protocol_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,29 @@ pub enum ProtocolError {
#[error("Shielded transaction build error: {0}")]
ShieldedBuildError(String),

/// A private document type's `entrySize` is smaller than its largest payload needs in the
/// standard private document envelope. The chain registers such a type, but clients refuse to.
#[error(
"private document type {document_type_name} takes entries of {entry_size} bytes, fewer than \
the {minimum_entry_size} its largest payload needs in the private document envelope"
)]
PrivateDocumentEntrySizeTooSmall {
document_type_name: String,
entry_size: u32,
minimum_entry_size: u32,
},

/// A property of a private document type sets no largest value, so no entry size holds every
/// payload of the type.
#[error(
"property {property_name} of private document type {document_type_name} sets no largest \
value, so no entry size holds every payload"
)]
UnboundedPrivateDocumentProperty {
document_type_name: String,
property_name: String,
},

#[error("Not supported Error: {0}")]
NotSupported(String),

Expand Down
5 changes: 3 additions & 2 deletions packages/rs-platform-encryption/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ description = "Cryptographic utilities for Dash Platform (DIP-15 DashPay encrypt
# doesn't pull in/compile all of dashcore. `secp256k1` is pinned to the same
# 0.33 dashcore re-exports, so the public `SecretKey`/`PublicKey` types unify
# with dashcore-typed callers (platform-wallet, rs-sdk-ffi).
secp256k1 = { version = "0.33.1", features = ["std"] }
secp256k1 = { version = "0.33.1", features = ["std", "recovery"] }
aes = "0.8"
cbc = "0.1"
chacha20poly1305 = "0.10"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
hmac = "0.12"
sha2 = "0.10"
thiserror = "1.0"

[dev-dependencies]
# Tests generate keypairs via secp256k1's RNG helpers (`generate_keypair`,
# `secp256k1::rand`), gated behind the `rand` feature.
secp256k1 = { version = "0.33.1", features = ["std", "rand"] }
secp256k1 = { version = "0.33.1", features = ["std", "rand", "recovery"] }
10 changes: 10 additions & 0 deletions packages/rs-platform-encryption/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//! - [`account_label`] β€” `encryptedAccountLabel`.
//! - [`contact_info`] β€” `contactInfo` (`encToUserId` + `privateData`).
//! - [`account_reference`] β€” the masked `accountReference`.
//! - [`private_document_envelope`] β€” the standard private document envelope (version 1).
//! - [`error`] β€” the shared [`CryptoError`].
//!
//! Every public item is re-exported at the crate root, so the API is flat
Expand All @@ -22,6 +23,7 @@ mod compact_xpub;
mod contact_info;
mod ecdh;
mod error;
mod private_document_envelope;

pub use account_label::{decrypt_account_label, encrypt_account_label};
pub use account_reference::{
Expand All @@ -38,3 +40,11 @@ pub use contact_info::{
};
pub use ecdh::derive_shared_key_ecdh;
pub use error::CryptoError;
pub use private_document_envelope::{
open_private_document_entry, private_document_outgoing_viewing_key,
recover_sent_private_document_entry, seal_private_document_entry, OpenedPrivateDocumentEntry,
PrivateDocumentAuthMode, PrivateDocumentEnvelopeError, PrivateDocumentRecipient,
PrivateDocumentSender, PrivateDocumentTarget, PRIVATE_DOCUMENT_ENVELOPE_BODY_HEADER_LEN,
PRIVATE_DOCUMENT_ENVELOPE_OVERHEAD_LEN, PRIVATE_DOCUMENT_ENVELOPE_VERSION,
PRIVATE_DOCUMENT_ENVELOPE_WRAPPER_LEN,
};
Loading
Loading