Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
93a13eb
fix(drive-abci): bill batch transformer drive reads (B7)
shumkov May 19, 2026
7a56346
refactor(drive-abci): split _v0/_v1 for transform_into_action
shumkov May 19, 2026
446110e
refactor(drive-abci): move transform_into_action_v1 to its own v1 module
shumkov May 19, 2026
201a2e3
chore(drive-abci): remove audit-doc references from code
shumkov May 19, 2026
7a93acb
fix(drive-abci): bill query_documents cost in batch transformer (B4)
shumkov May 19, 2026
fefba84
chore(drive-abci): delete dead fetch_documents wrappers
shumkov May 19, 2026
2f1dca8
refactor(drive-abci): consolidate B4 billing under transform_into_action
shumkov May 19, 2026
082fae9
refactor(drive-abci): data triggers return FeeResult for caller to bill
shumkov May 19, 2026
e5fda53
fix(drive-abci): bill DPNS data trigger query_documents costs (T1, T2)
shumkov May 19, 2026
33aa35c
fix(drive-abci): bill DashPay data trigger identity balance fetch (T3)
shumkov May 19, 2026
cb18189
fix(drive-abci): bill withdrawals data trigger query_documents cost (T4)
shumkov May 19, 2026
b5d4cec
fix(drive-abci): bill fetch_document_with_id query cost (B5), unify t…
shumkov May 19, 2026
f42497d
test(drive-abci): pin trigger fee billing — T1/T2/T3/T4 regression tests
shumkov May 19, 2026
e9f239f
refactor(drive-abci): strict _v1 sibling pattern for trigger fee billing
shumkov May 19, 2026
d162df9
refactor(drive-abci): drop block_info from DataTriggerExecutionContext
shumkov May 19, 2026
9fa24e1
docs(drive-abci): add PV11 consensus-safety comments at modified _v0 …
shumkov May 19, 2026
2564e4e
refactor(drive-abci): fetch_documents helpers bill internally via &mu…
shumkov May 19, 2026
db1836f
test(drive-abci): per-trigger v0 byte-identity + v1 billing assertions
shumkov May 19, 2026
ae932ac
docs(drive-abci): explain _v1 diffs from _v0 in trigger files
shumkov May 20, 2026
ab9f6b6
refactor(drive-abci): version-facade pattern for fetch_documents helpers
shumkov May 20, 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
305 changes: 305 additions & 0 deletions docs/paid-error-fee-audit.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,23 @@ impl DocumentCreateTransitionActionStateValidationV0 for DocumentCreateTransitio
};

// TODO: Use multi get https://github.com/facebook/rocksdb/wiki/MultiGet-Performance
// We should check to see if a document already exists in the state
let (already_existing_document, fee_result) = fetch_document_with_id(
// We should check to see if a document already exists in the state.
// `fetch_document_with_id` bills the query cost internally via
// execution_context on `transform_into_action: 1` (PROTOCOL_VERSION_12+);
// on v0 it forces epoch=None and skips billing — identical to
// the pre-PR pattern where this site explicitly added a
// zero-cost FeeResult.
let already_existing_document = fetch_document_with_id(
platform.drive,
contract,
document_type,
self.base().id(),
&block_info.epoch,
execution_context,
transaction,
platform_version,
)?;

execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result));

if already_existing_document.is_some() {
return Ok(ConsensusValidationResult::new_with_error(
ConsensusError::StateError(StateError::DocumentAlreadyPresentError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,19 @@ impl DocumentCreateTransitionActionStateValidationV1 for DocumentCreateTransitio

// TODO: Use multi get https://github.com/facebook/rocksdb/wiki/MultiGet-Performance
// We should check to see if a document already exists in the state
let (already_existing_document, fee_result) = fetch_document_with_id(
// `fetch_document_with_id` bills the query cost internally via
// execution_context on transform_into_action: 1+.
let already_existing_document = fetch_document_with_id(
platform.drive,
contract,
document_type,
self.base().id(),
&block_info.epoch,
execution_context,
transaction,
platform_version,
)?;

execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result));

if already_existing_document.is_some() {
return Ok(ConsensusValidationResult::new_with_error(
ConsensusError::StateError(StateError::DocumentAlreadyPresentError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,20 @@ impl DocumentDeleteTransitionActionStateValidationV0 for DocumentDeleteTransitio
};

// TODO: Use multi get https://github.com/facebook/rocksdb/wiki/MultiGet-Performance
let (original_document, fee) = fetch_document_with_id(
// `fetch_document_with_id` bills internally on transform_into_action: 1+.
// PV11 byte-safe: v0 forces epoch=None inside, no add_operation,
// same net effect as pre-PR's explicit zero-fee add_operation call.
let original_document = fetch_document_with_id(
platform.drive,
contract,
document_type,
self.base().id(),
&block_info.epoch,
execution_context,
transaction,
platform_version,
)?;

execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee));

let Some(document) = original_document else {
return Ok(ConsensusValidationResult::new_with_error(
ConsensusError::StateError(StateError::DocumentNotFoundError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ impl DataTriggerBindingV0Getters for DataTriggerBinding {
fn execute(
&self,
document_transition: &DocumentTransitionAction,
context: &DataTriggerExecutionContext<'_>,
context: &mut DataTriggerExecutionContext<'_>,
platform_version: &PlatformVersion,
) -> Result<DataTriggerExecutionResult, Error> {
match self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub trait DataTriggerBindingV0Getters {
fn execute(
&self,
document_transition: &DocumentTransitionAction,
context: &DataTriggerExecutionContext<'_>,
context: &mut DataTriggerExecutionContext<'_>,
platform_version: &PlatformVersion,
) -> Result<DataTriggerExecutionResult, Error>;

Expand All @@ -73,10 +73,15 @@ pub trait DataTriggerBindingV0Getters {
}

impl DataTriggerBindingV0Getters for DataTriggerBindingV0 {
// PROTOCOL_VERSION_11 consensus-safety: `execute` now takes
// `&mut DataTriggerExecutionContext` instead of `&...` so that
// `_v1` triggers can call `add_operation` on the context. On PV11
// the dispatched trigger is `_v0`, which never mutates the
// context, so the chain state is identical to pre-PR.
fn execute(
&self,
document_transition: &DocumentTransitionAction,
context: &DataTriggerExecutionContext<'_>,
context: &mut DataTriggerExecutionContext<'_>,
platform_version: &PlatformVersion,
) -> Result<DataTriggerExecutionResult, Error> {
(self.data_trigger)(document_transition, context, platform_version)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ use std::fmt::{Debug, Formatter};

/// DataTriggerExecutionContext represents the context in which a data trigger is executed.
/// It contains references to relevant state and transaction data needed for the trigger to perform its actions.
#[derive(Clone)]
pub struct DataTriggerExecutionContext<'a> {
/// A reference to the platform state, which contains information about the current blockchain environment.
pub platform: &'a PlatformStateRef<'a>,
/// The transaction argument that triggered the data trigger.
pub transaction: TransactionArg<'a, 'a>,
/// The identifier of the owner of the data contract that the trigger is associated with.
pub owner_id: &'a Identifier,
/// A reference to the execution context for the state transition that triggered the data trigger.
pub state_transition_execution_context: &'a StateTransitionExecutionContext,
/// Mutable reference to the outer execution context — `_v1` triggers
/// call `add_operation` on this to bill their drive reads directly.
/// `_v0` triggers ignore it (preserving PROTOCOL_VERSION_11 chain
/// replay byte-identity at the behavior level).
pub state_transition_execution_context: &'a mut StateTransitionExecutionContext,
}

impl Debug for DataTriggerExecutionContext<'_> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub trait DataTriggerExecutor {
fn validate_with_data_triggers(
&self,
data_trigger_bindings: &[DataTriggerBinding],
context: &DataTriggerExecutionContext<'_>,
context: &mut DataTriggerExecutionContext<'_>,
platform_version: &PlatformVersion,
) -> Result<DataTriggerExecutionResult, Error>;
}
Expand All @@ -23,15 +23,18 @@ impl DataTriggerExecutor for DocumentTransitionAction {
fn validate_with_data_triggers(
&self,
data_trigger_bindings: &[DataTriggerBinding],
context: &DataTriggerExecutionContext,
context: &mut DataTriggerExecutionContext,
platform_version: &PlatformVersion,
) -> Result<DataTriggerExecutionResult, Error> {
let data_contract_id = self.base().data_contract_id();
let document_type_name = self.base().document_type_name();
let transition_action = self.action_type();

// Match data triggers by action type, contract ID and document type name
// and then execute matched triggers until one of them returns invalid result
// Match data triggers by action type, contract ID and document
// type name, then execute matched triggers until one returns
// invalid. `_v1` triggers bill their own drive reads directly
// via `context.state_transition_execution_context.add_operation`;
// `_v0` triggers don't bill (PROTOCOL_VERSION_11 chain replay).
for data_trigger_binding in data_trigger_bindings {
if !data_trigger_binding.is_matching(
&data_contract_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ mod context;
mod executor;
mod triggers;

/// Data trigger function pointer.
///
/// Takes a mutable `DataTriggerExecutionContext` so `_v1` triggers can
/// call `context.state_transition_execution_context.add_operation(...)`
/// directly to bill their drive reads. `_v0` triggers don't call
/// `add_operation` (preserving PROTOCOL_VERSION_11 chain replay).
type DataTrigger = fn(
&DocumentTransitionAction,
&DataTriggerExecutionContext<'_>,
&mut DataTriggerExecutionContext<'_>,
&PlatformVersion,
) -> Result<DataTriggerExecutionResult, Error>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
use crate::error::execution::ExecutionError;
use crate::error::Error;
use crate::execution::validation::state_transition::batch::data_triggers::triggers::dashpay::v0::create_contact_request_data_trigger_v0;
use crate::execution::validation::state_transition::batch::data_triggers::triggers::dashpay::v1::create_contact_request_data_trigger_v1;
use crate::execution::validation::state_transition::batch::data_triggers::{
DataTriggerExecutionContext, DataTriggerExecutionResult,
};
use dpp::version::PlatformVersion;
use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction;

mod v0;
mod v1;

pub fn create_contact_request_data_trigger(
document_transition: &DocumentTransitionAction,
context: &DataTriggerExecutionContext<'_>,
context: &mut DataTriggerExecutionContext<'_>,
platform_version: &PlatformVersion,
) -> Result<DataTriggerExecutionResult, Error> {
match platform_version
Expand All @@ -24,9 +26,10 @@ pub fn create_contact_request_data_trigger(
.create_contact_request_data_trigger
{
0 => create_contact_request_data_trigger_v0(document_transition, context, platform_version),
1 => create_contact_request_data_trigger_v1(document_transition, context, platform_version),
version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch {
method: "create_contact_request_data_trigger".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
Expand Up @@ -30,10 +30,13 @@ use crate::execution::validation::state_transition::batch::data_triggers::{DataT
/// # Returns
///
/// A `DataTriggerExecutionResult` indicating the success or failure of the trigger execution.
// PROTOCOL_VERSION_11 consensus-safety: body byte-identical to
// v3.1-dev. Only the `context` param type changed from `&` to `&mut`
// (compile-time only — the body never mutates the context).
#[inline(always)]
pub(super) fn create_contact_request_data_trigger_v0(
document_transition: &DocumentTransitionAction,
context: &DataTriggerExecutionContext<'_>,
context: &mut DataTriggerExecutionContext<'_>,
platform_version: &PlatformVersion,
) -> Result<DataTriggerExecutionResult, Error> {
let data_contract_fetch_info = document_transition.base().data_contract_fetch_info();
Expand Down Expand Up @@ -177,18 +180,18 @@ mod test {

transition_execution_context.enable_dry_run();

let data_trigger_context = DataTriggerExecutionContext {
let mut data_trigger_context = DataTriggerExecutionContext {
platform: &platform_ref,
owner_id,
state_transition_execution_context: &transition_execution_context,
state_transition_execution_context: &mut transition_execution_context,
transaction: None,
};

let result = create_contact_request_data_trigger(
let result = create_contact_request_data_trigger( // dispatches to _v0 (per-trigger version field = 0 at this test's default PV)
&DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, *owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| {
Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version)))
}, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"),
&data_trigger_context,
&mut data_trigger_context,
platform_version,
)
.expect("the execution result should be returned");
Expand Down Expand Up @@ -279,7 +282,7 @@ mod test {
.as_transition_create()
.expect("expected a document create transition");

let transition_execution_context =
let mut transition_execution_context =
StateTransitionExecutionContext::default_for_platform_version(platform_version)
.unwrap();
let identity_fixture =
Expand All @@ -298,20 +301,20 @@ mod test {
)
.expect("expected to insert identity");

let data_trigger_context = DataTriggerExecutionContext {
let mut data_trigger_context = DataTriggerExecutionContext {
platform: &platform_ref,
owner_id: &owner_id,
state_transition_execution_context: &transition_execution_context,
state_transition_execution_context: &mut transition_execution_context,
transaction: None,
};

let _dashpay_identity_id = data_trigger_context.owner_id.to_owned();

let result = create_contact_request_data_trigger(
let result = create_contact_request_data_trigger( // dispatches to _v0 (per-trigger version field = 0 at this test's default PV)
&DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| {
Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version)))
}, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"),
&data_trigger_context,
&mut data_trigger_context,
platform_version,
)
.expect("data trigger result should be returned");
Expand Down Expand Up @@ -412,24 +415,24 @@ mod test {
.as_transition_create()
.expect("expected a document create transition");

let transition_execution_context =
let mut transition_execution_context =
StateTransitionExecutionContext::default_for_platform_version(platform_version)
.unwrap();

let data_trigger_context = DataTriggerExecutionContext {
let mut data_trigger_context = DataTriggerExecutionContext {
platform: &platform_ref,
owner_id: &owner_id,
state_transition_execution_context: &transition_execution_context,
state_transition_execution_context: &mut transition_execution_context,
transaction: None,
};

let _dashpay_identity_id = data_trigger_context.owner_id.to_owned();

let result = create_contact_request_data_trigger(
let result = create_contact_request_data_trigger( // dispatches to _v0 (per-trigger version field = 0 at this test's default PV)
&DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| {
Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version)))
}, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"),
&data_trigger_context,
&mut data_trigger_context,
platform_version,
)
.expect("data trigger result should be returned");
Expand All @@ -443,5 +446,19 @@ mod test {
e.message() == format!("Identity {contract_request_to_user_id} doesn't exist")
}
));

// T3 PROTOCOL_VERSION_12+ billing assertion: this test runs at
// `PlatformVersion::latest()` where
// `create_contact_request_data_trigger: 1` dispatches to `_v1`.
// `_v1` must surface the `fetch_identity_balance_with_costs`
// cost via `add_operation`. If a regression drops the
// `add_operation` call in `_v1`, this assertion fails.
let ops = data_trigger_context
.state_transition_execution_context
.operations_slice();
assert!(
!ops.is_empty(),
"T3: _v1 must add operations to execution_context (caught zero ops)"
);
}
}
Loading
Loading