Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5aa2a1e
feat(dpp)!: add indexOnly document types with terminal index keys
QuantumExplorer Aug 27, 2026
6970c09
feat(drive)!: indexOnly storage layout — index entries as the rows
QuantumExplorer Aug 27, 2026
5cd8f9b
feat(dpp)!: indexOnly transitions and ABCI validation — delete-by-values
QuantumExplorer Aug 27, 2026
fb9dc14
fix(dpp): address indexOnly review — owner-bound entries, terminal ty…
QuantumExplorer Aug 27, 2026
74dbf92
Merge branch 'feat/index-only-dpp' into feat/index-only-drive
QuantumExplorer Aug 27, 2026
9a80440
Merge branch 'feat/index-only-drive' into feat/index-only-transitions
QuantumExplorer Aug 27, 2026
f675274
fix(drive-abci): probe every index entry on indexOnly delete validation
QuantumExplorer Aug 27, 2026
2cc3ce0
fix(drive): bind indexOnly entries into one row with a stored commitment
QuantumExplorer Aug 27, 2026
e71f609
Merge branch 'feat/index-only-drive' into feat/index-only-transitions
QuantumExplorer Aug 27, 2026
0cadcd1
fix(drive)!: enforce row commitments on indexOnly deletes; version-pr…
QuantumExplorer Aug 27, 2026
cdf08cd
Merge remote-tracking branch 'origin/v4.2-dev' into feat/index-only-d…
QuantumExplorer Aug 27, 2026
94a17f5
Merge branch 'feat/index-only-drive' into feat/index-only-transitions
QuantumExplorer Aug 27, 2026
cf3bd34
fix(drive): drop unused EpochCosts import in indexOnly e2e tests
QuantumExplorer Aug 27, 2026
e6a6512
Merge branch 'feat/index-only-drive' into feat/index-only-transitions
QuantumExplorer Aug 27, 2026
3e77942
refactor(dpp): default index terminals at parse time, not by rebuild
QuantumExplorer Aug 27, 2026
d680ce3
Merge branch 'feat/index-only-drive' into feat/index-only-transitions
QuantumExplorer Aug 27, 2026
39a21bc
style(dpp): keep platform_version last in the core parse signature
QuantumExplorer Aug 27, 2026
1362499
Merge branch 'feat/index-only-drive' into feat/index-only-transitions
QuantumExplorer Aug 27, 2026
52608c0
refactor(drive): move index_only_row_commitment into its own module
QuantumExplorer Aug 27, 2026
1676c4f
Merge branch 'feat/index-only-drive' into feat/index-only-transitions
QuantumExplorer Aug 27, 2026
d566dfb
fix(drive-abci): validate indexOnly delete values and bill entry probes
QuantumExplorer Aug 27, 2026
edade25
Merge remote-tracking branch 'origin/v4.2-dev' into feat/index-only-t…
QuantumExplorer Aug 27, 2026
c73007c
docs(drive): align indexOnly test comments and naming with review
QuantumExplorer Aug 27, 2026
e1db8ed
test(drive-abci): pin the V0-on-indexOnly refusal to its structure-ga…
QuantumExplorer Aug 27, 2026
9431f84
style: cargo fmt
QuantumExplorer Aug 27, 2026
6f3d0ce
fix(dpp): key the V1 delete's $createdAt ride-along on the doctype re…
QuantumExplorer Aug 27, 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
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,28 @@ impl DocumentDeleteTransition {
feature_version: Option<FeatureVersion>,
base_feature_version: Option<FeatureVersion>,
) -> Result<Self, ProtocolError> {
match feature_version.unwrap_or(
platform_version
// An indexOnly document is deleted from its values (V1 or later);
// everything else keeps the table's default (V0, delete-by-id).
// Selecting here means every construction path — SDKs included —
// produces the variant the ABCI structure gates require for the
// doctype, with no per-client knowledge of the storage mode. The
// table default is raised to 1, never clamped down: a future table
// whose default is a later values-carrying variant keeps winning.
let default_version = {
use crate::data_contract::document_type::accessors::DocumentTypeV2Getters;
let table_default = platform_version
.dpp
.state_transition_serialization_versions
.document_delete_state_transition
.bounds
.default_current_version,
) {
.default_current_version;
if document_type.index_only() {
table_default.max(1)
} else {
table_default
}
};
match feature_version.unwrap_or(default_version) {
0 => Ok(DocumentDeleteTransitionV0::from_document(
document,
document_type,
Expand All @@ -36,9 +50,20 @@ impl DocumentDeleteTransition {
base_feature_version,
)?
.into()),
1 => Ok(
crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransitionV1::from_document(
document,
document_type,
token_payment_info,
identity_contract_nonce,
platform_version,
base_feature_version,
)?
.into(),
),
version => Err(ProtocolError::UnknownVersionMismatch {
method: "DocumentDeleteTransition::from_document".to_string(),
known_versions: vec![0],
known_versions: vec![0, 1],
received: version,
}),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
mod from_document;
pub mod v0;
pub mod v0_methods;
pub mod v1;
pub mod v1_methods;

use bincode::{Decode, Encode};
use derive_more::{Display, From};
#[cfg(feature = "serde-conversion")]
use serde::{Deserialize, Serialize};
pub use v0::*;
pub use v1::*;

#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)]
#[cfg_attr(
Expand All @@ -18,6 +21,14 @@ pub enum DocumentDeleteTransition {
#[display("V0({})", "_0")]
#[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
V0(DocumentDeleteTransitionV0),
/// The indexOnly delete: base plus the document's full property-value
/// tuple (there is no primary-storage row to fetch values from). Only
/// accepted for indexOnly document types — the ABCI structure gates
/// pair each variant with its storage mode. Serialization bound is
/// raised to 1 at PV14 (STATE_TRANSITION_SERIALIZATION_VERSIONS_V3).
#[display("V1({})", "_0")]
#[cfg_attr(feature = "serde-conversion", serde(rename = "1"))]
V1(DocumentDeleteTransitionV1),
}

#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ impl DocumentBaseTransitionAccessors for DocumentDeleteTransition {
fn base(&self) -> &DocumentBaseTransition {
match self {
DocumentDeleteTransition::V0(v0) => &v0.base,
DocumentDeleteTransition::V1(v1) => &v1.base,
}
}

fn base_mut(&mut self) -> &mut DocumentBaseTransition {
match self {
DocumentDeleteTransition::V0(v0) => &mut v0.base,
DocumentDeleteTransition::V1(v1) => &mut v1.base,
}
}

fn set_base(&mut self, base: DocumentBaseTransition) {
match self {
DocumentDeleteTransition::V0(v0) => v0.base = base,
DocumentDeleteTransition::V1(v1) => v1.base = base,
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use crate::data_contract::document_type::accessors::DocumentTypeV0Getters;
use crate::data_contract::document_type::DocumentTypeRef;
use crate::document::property_names::CREATED_AT;
use crate::document::{Document, DocumentV0Getters};
use crate::prelude::IdentityNonce;
use crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransitionV1;
use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition;
use crate::tokens::token_payment_info::TokenPaymentInfo;
use crate::ProtocolError;
use platform_value::Value;
use platform_version::version::{FeatureVersion, PlatformVersion};

impl DocumentDeleteTransitionV1 {
pub(crate) fn from_document(
document: Document,
document_type: DocumentTypeRef,
token_payment_info: Option<TokenPaymentInfo>,
identity_contract_nonce: IdentityNonce,
platform_version: &PlatformVersion,
base_feature_version: Option<FeatureVersion>,
) -> Result<Self, ProtocolError> {
Ok(DocumentDeleteTransitionV1 {
base: DocumentBaseTransition::from_document(
&document,
document_type,
token_payment_info,
identity_contract_nonce,
platform_version,
base_feature_version,
)?,
// The values ARE the document on an indexOnly type — the
// delete carries them so every index entry can be recomputed
// without a primary-storage fetch. `$createdAt` rides along
// under its system key exactly when the doctype requires it
// (an indexed `$createdAt` forces the requirement, and it
// feeds the row commitment) — keyed on the TYPE, not on
// whatever the local `Document` object happens to carry, so
// construction always emits the payload shape the structure
// validation accepts.
data: {
let mut data = document.properties().clone();
if document_type.required_fields().contains(CREATED_AT) {
let created_at = document.created_at().ok_or_else(|| {
ProtocolError::Generic(format!(
"an indexOnly document of type {} requires $createdAt, but the \
document being deleted does not carry one",
document_type.name()
))
})?;
data.insert(CREATED_AT.to_string(), Value::U64(created_at));
}
data
},
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
mod from_document;
pub mod v1_methods;

use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition;
use std::collections::BTreeMap;

use bincode::{Decode, Encode};
use derive_more::Display;
use platform_value::Value;

#[cfg(feature = "json-conversion")]
use crate::serialization::json_safe_fields;
#[cfg(feature = "serde-conversion")]
use serde::{Deserialize, Serialize};

pub use super::super::document_base_transition::IDENTIFIER_FIELDS;

/// The **indexOnly** delete: carries the document's full property-value
/// tuple alongside the base.
///
/// An indexOnly document has no primary-storage row, so a delete cannot
/// fetch anything by id — the values in `data` (plus the signer as owner)
/// are what every index entry is recomputed from, the exact mirror of what
/// the create wrote. V0 (base only) stays the delete for stored document
/// types; the ABCI structure gates pair each variant with its storage mode.
#[cfg_attr(feature = "json-conversion", json_safe_fields)]
// `Deserialize` is implemented manually below — same reason as
// `DocumentCreateTransitionV0`: two `#[serde(flatten)]` fields, one of
// which is a catchall map that would otherwise swallow the base's keys.
#[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)]
#[cfg_attr(
feature = "serde-conversion",
derive(Serialize),
serde(rename_all = "camelCase")
)]
#[display("Base: {}, Data: {:?}", "base", "data")]
pub struct DocumentDeleteTransitionV1 {
/// Document Base Transition
#[cfg_attr(feature = "serde-conversion", serde(flatten))]
pub base: DocumentBaseTransition,

/// The property values of the indexOnly document being deleted.
#[cfg_attr(feature = "serde-conversion", serde(flatten))]
pub data: BTreeMap<String, Value>,
}

// Manual `Deserialize`: peel the base's known keys off the flat object,
// reconstruct the base from them, and route everything left to `data`.
// See the WARNING on `DocumentCreateTransitionV0`'s impl — a new base
// field must be added to `BASE_FIELD_NAMES` here too.
#[cfg(feature = "serde-conversion")]
impl<'de> Deserialize<'de> for DocumentDeleteTransitionV1 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;

// Tag + every serde-renamed field of `DocumentBaseTransitionV0` /
// `DocumentBaseTransitionV1`. Keep in sync with the base structs.
const BASE_FIELD_NAMES: &[&str] = &[
"$baseFormatVersion",
"$id",
"$identityContractNonce",
"$type",
"$dataContractId",
"$tokenPaymentInfo",
];

let mut map: BTreeMap<String, Value> = BTreeMap::deserialize(deserializer)?;

let mut base_pairs: Vec<(Value, Value)> = Vec::with_capacity(BASE_FIELD_NAMES.len());
for key in BASE_FIELD_NAMES {
if let Some(value) = map.remove(*key) {
base_pairs.push((Value::Text((*key).to_string()), value));
}
}
let base = platform_value::from_value::<DocumentBaseTransition>(Value::Map(base_pairs))
.map_err(D::Error::custom)?;

Ok(DocumentDeleteTransitionV1 { base, data: map })
Comment on lines +52 to +81

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: The V1 manual deserializer has no V1 round-trip coverage

This hand-written deserializer relies on an exhaustively maintained list of flattened base keys; an omitted current or future base key is silently routed into document data. The delete JSON/value fixtures and umbrella tests construct only DocumentDeleteTransition::V0, while the ABCI tests exercise platform binary serialization rather than this serde implementation. Add V1 JSON and platform-value round-trip fixtures containing non-default V1 base fields, ordinary document properties, and $createdAt, and assert the complete flattened wire shape and recovered value.

source: ['codex']

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransitionV1;
use crate::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors;
use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition;

impl DocumentBaseTransitionAccessors for DocumentDeleteTransitionV1 {
fn base(&self) -> &DocumentBaseTransition {
&self.base
}

fn base_mut(&mut self) -> &mut DocumentBaseTransition {
&mut self.base
}

fn set_base(&mut self, base: DocumentBaseTransition) {
self.base = base
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use crate::state_transition::batch_transition::batched_transition::DocumentDeleteTransition;
use platform_value::Value;
use std::collections::BTreeMap;

/// V1 (indexOnly) accessors on the delete transition enum. V0 arms return
/// `None` — a stored-document delete carries no values, same convention as
/// the base transition's `V1Methods`.
pub trait DocumentDeleteTransitionV1Methods {
/// The property values of the indexOnly document being deleted, or
/// `None` on a V0 (stored-document) delete.
fn data(&self) -> Option<&BTreeMap<String, Value>>;
}

impl DocumentDeleteTransitionV1Methods for DocumentDeleteTransition {
fn data(&self) -> Option<&BTreeMap<String, Value>> {
match self {
DocumentDeleteTransition::V0(_) => None,
DocumentDeleteTransition::V1(v1) => Some(&v1.data),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ impl BatchTransition {
NonceOutOfBoundsError::new(transition.identity_contract_nonce()),
));
}

// The V1 (indexOnly delete-by-values) variant joined the
// wire at PV14. Old software cannot decode it at all, so
// no historical block can contain one — this check exists
// so that NEW software agrees with old software while a
// pre-PV14 protocol version is still active: without it, a
// V1 delete submitted at PV13 would decode fine here while
// being undecodable on 4.1 nodes.
if let DocumentTransition::Delete(
crate::state_transition::batch_transition::batched_transition::DocumentDeleteTransition::V1(_),
) = transition
{
let bounds = &platform_version
.dpp
.state_transition_serialization_versions
.document_delete_state_transition
.bounds;
if bounds.max_version < 1 {
result.add_error(BasicError::UnsupportedVersionError(
crate::consensus::basic::unsupported_version_error::UnsupportedVersionError::new(
1,
bounds.min_version,
bounds.max_version,
),
));
}
}
}

// Make sure we don't have duplicate transitions
Expand Down Expand Up @@ -289,6 +316,60 @@ mod tests {
})
}

// -----------------------------------------------------------------------
// delete V1 (indexOnly delete-by-values) wire gate
// -----------------------------------------------------------------------

/// A V1 delete cannot decode at all on pre-4.2 software, so blocks never
/// contain one below PV14 — this check is what keeps NEW software
/// agreeing with old software at check_tx while an earlier protocol
/// version is still active. Admitted at PV14 (bounds max_version 1,
/// STATE_TRANSITION_SERIALIZATION_VERSIONS_V3), rejected below.
#[test]
fn validate_base_structure_v0_gates_delete_v1_by_protocol_version() {
use crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransitionV1;
use crate::state_transition::batch_transition::batched_transition::DocumentDeleteTransition;

let delete_v1 =
DocumentTransition::Delete(DocumentDeleteTransition::V1(DocumentDeleteTransitionV1 {
base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 {
id: Identifier::new([0x11; 32]),
identity_contract_nonce: 1,
document_type_name: "like".to_string(),
data_contract_id: Identifier::new([0xAA; 32]),
}),
data: Default::default(),
}));

let batch = make_batch_v0(vec![delete_v1]);

let pv13 = PlatformVersion::get(13).expect("PV13 exists");
let result = batch
.validate_base_structure_v0(pv13)
.expect("no protocol err");
assert!(
result.errors.iter().any(|error| matches!(
error,
ConsensusError::BasicError(BasicError::UnsupportedVersionError(_))
)),
"PV13 must reject a V1 delete as an unsupported version, got {:?}",
result.errors
);

let pv14 = PlatformVersion::get(14).expect("PV14 exists");
let result = batch
.validate_base_structure_v0(pv14)
.expect("no protocol err");
assert!(
!result.errors.iter().any(|error| matches!(
error,
ConsensusError::BasicError(BasicError::UnsupportedVersionError(_))
)),
"PV14 must admit a V1 delete, got {:?}",
result.errors
);
}

// -----------------------------------------------------------------------
// empty batch — DocumentTransitionsAreAbsentError
// -----------------------------------------------------------------------
Expand Down
Loading
Loading