Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -0,0 +1,135 @@
//! Batch-scoped tracking of the index entries indexOnly creates write.
//!
//! Every transition of a batch is validated against the same, not yet
//! applied state, and the whole batch is then applied as ONE grove batch.
//! For a stored document type that is safe: two creates in one batch
//! cannot share an id (`find_duplicates_by_id` refuses that at basic
//! structure) and so write distinct primary rows. An indexOnly document
//! has no row — its index entries ARE the rows — and two creates by the
//! same owner can address the same entry under one index while differing
//! under another (a shorter index projects fewer properties). The create's
//! state probe (`has_index_only_document_entry`) reads committed state, so
//! it sees neither create's entries; the storage walker's if-not-exists
//! insert reads the same state; and grovedb files a batch's operations by
//! path and key, so the second insert silently replaces the first. The
//! result is one entry carrying the other document's row commitment while
//! the rest of the loser's entries stand: a document nobody can delete
//! (its commitment probe fails on the replaced entry) or recreate (its
//! surviving entries are duplicates).
//!
//! This tracker closes the gap the state probe cannot see. It records the
//! entries every create the batch has already accepted will write and
//! refuses a later create of the same batch that would write any of them,
//! with the `DuplicateUniqueIndexError` the state probe raises for the
//! same collision against committed state. Nothing is read: the entry
//! paths and member keys come from the action's own values through
//! [`Drive::index_only_entry_paths_and_key`], the derivation the index
//! walkers write with, so nothing is billed and the check cannot drift
//! from storage.
//!
//! `index_only()` can only be true on a PV14+ contract, so the tracker is
//! a no-op for every historical batch. It is also dormant today for a
//! second reason: `max_transitions_in_documents_batch` is 1 at every
//! protocol version, so basic structure validation refuses any batch
//! carrying two transitions before either reaches this loop, and two
//! transitions in one block apply as two grove batches (the second's
//! state probe sees the first's entries). The tracker is what keeps
//! indexOnly types safe on the day that cap is raised — the test in
//! `batch/tests/document/index_only.rs` drives the loop directly to pin
//! it.

use crate::error::Error;
use dpp::consensus::basic::document::InvalidDocumentTypeError;
use dpp::consensus::state::document::duplicate_unique_index_error::DuplicateUniqueIndexError;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters};
use dpp::document::Document;
use dpp::identifier::Identifier;
use dpp::validation::SimpleConsensusValidationResult;
use dpp::version::PlatformVersion;
use drive::drive::Drive;
use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0;
use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::{
DocumentCreateTransitionAction, DocumentCreateTransitionActionAccessorsV0,
DocumentFromCreateTransitionAction,
};
use std::collections::BTreeSet;

/// The `(entry path, member key)` pairs every accepted indexOnly create of
/// one batch writes. One tracker per batch state validation.
#[derive(Default)]
pub(super) struct IndexOnlyBatchEntries {
entries: BTreeSet<(Vec<Vec<u8>>, Vec<u8>)>,
}

impl IndexOnlyBatchEntries {
/// Refuses `create_action` when any entry it would write is already
/// claimed by an earlier accepted create of the same batch, and claims
/// all of its entries otherwise. Call it only for a create that state
/// validation (and the data triggers) accepted: a refused create writes
/// nothing, so it must not block a later create in the batch. A no-op
/// for stored (non-indexOnly) document types.
pub(super) fn validate_and_record_create(
&mut self,
create_action: &DocumentCreateTransitionAction,
owner_id: Identifier,
platform_version: &PlatformVersion,
) -> Result<SimpleConsensusValidationResult, Error> {
let contract_fetch_info = create_action.base().data_contract_fetch_info();
let contract = &contract_fetch_info.contract;
let document_type_name = create_action.base().document_type_name();

// The create's own state validation resolves the document type
// first and refuses an unknown one, so this mirrors that refusal
// rather than treating it as a code error.
let Some(document_type) = contract.document_type_optional_for_name(document_type_name)
else {
return Ok(SimpleConsensusValidationResult::new_with_error(
InvalidDocumentTypeError::new(document_type_name.clone(), contract.id()).into(),
));
};

if !document_type.index_only() {
return Ok(SimpleConsensusValidationResult::new());
}

let document =
Document::try_from_create_transition_action(create_action, owner_id, platform_version)?;

let mut claimed = Vec::new();
for index in document_type.indexes().values() {
let (paths, member_key) = Drive::index_only_entry_paths_and_key(
contract.id(),
document_type,
index,
&document,
platform_version,
)
.map_err(Error::Drive)?;
for path in paths {
let entry = (path, member_key.clone());
if self.entries.contains(&entry) {
return Ok(SimpleConsensusValidationResult::new_with_error(
DuplicateUniqueIndexError::new(
create_action.base().id(),
index
.properties
.iter()
.map(|property| property.name.clone())
.chain(index.terminal.clone())
.collect(),
)
.into(),
));
}
claimed.push(entry);
}
}

// Claim only once every entry is known to be free, so a refused
// create leaves the tracker exactly as it found it.
self.entries.extend(claimed);

Ok(SimpleConsensusValidationResult::new())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ use crate::execution::validation::state_transition::batch::action_validation::to
use crate::execution::validation::state_transition::batch::action_validation::token::token_transfer_transition_action::TokenTransferTransitionActionValidation;
use crate::execution::validation::state_transition::batch::action_validation::token::token_unfreeze_transition_action::TokenUnfreezeTransitionActionValidation;
use crate::execution::validation::state_transition::batch::data_triggers::{data_trigger_bindings_list, DataTriggerExecutionContext, DataTriggerExecutor};
use crate::execution::validation::state_transition::batch::state::v0::index_only_batch_entries::IndexOnlyBatchEntries;
use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0;
use crate::platform_types::platform::{PlatformStateRef};
use crate::execution::validation::state_transition::state_transitions::batch::transformer::v0::BatchTransitionTransformerV0;
use crate::execution::validation::state_transition::ValidationMode;
use crate::platform_types::platform_state::PlatformStateV0Methods;

pub mod fetch_contender;
pub mod fetch_documents;
mod index_only_batch_entries;

pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionStateValidationV0
{
Expand Down Expand Up @@ -85,6 +88,13 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition {
vec![]
};

// The entries every accepted indexOnly create of THIS batch writes.
// The per-create state probe reads committed state, which none of
// the batch's own creates have reached yet, and the batch applies as
// one grove batch where a second insert at the same path and key
// silently replaces the first — see `index_only_batch_entries`.
let mut index_only_batch_entries = IndexOnlyBatchEntries::default();

// Next we need to validate the structure of all actions (this means with the data contract)
for transition in state_transition_action.transitions_take() {
let transition_validation_result = match &transition {
Expand Down Expand Up @@ -276,7 +286,10 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition {
state_transition_action.user_fee_increase(),
)?,
));
} else if platform.config.execution.use_document_triggers {
continue;
}

if platform.config.execution.use_document_triggers {
if let BatchedTransitionAction::DocumentAction(document_transition) = &transition {
// Pre-PR this site allocated a default-initialized local
// `StateTransitionExecutionContext` and passed `&local` to
Expand Down Expand Up @@ -338,15 +351,41 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition {
state_transition_action.user_fee_increase(),
),
));
} else {
validated_transitions.push(transition);
continue;
}
} else {
validated_transitions.push(transition);
}
} else {
validated_transitions.push(transition);
}

// An accepted indexOnly create claims the entries it writes for
// the rest of the batch; a later create addressing any of them
// is refused here exactly as the state probe refuses the same
// collision against committed state. Only reachable for PV14+
// contracts (`index_only()` cannot be true below that), so no
// historical batch takes this path.
if let BatchedTransitionAction::DocumentAction(
DocumentTransitionAction::CreateAction(create_action),
) = &transition
{
let batch_entries_result = index_only_batch_entries.validate_and_record_create(
create_action,
owner_id,
platform_version,
)?;
if !batch_entries_result.is_valid() {
validation_result.add_errors(batch_entries_result.errors);
validated_transitions
.push(BatchedTransitionAction::BumpIdentityDataContractNonce(
BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(
create_action.base(),
owner_id,
state_transition_action.user_fee_increase(),
),
));
continue;
}
}

validated_transitions.push(transition);
}

state_transition_action.set_transitions(validated_transitions);
Expand Down
Loading
Loading