diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs index 5b016084d44..9da8abd7843 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs @@ -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( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs index 58f8e8b8204..67177456e68 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs @@ -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( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs index 7b386543d41..31f9c6cb94b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs @@ -15,8 +15,7 @@ use drive::grovedb::TransactionArg; 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_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; use crate::error::Error; -use crate::execution::types::execution_operation::ValidationOperation; -use crate::execution::types::state_transition_execution_context::{StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0}; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::DocumentBaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::fetch_document_with_id; use crate::platform_types::platform::PlatformStateRef; @@ -69,17 +68,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( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs index 9c06e6b4294..0468c93a7e8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs @@ -22,7 +22,7 @@ impl DataTriggerBindingV0Getters for DataTriggerBinding { fn execute( &self, document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, ) -> Result { match self { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs index 39e3c871c66..8b722617c84 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs @@ -46,7 +46,7 @@ pub trait DataTriggerBindingV0Getters { fn execute( &self, document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, ) -> Result; @@ -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 { (self.data_trigger)(document_transition, context, platform_version) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs index 3060e541601..08822174af6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs @@ -1,27 +1,37 @@ use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; use dpp::prelude::*; use drive::grovedb::TransactionArg; 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 block currently being validated. `_v1` triggers price their + /// grovedb reads against this block's epoch — using + /// `platform.state.last_committed_block_info()` instead would bill + /// at the previous epoch's fee schedule on the first block of a new + /// epoch. + pub block_info: &'a BlockInfo, /// 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<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("data_trigger_execution_context") .field("platform", self.platform) + .field("block_info", self.block_info) .field("owner_id", self.owner_id) .field( "state_transition_execution_context", diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs index a85f1b6febb..e34d6341cfa 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs @@ -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; } @@ -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 { 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, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs index 21bdb917740..ac4267d7040 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs @@ -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; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs index 61c486c13ad..c782ac77381 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs @@ -1,6 +1,7 @@ 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, }; @@ -8,10 +9,11 @@ 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 { match platform_version @@ -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, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs index 3adefc5de0c..66d86eb1470 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs @@ -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 { let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); @@ -177,18 +180,20 @@ mod test { transition_execution_context.enable_dry_run(); - let data_trigger_context = DataTriggerExecutionContext { + let trigger_block_info = BlockInfo::default(); + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, + block_info: &trigger_block_info, 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"); @@ -279,7 +284,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 = @@ -298,20 +303,22 @@ mod test { ) .expect("expected to insert identity"); - let data_trigger_context = DataTriggerExecutionContext { + let trigger_block_info = BlockInfo::default(); + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, + block_info: &trigger_block_info, 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"); @@ -412,24 +419,26 @@ 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 trigger_block_info = BlockInfo::default(); + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, + block_info: &trigger_block_info, 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"); @@ -443,5 +452,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)" + ); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs new file mode 100644 index 00000000000..fc169106fa5 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs @@ -0,0 +1,107 @@ +//! PROTOCOL_VERSION_12+ version of the DashPay contact-request trigger. +//! +//! Mirrors `create_contact_request_data_trigger_v0` but uses +//! `fetch_identity_balance_with_costs` (instead of `fetch_identity_balance`) +//! and bills the returned `FeeResult` via +//! `context.state_transition_execution_context.add_operation(...)`. + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; +use crate::execution::validation::state_transition::batch::data_triggers::{ + DataTriggerExecutionContext, DataTriggerExecutionResult, +}; +use dpp::consensus::state::data_trigger::data_trigger_condition_error::DataTriggerConditionError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; +use dpp::system_data_contracts::dashpay_contract::v1::document_types::contact_request::properties::TO_USER_ID; +use dpp::version::PlatformVersion; +use dpp::ProtocolError; +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::DocumentCreateTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; + +#[inline(always)] +pub(super) fn create_contact_request_data_trigger_v1( + document_transition: &DocumentTransitionAction, + context: &mut DataTriggerExecutionContext<'_>, + platform_version: &PlatformVersion, +) -> Result { + let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); + let data_contract = &data_contract_fetch_info.contract; + let mut result = DataTriggerExecutionResult::default(); + let is_dry_run = context.state_transition_execution_context.in_dry_run(); + let owner_id = context.owner_id; + + let DocumentTransitionAction::CreateAction(document_create_transition) = document_transition + else { + return Err(Error::Execution(ExecutionError::DataTriggerExecutionError( + format!( + "the Document Transition {} isn't 'CREATE", + document_transition.base().id() + ), + ))); + }; + + let data = document_create_transition.data(); + + let to_user_id = data + .get_identifier(TO_USER_ID) + .map_err(ProtocolError::ValueError)?; + + // You shouldn't create a contract request to yourself + if !is_dry_run && owner_id == &to_user_id { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!("Identity {to_user_id} must not be equal to owner id"), + ); + + result.add_error(err); + + return Ok(result); + } + + // Diff vs `_v0` (recipient identity existence check): + // + // _v0: `.fetch_identity_balance(to_user_id, transaction, pv)?` + // — returns just the balance (no cost info). No way to + // bill the grovedb read; the explicit `TODO: Calculate fee + // operations` comment marked the gap. + // + // _v1: `.fetch_identity_balance_with_costs(to_user_id, + // block_info, apply=true, transaction, pv)?` — returns + // `(Option, FeeResult)`. Bill the FeeResult via + // `add_operation`. + // + // Why the change: contact-request creates would do a grovedb + // identity-balance lookup for free on PV11. `apply: true` matches + // `_v0`'s stateful query (not the stateless estimated-cost path) + // so the balance value returned is byte-identical to `_v0`. + let (to_identity, balance_fee_result) = + context.platform.drive.fetch_identity_balance_with_costs( + to_user_id.to_buffer(), + context.block_info, + true, + context.transaction, + platform_version, + )?; + context.state_transition_execution_context.add_operation( + ValidationOperation::PrecalculatedOperation(balance_fee_result), + ); + + if !is_dry_run && to_identity.is_none() { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_create_transition.base().id(), + format!("Identity {to_user_id} doesn't exist"), + ); + + result.add_error(err); + + return Ok(result); + } + + Ok(result) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs index 42159810ddf..2ef56404c59 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs @@ -1,6 +1,7 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::validation::state_transition::batch::data_triggers::triggers::dpns::v0::create_domain_data_trigger_v0; +use crate::execution::validation::state_transition::batch::data_triggers::triggers::dpns::v1::create_domain_data_trigger_v1; use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; @@ -8,10 +9,11 @@ use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; mod v0; +mod v1; pub fn create_domain_data_trigger( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, ) -> Result { match platform_version @@ -23,10 +25,14 @@ pub fn create_domain_data_trigger( .triggers .create_domain_data_trigger { + // PROTOCOL_VERSION_11 and below: trigger doesn't bill drive reads. 0 => create_domain_data_trigger_v0(document_transition, context, platform_version), + // PROTOCOL_VERSION_12+: trigger bills via add_operation on the + // outer execution_context (threaded through the mutable context). + 1 => create_domain_data_trigger_v1(document_transition, context, platform_version), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "create_domain_data_trigger".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 6aff34de018..0c8e6789653 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -44,10 +44,17 @@ pub const MAX_PRINTABLE_DOMAIN_NAME_LENGTH: usize = 253; /// # Returns /// /// A `DataTriggerExecutionResult` indicating the success or failure of the trigger execution. +// PROTOCOL_VERSION_11 consensus-safety: the body of this function is +// byte-identical to v3.1-dev. The only signature change is the +// `context` parameter: pre-PR `&DataTriggerExecutionContext`, now +// `&mut DataTriggerExecutionContext` (required by the new +// `DataTrigger` fn type that `_v1` triggers need). The body never +// mutates the context (no `add_operation` calls, only reads via +// `in_dry_run()` and field accesses), so PV11 behavior is identical. #[inline(always)] pub(super) fn create_domain_data_trigger_v0( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, ) -> Result { let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); @@ -439,10 +446,12 @@ mod test { transition_execution_context.enable_dry_run(); - let data_trigger_context = DataTriggerExecutionContext { + let trigger_block_info = BlockInfo::default(); + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, + block_info: &trigger_block_info, owner_id: &owner_id, - state_transition_execution_context: &transition_execution_context, + state_transition_execution_context: &mut transition_execution_context, transaction: None, }; @@ -451,10 +460,22 @@ mod test { document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dpns_contract_fixture(platform_version.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"); + + // PROTOCOL_VERSION_11 byte-identity assertion: _v0 must NOT add + // any operations to the execution_context. If a future refactor + // accidentally re-introduces billing in _v0, this assertion + // fails and PV11 chain replay would diverge. + assert!( + data_trigger_context + .state_transition_execution_context + .operations_slice() + .is_empty(), + "create_domain_data_trigger_v0 must not add operations (PV11 byte-identity)" + ); assert!(result.is_valid()); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs new file mode 100644 index 00000000000..388db1379a5 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs @@ -0,0 +1,398 @@ +use dpp::consensus::state::data_trigger::data_trigger_condition_error::DataTriggerConditionError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contracts::dpns_contract::v1::document_types::domain::properties::PARENT_DOMAIN_NAME; +/// The `dpns_triggers` module contains data triggers specific to the DPNS data contract. +use dpp::util::hash::hash_double; +use std::collections::BTreeMap; + +use crate::error::execution::ExecutionError; +use crate::error::Error; + +use crate::execution::validation::state_transition::batch::data_triggers::{ + DataTriggerExecutionContext, DataTriggerExecutionResult, +}; +use dpp::document::DocumentV0Getters; +use dpp::platform_value::btreemap_extensions::{BTreeValueMapHelper, BTreeValueMapPathHelper}; +use dpp::platform_value::Value; +use dpp::ProtocolError; +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::DocumentCreateTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use dpp::system_data_contracts::dpns_contract; +use dpp::system_data_contracts::dpns_contract::v1::document_types::domain::properties::{ALLOW_SUBDOMAINS, + DASH_ALIAS_IDENTITY_ID, DASH_UNIQUE_IDENTITY_ID, LABEL, NORMALIZED_LABEL, NORMALIZED_PARENT_DOMAIN_NAME, PREORDER_SALT, RECORDS}; +use dpp::util::strings::convert_to_homograph_safe_chars; +use dpp::version::PlatformVersion; +use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; +use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; + +pub const MAX_PRINTABLE_DOMAIN_NAME_LENGTH: usize = 253; + +/// Creates a data trigger for handling domain documents. +/// +/// The trigger is executed whenever a new domain document is created on the blockchain. +/// It performs various actions depending on the state of the document and the context in which it was created. +/// +/// # Arguments +/// +/// * `document_transition` - A reference to the document transition that triggered the data trigger. +/// * `context` - A reference to the data trigger execution context. +/// * `dpns_contract::OWNER_ID` - An optional identifier for the top-level identity associated with the domain +/// document (if one exists). +/// +/// # Returns +/// +/// A `DataTriggerExecutionResult` indicating the success or failure of the trigger execution. +#[inline(always)] +pub(super) fn create_domain_data_trigger_v1( + document_transition: &DocumentTransitionAction, + context: &mut DataTriggerExecutionContext<'_>, + platform_version: &PlatformVersion, +) -> Result { + let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); + let data_contract = &data_contract_fetch_info.contract; + let is_dry_run = context.state_transition_execution_context.in_dry_run(); + let document_create_transition = match document_transition { + DocumentTransitionAction::CreateAction(d) => d, + _ => { + return Err(Error::Execution(ExecutionError::DataTriggerExecutionError( + format!( + "the Document Transition {} isn't 'CREATE", + document_transition.base().id() + ), + ))) + } + }; + + let data = document_create_transition.data(); + + let owner_id = context.owner_id; + let label = data.get_string(LABEL).map_err(ProtocolError::ValueError)?; + let normalized_label = data + .get_str(NORMALIZED_LABEL) + .map_err(ProtocolError::ValueError)?; + + let parent_domain_name = data + .get_string(PARENT_DOMAIN_NAME) + .map_err(ProtocolError::ValueError)?; + let normalized_parent_domain_name = data + .get_string(NORMALIZED_PARENT_DOMAIN_NAME) + .map_err(ProtocolError::ValueError)?; + + let preorder_salt = data + .get_hash256_bytes(PREORDER_SALT) + .map_err(ProtocolError::ValueError)?; + let records = data + .get(RECORDS) + .ok_or(ExecutionError::DataTriggerExecutionError(format!( + "property '{}' doesn't exist", + RECORDS + )))? + .to_btree_ref_string_map() + .map_err(ProtocolError::ValueError)?; + + let rule_allow_subdomains = data + .get_bool_at_path(ALLOW_SUBDOMAINS) + .map_err(ProtocolError::ValueError)?; + + let full_domain_name = if parent_domain_name.is_empty() { + label.to_string() + } else { + format!("{normalized_label}.{parent_domain_name}") + }; + + let mut result = DataTriggerExecutionResult::default(); + + if !is_dry_run { + if full_domain_name.len() > MAX_PRINTABLE_DOMAIN_NAME_LENGTH { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "Full domain name length can not be more than {} characters long but got {}", + MAX_PRINTABLE_DOMAIN_NAME_LENGTH, + full_domain_name.len() + ), + ); + + result.add_error(err) + } + + if normalized_label != convert_to_homograph_safe_chars(label.as_str()) { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "Normalized label doesn't match label: {} != {}", + normalized_label, label + ), + ); + + result.add_error(err); + } + + if normalized_parent_domain_name + != convert_to_homograph_safe_chars(parent_domain_name.as_str()) + { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "Normalized parent domain name doesn't match parent domain name: {} != {}", + normalized_parent_domain_name, parent_domain_name + ), + ); + + result.add_error(err); + } + + if let Some(id) = records + .get_optional_identifier(DASH_UNIQUE_IDENTITY_ID) + .map_err(ProtocolError::ValueError)? + { + if id != owner_id { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "ownerId {} doesn't match {} {}", + owner_id, DASH_UNIQUE_IDENTITY_ID, id + ), + ); + + result.add_error(err); + } + } + + if let Some(id) = records + .get_optional_identifier(DASH_ALIAS_IDENTITY_ID) + .map_err(ProtocolError::ValueError)? + { + if id != owner_id { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "ownerId {} doesn't match {} {}", + owner_id, DASH_ALIAS_IDENTITY_ID, id + ), + ); + + result.add_error(err); + } + } + + if normalized_parent_domain_name.is_empty() && context.owner_id != &dpns_contract::OWNER_ID + { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + "Can't create top level domain for this identity".to_string(), + ); + + result.add_error(err); + } + } + + if !normalized_parent_domain_name.is_empty() { + //? What is the `normalized_parent_name`. Are we sure the content is a valid dot-separated data + let mut parent_domain_segments = normalized_parent_domain_name.split('.'); + let parent_domain_label = parent_domain_segments.next().unwrap().to_string(); + let grand_parent_domain_name = parent_domain_segments.collect::>().join("."); + + let document_type = data_contract.document_type_for_name( + document_create_transition + .base() + .document_type_name() + .as_str(), + )?; + + let drive_query = DriveDocumentQuery { + contract: data_contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: None, + in_clause: None, + range_clause: None, + equal_clauses: BTreeMap::from([ + ( + "normalizedParentDomainName".to_string(), + WhereClause { + field: "normalizedParentDomainName".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(grand_parent_domain_name), + }, + ), + ( + "normalizedLabel".to_string(), + WhereClause { + field: "normalizedLabel".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(parent_domain_label), + }, + ), + ]), + }, + offset: None, + limit: None, + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // Diff vs `_v0` (parent-domain query): + // + // _v0: `.query_documents(drive_query, None, is_dry_run, ...)` + // — `epoch=None` short-circuits the cost computation to + // 0 inside `query_documents_v0`. The outcome's documents + // are used; the cost is implicitly zero and discarded. + // Net effect: trigger does the read but never charges + // the user for it. + // + // _v1: `.query_documents(drive_query, Some(epoch), ...)` and + // immediately `add_operation(PrecalculatedOperation(...))` + // with the real cost. The user now pays for the trigger's + // grovedb read on the outer execution_context. + // + // Why the change: DPNS subdomain registrations were a free DoS + // surface on PROTOCOL_VERSION_11 because the trigger's + // parent-domain lookup ran on the chain but the user paid + // nothing for it. + let parent_domain_outcome = context.platform.drive.query_documents( + drive_query, + Some(&context.block_info.epoch), + is_dry_run, + context.transaction, + Some(platform_version.protocol_version), + )?; + context.state_transition_execution_context.add_operation( + crate::execution::types::execution_operation::ValidationOperation::PrecalculatedOperation( + dpp::fee::fee_result::FeeResult { + processing_fee: parent_domain_outcome.cost(), + ..Default::default() + }, + ), + ); + let documents = parent_domain_outcome.documents_owned(); + + if !is_dry_run { + if documents.is_empty() { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + "Parent domain is not present".to_string(), + ); + + result.add_error(err); + + return Ok(result); + } + let parent_domain = &documents[0]; + + if rule_allow_subdomains { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + "Allowing subdomains registration is forbidden for this domain".to_string(), + ); + + result.add_error(err); + + return Ok(result); + } + + if (!parent_domain + .properties() + .get_bool_at_path(ALLOW_SUBDOMAINS) + .map_err(ProtocolError::ValueError)?) + && context.owner_id != &parent_domain.owner_id() + { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + "The subdomain can be created only by the parent domain owner".to_string(), + ); + + result.add_error(err); + + return Ok(result); + } + } + } + + let mut salted_domain_buffer: Vec = vec![]; + salted_domain_buffer.extend(preorder_salt); + salted_domain_buffer.extend(full_domain_name.as_bytes()); + + let salted_domain_hash = hash_double(salted_domain_buffer); + + let document_type = data_contract.document_type_for_name("preorder")?; + + let drive_query = DriveDocumentQuery { + contract: data_contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: None, + in_clause: None, + range_clause: None, + equal_clauses: BTreeMap::from([( + "saltedDomainHash".to_string(), + WhereClause { + field: "saltedDomainHash".to_string(), + operator: WhereOperator::Equal, + value: Value::Bytes32(salted_domain_hash), + }, + )]), + }, + offset: None, + limit: None, + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // Diff vs `_v0` (preorder query): same change as above. `_v0` + // passes `epoch=None` (zero cost, discarded); `_v1` passes + // `Some(epoch)` and bills via `add_operation`. Closes T2 — the + // preorder lookup runs on every DPNS domain create (not just + // subdomain), so the unbilled cost compounded faster than T1. + let preorder_outcome = context.platform.drive.query_documents( + drive_query, + Some(&context.block_info.epoch), + is_dry_run, + context.transaction, + Some(platform_version.protocol_version), + )?; + context.state_transition_execution_context.add_operation( + crate::execution::types::execution_operation::ValidationOperation::PrecalculatedOperation( + dpp::fee::fee_result::FeeResult { + processing_fee: preorder_outcome.cost(), + ..Default::default() + }, + ), + ); + let preorder_documents = preorder_outcome.documents_owned(); + + if is_dry_run { + return Ok(result); + } + + if preorder_documents.is_empty() { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "preorderDocument was not found with a salted domain hash of {}", + hex::encode(salted_domain_hash) + ), + ); + result.add_error(err) + } + + Ok(result) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs index 7c540836cad..a626e8e6908 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs @@ -11,7 +11,7 @@ mod v0; pub fn reject_data_trigger( document_transition: &DocumentTransitionAction, - _context: &DataTriggerExecutionContext<'_>, + _context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, ) -> Result { match platform_version @@ -23,6 +23,7 @@ pub fn reject_data_trigger( .triggers .reject_data_trigger { + // Reject performs no drive reads — never bills. 0 => reject_data_trigger_v0(document_transition), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "reject_data_trigger".to_string(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs index 967adaba8c8..55886df78f7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs @@ -6,12 +6,14 @@ use crate::execution::validation::state_transition::batch::data_triggers::{ use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; use dpp::version::PlatformVersion; use crate::execution::validation::state_transition::batch::data_triggers::triggers::withdrawals::v0::delete_withdrawal_data_trigger_v0; +use crate::execution::validation::state_transition::batch::data_triggers::triggers::withdrawals::v1::delete_withdrawal_data_trigger_v1; mod v0; +mod v1; pub fn delete_withdrawal_data_trigger( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, ) -> Result { match platform_version @@ -24,9 +26,10 @@ pub fn delete_withdrawal_data_trigger( .delete_withdrawal_data_trigger { 0 => delete_withdrawal_data_trigger_v0(document_transition, context, platform_version), + 1 => delete_withdrawal_data_trigger_v1(document_transition, context, platform_version), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "delete_withdrawal_data_trigger".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index 791ebaeb5fe..32115dcd341 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -33,10 +33,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 delete_withdrawal_data_trigger_v0( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, ) -> Result { let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); @@ -141,7 +144,9 @@ mod tests { use dpp::version::PlatformVersion; use drive::util::object_size_info::DocumentInfo::DocumentRefInfo; use drive::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; - use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; + use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, + }; use dpp::withdrawal::Pooling; use drive::drive::contract::DataContractFetchInfo; use crate::execution::types::state_transition_execution_context::v0::StateTransitionExecutionContextV0; @@ -160,7 +165,7 @@ mod tests { }; let platform_version = state_read_guard.current_platform_version().unwrap(); - let transition_execution_context = StateTransitionExecutionContextV0::default(); + let mut transition_execution_context = StateTransitionExecutionContextV0::default(); let data_contract = get_data_contract_fixture(None, 0, platform_version.protocol_version) .data_contract_owned(); let owner_id = data_contract.owner_id(); @@ -181,18 +186,20 @@ mod tests { .into(); let document_transition = DocumentTransitionAction::DeleteAction(delete_transition); - let data_trigger_context = DataTriggerExecutionContext { + let mut state_transition_execution_context_outer = + StateTransitionExecutionContext::V0(transition_execution_context); + let trigger_block_info = BlockInfo::default(); + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, + block_info: &trigger_block_info, owner_id: &owner_id, - state_transition_execution_context: &StateTransitionExecutionContext::V0( - transition_execution_context, - ), + state_transition_execution_context: &mut state_transition_execution_context_outer, transaction: None, }; let result = delete_withdrawal_data_trigger_v0( &document_transition, - &data_trigger_context, + &mut data_trigger_context, platform_version, ) .expect_err("the execution result should be returned"); @@ -252,7 +259,7 @@ mod tests { config: &platform.config, }; - let transition_execution_context = + let mut transition_execution_context = StateTransitionExecutionContext::V0(StateTransitionExecutionContextV0::default()); let platform_version = state_read_guard @@ -320,15 +327,17 @@ mod tests { }), ); - let data_trigger_context = DataTriggerExecutionContext { + let trigger_block_info = BlockInfo::default(); + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, + block_info: &trigger_block_info, owner_id: &owner_id, - state_transition_execution_context: &transition_execution_context, + state_transition_execution_context: &mut transition_execution_context, transaction: None, }; let result = delete_withdrawal_data_trigger_v0( &document_transition, - &data_trigger_context, + &mut data_trigger_context, platform_version, ) .expect("the execution result should be returned"); @@ -341,5 +350,46 @@ mod tests { error.to_string(), "withdrawal deletion is allowed only for COMPLETE statuses" ); + + // PROTOCOL_VERSION_11 byte-identity assertion: _v0 must NOT add + // any operations to the execution_context. Catches any regression + // that accidentally re-introduces billing in _v0. + assert!( + data_trigger_context + .state_transition_execution_context + .operations_slice() + .is_empty(), + "delete_withdrawal_data_trigger_v0 must not add operations (PV11 byte-identity)" + ); + + // T4 PROTOCOL_VERSION_12+ billing assertion: run the same + // scenario through `_v1` directly and verify it DID add an + // operation. Same fixture, different code path — catches the + // regression where `_v1` drops the `add_operation` call. + let mut transition_execution_context_v1 = + StateTransitionExecutionContext::V0(StateTransitionExecutionContextV0::default()); + let trigger_block_info_v1 = BlockInfo::default(); + let mut data_trigger_context_v1 = DataTriggerExecutionContext { + platform: &platform_ref, + block_info: &trigger_block_info_v1, + owner_id: &owner_id, + state_transition_execution_context: &mut transition_execution_context_v1, + transaction: None, + }; + use super::super::v1::delete_withdrawal_data_trigger_v1; + let result_v1 = delete_withdrawal_data_trigger_v1( + &document_transition, + &mut data_trigger_context_v1, + platform_version, + ) + .expect("the execution result should be returned (v1)"); + assert!(!result_v1.is_valid()); + assert!( + !data_trigger_context_v1 + .state_transition_execution_context + .operations_slice() + .is_empty(), + "T4: delete_withdrawal_data_trigger_v1 must add operations to execution_context" + ); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs new file mode 100644 index 00000000000..5a70fcd1446 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs @@ -0,0 +1,136 @@ +//! PROTOCOL_VERSION_12+ version of the withdrawals delete trigger. +//! +//! Mirrors `delete_withdrawal_data_trigger_v0` but passes `Some(epoch)` +//! to `query_documents` so the real grovedb cost is computed, and +//! bills it via +//! `context.state_transition_execution_context.add_operation(...)` so +//! the user pays for the read. + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; +use crate::execution::validation::state_transition::batch::data_triggers::{ + DataTriggerExecutionContext, DataTriggerExecutionResult, +}; +use dpp::consensus::state::data_trigger::data_trigger_condition_error::DataTriggerConditionError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::document::DocumentV0Getters; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; +use dpp::platform_value::Value; +use dpp::system_data_contracts::withdrawals_contract; +use dpp::system_data_contracts::withdrawals_contract::v1::document_types::withdrawal; +use dpp::version::PlatformVersion; +use dpp::{document, ProtocolError}; +use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; +use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; +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_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use std::collections::BTreeMap; + +#[inline(always)] +pub(super) fn delete_withdrawal_data_trigger_v1( + document_transition: &DocumentTransitionAction, + context: &mut DataTriggerExecutionContext<'_>, + platform_version: &PlatformVersion, +) -> Result { + let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); + let data_contract = &data_contract_fetch_info.contract; + let mut result = DataTriggerExecutionResult::default(); + + let DocumentTransitionAction::DeleteAction(dt_delete) = document_transition else { + return Err(Error::Execution(ExecutionError::DataTriggerExecutionError( + format!( + "the Document Transition {} isn't 'DELETE", + document_transition.base().id() + ), + ))); + }; + + let document_type = data_contract.document_type_for_name(withdrawal::NAME)?; + + let drive_query = DriveDocumentQuery { + contract: data_contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: Some(WhereClause { + field: document::property_names::ID.to_string(), + operator: WhereOperator::Equal, + value: Value::Identifier(dt_delete.base().id().to_buffer()), + }), + in_clause: None, + range_clause: None, + equal_clauses: BTreeMap::default(), + }, + offset: None, + limit: Some(100), + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // Diff vs `_v0` (withdrawal-document lookup): + // + // _v0: `.query_documents(drive_query, None, false, ...)` — + // `epoch=None` short-circuits cost to 0. The outcome's + // documents drive the status-check; the cost is discarded. + // + // _v1: `.query_documents(drive_query, Some(epoch), ...)` and + // immediately bill via `add_operation`. + // + // Why the change: every withdrawal-document delete ran a grovedb + // lookup on PV11 that the user didn't pay for. The fetched + // document is epoch-independent so changing `None` to + // `Some(epoch)` only affects the cost field — not the validation + // outcome. + let withdrawals_outcome = context.platform.drive.query_documents( + drive_query, + Some(&context.block_info.epoch), + false, + context.transaction, + Some(platform_version.protocol_version), + )?; + let query_fee = FeeResult { + processing_fee: withdrawals_outcome.cost(), + ..Default::default() + }; + context + .state_transition_execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(query_fee)); + let withdrawals = withdrawals_outcome.documents_owned(); + + let Some(withdrawal) = withdrawals.first() else { + let err = DataTriggerConditionError::new( + data_contract.id(), + dt_delete.base().id(), + "Withdrawal document was not found".to_string(), + ); + + result.add_error(err); + + return Ok(result); + }; + + let status: u8 = withdrawal + .properties() + .get_integer("status") + .map_err(ProtocolError::ValueError)?; + + if status != withdrawals_contract::WithdrawalStatus::COMPLETE as u8 { + let err = DataTriggerConditionError::new( + data_contract.id(), + dt_delete.base().id(), + "withdrawal deletion is allowed only for COMPLETE statuses".to_string(), + ); + + result.add_error(err); + + return Ok(result); + } + + Ok(result) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs index 8c0ba510d5e..c1fef559bd1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs @@ -33,6 +33,7 @@ use crate::rpc::core::CoreRPCLike; use crate::execution::validation::state_transition::batch::advanced_structure::v0::DocumentsBatchStateTransitionStructureValidationV0; use crate::execution::validation::state_transition::batch::identity_contract_nonce::v0::DocumentsBatchStateTransitionIdentityContractNonceV0; use crate::execution::validation::state_transition::batch::state::v0::DocumentsBatchStateTransitionStateValidationV0; +use crate::execution::validation::state_transition::batch::state::v1::DocumentsBatchStateTransitionStateValidationV1; use crate::execution::validation::state_transition::processor::advanced_structure_with_state::StateTransitionStructureKnownInStateValidationV0; use crate::execution::validation::state_transition::processor::basic_structure::StateTransitionBasicStructureValidationV0; use crate::execution::validation::state_transition::processor::identity_nonces::StateTransitionIdentityNonceValidationV0; @@ -62,7 +63,7 @@ impl StateTransitionActionTransformer for BatchTransition { BTreeMap, >, validation_mode: ValidationMode, - _execution_context: &mut StateTransitionExecutionContext, + execution_context: &mut StateTransitionExecutionContext, tx: TransactionArg, ) -> Result, Error> { let platform_version = platform.state.current_platform_version()?; @@ -74,10 +75,24 @@ impl StateTransitionActionTransformer for BatchTransition { .batch_state_transition .transform_into_action { + // PROTOCOL_VERSION_11 and below: legacy `_v0` drops every + // transformer-phase fee_result via a local execution_context. + // Preserved verbatim for chain replay. 0 => self.transform_into_action_v0(&platform.into(), block_info, validation_mode, tx), + // PROTOCOL_VERSION_12+: `_v1` threads the outer execution_context + // into the transformer so per-transition fees accumulated by + // `try_from_borrowed_*_with_contract_lookup` are billed to the + // user instead of being dropped via a local ctx. + 1 => self.transform_into_action_v1( + &platform.into(), + block_info, + validation_mode, + execution_context, + tx, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: transform_into_action".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs index 9a1925de7fc..008be12cc67 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs @@ -1 +1,2 @@ pub(crate) mod v0; +pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.rs deleted file mode 100644 index 554fac68077..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::error::Error; -use crate::execution::validation::state_transition::batch::data_triggers::{ - data_trigger_bindings_list, DataTriggerExecutionContext, DataTriggerExecutionResult, - DataTriggerExecutor, -}; -use dpp::version::PlatformVersion; - -use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; - -#[allow(dead_code)] -#[deprecated(note = "This function is marked as unused.")] -#[allow(deprecated)] -pub(super) fn execute_data_triggers( - document_transition_actions: &Vec, - context: &DataTriggerExecutionContext, - platform_version: &PlatformVersion, -) -> Result { - let data_trigger_bindings = data_trigger_bindings_list(platform_version)?; - - for document_transition_action in document_transition_actions { - let data_trigger_execution_result = document_transition_action - .validate_with_data_triggers(&data_trigger_bindings, context, platform_version)?; - - if !data_trigger_execution_result.is_valid() { - return Ok(data_trigger_execution_result); - } - } - - Ok(DataTriggerExecutionResult::default()) -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index 58583fb5455..f344047cbd0 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -1,23 +1,17 @@ use crate::error::Error; -use crate::platform_types::platform::PlatformStateRef; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; use dpp::block::epoch::Epoch; -use dpp::consensus::basic::document::{DataContractNotPresentError, InvalidDocumentTypeError}; -use dpp::consensus::basic::BasicError; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use std::collections::btree_map::Entry; -use std::collections::BTreeMap; - use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; - -use crate::platform_types::platform_state::PlatformStateV0Methods; use dpp::document::Document; use dpp::fee::fee_result::FeeResult; use dpp::platform_value::{Identifier, Value}; use dpp::state_transition::batch_transition::batched_transition::document_transition::{ DocumentTransition, DocumentTransitionV0Methods, }; -use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; use dpp::validation::ConsensusValidationResult; use dpp::version::PlatformVersion; use drive::drive::document::query::query_contested_documents_storage::QueryContestedDocumentsOutcomeV0Methods; @@ -29,107 +23,133 @@ use drive::query::drive_contested_document_query::{ }; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; -#[allow(dead_code)] -#[deprecated(note = "This function is marked as unused.")] -#[allow(deprecated)] -pub(crate) fn fetch_documents_for_transitions( - platform: &PlatformStateRef, - document_transitions: &[&DocumentTransition], +// ============================================================================ +// fetch_documents_for_transitions_knowing_contract_and_document_type +// ============================================================================ + +/// Versioned facade for `fetch_documents_for_transitions_knowing_contract_and_document_type`. +/// +/// Dispatches on the per-helper version field on +/// `DriveAbciDocumentsStateTransitionValidationVersions`: +/// - v0 (PROTOCOL_VERSION_11 and below) — byte-identical to pre-PR. +/// `epoch` and `execution_context` arguments are unused. No fees billed. +/// - v1 (PROTOCOL_VERSION_12+) — passes `Some(epoch)` to `query_documents` +/// and bills the cost via `execution_context.add_operation`. +#[allow(clippy::too_many_arguments)] +pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + transitions: &[&DocumentTransition], + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result>, Error> { - let mut transitions_by_contracts_and_types: BTreeMap< - (&Identifier, &String), - Vec<&DocumentTransition>, - > = BTreeMap::new(); - - for document_transition in document_transitions { - let document_type = document_transition.base().document_type_name(); - let data_contract_id = document_transition.base().data_contract_id_ref(); - - match transitions_by_contracts_and_types.entry((data_contract_id, document_type)) { - Entry::Vacant(v) => { - v.insert(vec![document_transition]); - } - Entry::Occupied(mut o) => o.get_mut().push(document_transition), - } + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_documents_for_transitions_knowing_contract_and_document_type + { + 0 => fetch_documents_for_transitions_knowing_contract_and_document_type_v0( + drive, + contract, + document_type, + transitions, + transaction, + platform_version, + ), + 1 => fetch_documents_for_transitions_knowing_contract_and_document_type_v1( + drive, + contract, + document_type, + transitions, + epoch, + execution_context, + transaction, + platform_version, + ), + version => Err(Error::Execution( + crate::error::execution::ExecutionError::UnknownVersionMismatch { + method: "fetch_documents_for_transitions_knowing_contract_and_document_type" + .to_string(), + known_versions: vec![0, 1], + received: version, + }, + )), } - - let validation_results_of_documents = transitions_by_contracts_and_types - .into_iter() - .map(|((contract_id, document_type_name), transitions)| { - fetch_documents_for_transitions_knowing_contract_id_and_document_type_name( - platform, - contract_id, - document_type_name, - transitions.as_slice(), - transaction, - platform_version, - ) - }) - .collect::>>, Error>>()?; - - let validation_result = - ConsensusValidationResult::flatten(validation_results_of_documents, platform_version)?; - - Ok(validation_result) } -#[allow(dead_code)] -pub(crate) fn fetch_documents_for_transitions_knowing_contract_id_and_document_type_name( - platform: &PlatformStateRef, - contract_id: &Identifier, - document_type_name: &str, +/// PROTOCOL_VERSION_11 byte-identical implementation. Passes `epoch=None` +/// to `query_documents`, ignores `execution_context`, never bills. +/// Body matches pre-PR (v3.1-dev). +fn fetch_documents_for_transitions_knowing_contract_and_document_type_v0( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, transitions: &[&DocumentTransition], transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result>, Error> { - let drive = platform.drive; - //todo: deal with fee result - //we only want to add to the cache if we are validating in a transaction - let add_to_cache_if_pulled = transaction.is_some(); - let (_, contract_fetch_info) = drive.get_contract_with_fetch_info_and_fee( - contract_id.to_buffer(), - Some(platform.state.last_committed_block_epoch_ref()), - add_to_cache_if_pulled, - transaction, - platform_version, - )?; + if transitions.is_empty() { + return Ok(ConsensusValidationResult::new_with_data(vec![])); + } - let Some(contract_fetch_info) = contract_fetch_info else { - return Ok(ConsensusValidationResult::new_with_error( - BasicError::DataContractNotPresentError(DataContractNotPresentError::new(*contract_id)) - .into(), - )); - }; + let ids: Vec = transitions + .iter() + .map(|dt| Value::Identifier(dt.get_id().to_buffer())) + .collect(); - let Some(document_type) = contract_fetch_info - .contract - .document_type_optional_for_name(document_type_name) - else { - return Ok(ConsensusValidationResult::new_with_error( - BasicError::InvalidDocumentTypeError(InvalidDocumentTypeError::new( - document_type_name.to_string(), - *contract_id, - )) - .into(), - )); - }; - fetch_documents_for_transitions_knowing_contract_and_document_type( - drive, - &contract_fetch_info.contract, + let drive_query = DriveDocumentQuery { + contract, document_type, - transitions, + internal_clauses: InternalClauses { + primary_key_in_clause: Some(WhereClause { + field: "$id".to_string(), + operator: WhereOperator::In, + value: Value::Array(ids), + }), + primary_key_equal_clause: None, + in_clause: None, + range_clause: None, + equal_clauses: Default::default(), + }, + offset: None, + limit: Some(transitions.len() as u16), + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // todo: deal with cost of this operation + let documents_outcome = drive.query_documents( + drive_query, + None, + false, transaction, - platform_version, - ) + Some(platform_version.protocol_version), + )?; + + Ok(ConsensusValidationResult::new_with_data( + documents_outcome.documents_owned(), + )) } -pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type( +/// PROTOCOL_VERSION_12+ implementation. Passes `Some(epoch)` to +/// `query_documents` so the real grovedb cost is computed; bills it via +/// `execution_context.add_operation`. Documents returned are +/// epoch-independent — same as v0. +#[allow(clippy::too_many_arguments)] +fn fetch_documents_for_transitions_knowing_contract_and_document_type_v1( drive: &Drive, contract: &DataContract, document_type: DocumentTypeRef, transitions: &[&DocumentTransition], + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result>, Error> { @@ -164,21 +184,102 @@ pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type block_time_ms: None, }; - // todo: deal with cost of this operation + // Diff vs `_v0`: epoch is `Some(...)` and the cost is billed via + // add_operation on the outer execution_context. let documents_outcome = drive.query_documents( drive_query, - None, + Some(epoch), false, transaction, Some(platform_version.protocol_version), )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { + storage_fee: 0, + processing_fee: documents_outcome.cost(), + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + })); Ok(ConsensusValidationResult::new_with_data( documents_outcome.documents_owned(), )) } +// ============================================================================ +// fetch_document_with_id +// ============================================================================ + +/// Versioned facade for `fetch_document_with_id`. +/// +/// Dispatches on the per-helper version field on +/// `DriveAbciDocumentsStateTransitionValidationVersions`: +/// - v0 (PROTOCOL_VERSION_11 and below) — byte-identical to pre-PR. +/// Calls `_v0` (returns `(Option, FeeResult)` with a zero-cost +/// FeeResult), adds the zero-fee FeeResult to `execution_context` via +/// `add_operation` — matching the pre-PR caller's behavior exactly. +/// - v1 (PROTOCOL_VERSION_12+) — calls `_v1` which passes `Some(epoch)` +/// for the real grovedb cost and bills internally. +#[allow(clippy::too_many_arguments)] pub(crate) fn fetch_document_with_id( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + id: Identifier, + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result, Error> { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_document_with_id + { + 0 => { + // Preserve pre-PR caller semantics: the caller used to call + // `add_operation(PrecalculatedOperation(fee_result))` with + // the (always-zero) FeeResult returned by the old fn. We + // emulate that here so the operations_slice on v0 has the + // same shape as pre-PR. + let (document, fee_result) = fetch_document_with_id_v0( + drive, + contract, + document_type, + id, + transaction, + platform_version, + )?; + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + Ok(document) + } + 1 => fetch_document_with_id_v1( + drive, + contract, + document_type, + id, + epoch, + execution_context, + transaction, + platform_version, + ), + version => Err(Error::Execution( + crate::error::execution::ExecutionError::UnknownVersionMismatch { + method: "fetch_document_with_id".to_string(), + known_versions: vec![0, 1], + received: version, + }, + )), + } +} + +/// PROTOCOL_VERSION_11 byte-identical implementation. Passes `epoch=None` +/// to `query_documents` (cost hard-coded to 0). Returns `(Option, +/// FeeResult)` with `processing_fee=0` — matches pre-PR (v3.1-dev) signature +/// and behavior exactly. +fn fetch_document_with_id_v0( drive: &Drive, contract: &DataContract, document_type: DocumentTypeRef, @@ -233,6 +334,72 @@ pub(crate) fn fetch_document_with_id( } } +/// PROTOCOL_VERSION_12+ implementation. Passes `Some(epoch)` to +/// `query_documents` for the real grovedb cost; bills it via +/// `execution_context.add_operation`. Document returned is +/// epoch-independent — same as v0. +#[allow(clippy::too_many_arguments)] +fn fetch_document_with_id_v1( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + id: Identifier, + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result, Error> { + let drive_query = DriveDocumentQuery { + contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: Some(WhereClause { + field: "$id".to_string(), + operator: WhereOperator::Equal, + value: Value::Identifier(id.to_buffer()), + }), + in_clause: None, + range_clause: None, + equal_clauses: Default::default(), + }, + offset: None, + limit: Some(1), + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // Diff vs `_v0`: epoch is `Some(...)` and the cost is billed via + // add_operation on the outer execution_context. + let documents_outcome = drive.query_documents( + drive_query, + Some(epoch), + false, + transaction, + Some(platform_version.protocol_version), + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { + storage_fee: 0, + processing_fee: documents_outcome.cost(), + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + })); + + let mut documents = documents_outcome.documents_owned(); + + if documents.is_empty() { + Ok(None) + } else { + Ok(Some(documents.remove(0))) + } +} + +// ============================================================================ +// has_contested_document_with_document_id — unchanged +// ============================================================================ + pub(crate) fn has_contested_document_with_document_id<'a>( drive: &Drive, contract: &'a DataContract, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 5c75b5e3c6b..01299360d44 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -38,7 +38,6 @@ use crate::execution::validation::state_transition::state_transitions::batch::tr use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform_state::PlatformStateV0Methods; -mod data_triggers; pub mod fetch_contender; pub mod fetch_documents; @@ -75,9 +74,6 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { ) -> Result, Error> { let mut validation_result = ConsensusValidationResult::::new(); - let state_transition_execution_context = - StateTransitionExecutionContext::default_for_platform_version(platform_version)?; - let owner_id = state_transition_action.owner_id(); let mut validated_transitions = vec![]; @@ -271,17 +267,46 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { )); } else if platform.config.execution.use_document_triggers { if let BatchedTransitionAction::DocumentAction(document_transition) = &transition { - // we should also validate document triggers - let data_trigger_execution_context = DataTriggerExecutionContext { + // Pre-PR this site allocated a default-initialized local + // `StateTransitionExecutionContext` and passed `&local` to + // the trigger context. The local was dropped on return and + // all of its add_operation calls were silently discarded. + // `_v1` triggers need to actually bill (call add_operation), + // so the trigger context now references the OUTER mutable + // execution_context that the processor threaded in. + // + // PROTOCOL_VERSION_11 consensus-safety: on PV11 the + // per-trigger version fields stay at 0, so wrappers + // dispatch to `_v0` triggers whose bodies are + // byte-identical to v3.1-dev (only their param signature + // gained `&mut`, the body never mutates). _v0 triggers + // do not call `add_operation`, so the outer + // execution_context is read-only from the trigger's + // perspective on PV11 — same chain state as pre-PR. + // + // Non-consensus side-effect on PV11 mempool: the trigger + // now sees the outer ctx's real `dry_run` flag instead of + // the previous-default `false`. During CheckTx with + // `dry_run: true`, _v0 triggers short-circuit their + // `query_documents` (via `query_documents_v0`'s internal + // dry-run guard) and skip the post-query validation. + // Doesn't affect block validation (Validator mode is + // always `dry_run: false`), so chain replay matches + // pre-PR byte-for-byte. Pre-PR also did the query but + // ignored its result during dry-run validation, so the + // net mempool outcome is the same. + let owner_id_value = self.owner_id(); + let mut data_trigger_execution_context = DataTriggerExecutionContext { platform, + block_info, transaction, - owner_id: &self.owner_id(), - state_transition_execution_context: &state_transition_execution_context, + owner_id: &owner_id_value, + state_transition_execution_context: execution_context, }; let data_trigger_execution_result = document_transition .validate_with_data_triggers( &data_trigger_bindings, - &data_trigger_execution_context, + &mut data_trigger_execution_context, platform_version, )?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs new file mode 100644 index 00000000000..c375e3bbb53 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs @@ -0,0 +1,83 @@ +use dpp::block::block_info::BlockInfo; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::batch_transition::BatchTransition; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::StateTransitionAction; + +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::state_transitions::batch::transformer::v0::BatchTransitionTransformerV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; + +/// PROTOCOL_VERSION_12+: like v0, but threads the caller's +/// `execution_context` into `try_into_action_v0` so per-transition +/// fee_results accumulated by the transformer +/// (`try_from_borrowed_*_with_contract_lookup`) are billed to the user +/// instead of being dropped via a local ctx. +/// +/// The transformer body (`try_into_action_v0` and all helpers in +/// `transformer/v0/mod.rs`) is intentionally still at `_v0` per the +/// file-header comment there — both `transform_into_action_v0` and this +/// `_v1` wrapper share the same single transformer entry point. +pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionStateValidationV1 +{ + fn transform_into_action_v1( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + ) -> Result, Error>; +} + +impl DocumentsBatchStateTransitionStateValidationV1 for BatchTransition { + fn transform_into_action_v1( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + ) -> Result, Error> { + // Diff vs `_v0`: + // + // _v0: creates a `StateTransitionExecutionContext::default(...)` + // local at the top of the fn and passes `&mut local` to + // `try_into_action_v0`. Every per-transition fee_result + // accumulated inside the transformer + // (`try_from_borrowed_*_with_contract_lookup`) lands in + // the local, which is dropped on return. The user pays + // nothing for the transformer-phase reads. + // + // _v1: skips the local allocation. Passes the OUTER + // `execution_context` (threaded down by the processor) + // directly into `try_into_action_v0`. Per-transition + // fee_results now reach the user's bill via the existing + // `ValidationOperation::add_many_to_fee_result` plumbing + // in `execute_event/v0/mod.rs`. + // + // Why the change: closes the dominant transformer-phase fee + // leak. Token group action confirmer fees, for example, were + // silently dropped on PV11 because the local-ctx-drop + // swallowed the 3 grovedb reads (~30K credits) + // `try_from_borrowed_base_transition_with_contract_lookup` + // performs. + // + // The transformer body itself (`try_into_action_v0` and its + // helpers in `transformer/v0/mod.rs`) is unchanged — both v0 + // and v1 wrappers call the same function. The only difference + // is which execution_context lives long enough to be read by + // the fee-accumulation step downstream. + let validation_result = self.try_into_action_v0( + platform, + block_info, + validation_mode.should_validate_batch_valid_against_state(), + tx, + execution_context, + )?; + + Ok(validation_result.map(Into::into)) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs index f48ffe27f0d..beacc1093a3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs @@ -8,9 +8,38 @@ mod deletion_tests { #[tokio::test] async fn test_document_delete_on_document_type_that_is_mutable_and_can_be_deleted() { - let platform_version = PlatformVersion::latest(); + run_document_delete_on_document_type_that_is_mutable_and_can_be_deleted_at_protocol_version( + PlatformVersion::latest().protocol_version, + 1678920, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B7 fee — the transformer's local execution + /// context was dropped, so the user wasn't charged for the per-transition + /// grovedb reads `try_from_borrowed_*_with_contract_lookup` performs. + /// Pinned so v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_delete_on_document_type_that_is_mutable_and_can_be_deleted_protocol_version_11( + ) { + run_document_delete_on_document_type_that_is_mutable_and_can_be_deleted_at_protocol_version( + 11, 1666860, + ) + .await; + } + + /// Helper for the paired happy-path delete fee test. Same scenario is + /// exercised at PROTOCOL_VERSION_11 (transformer reads dropped) and at + /// PROTOCOL_VERSION_12+ (transformer reads billed via outer execution + /// context). + async fn run_document_delete_on_document_type_that_is_mutable_and_can_be_deleted_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -143,7 +172,12 @@ mod deletion_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 1666860); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: happy-path delete processing fee must match the version-specific baseline", + protocol_version, + ); let issues = platform .drive @@ -329,16 +363,38 @@ mod deletion_tests { #[tokio::test] async fn test_document_delete_on_document_type_that_is_not_mutable_and_can_be_deleted() { + run_document_delete_on_document_type_that_is_not_mutable_and_can_be_deleted_at_protocol_version( + PlatformVersion::latest().protocol_version, + 2778700, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B7 fee — see sibling docs above. Pinned so + /// v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_delete_on_document_type_that_is_not_mutable_and_can_be_deleted_protocol_version_11( + ) { + run_document_delete_on_document_type_that_is_not_mutable_and_can_be_deleted_at_protocol_version( + 11, 2762400, + ) + .await; + } + + async fn run_document_delete_on_document_type_that_is_not_mutable_and_can_be_deleted_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure(); let contract_path = "tests/supporting_files/contract/dashpay/dashpay-contract-contact-request-not-mutable-and-can-be-deleted.json"; let platform_state = platform.state.load(); - let platform_version = platform_state - .current_platform_version() - .expect("expected to get current platform version"); + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); // let's construct the grovedb structure for the card game data contract let dashpay_contract = json_document_to_contract(contract_path, true, platform_version) @@ -488,7 +544,12 @@ mod deletion_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2762400); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: happy-path delete (non-mutable doc) processing fee must match the version-specific baseline", + protocol_version, + ); let issues = platform .drive @@ -657,9 +718,33 @@ mod deletion_tests { #[tokio::test] async fn test_document_delete_that_does_not_yet_exist() { - let platform_version = PlatformVersion::latest(); + run_document_delete_that_does_not_yet_exist_at_protocol_version( + PlatformVersion::latest().protocol_version, + 520340, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B7 fee — the transformer's local execution + /// context was dropped, so the per-transition grovedb reads + /// `try_from_borrowed_*_with_contract_lookup` performs were not billed. + /// Pinned so v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_delete_that_does_not_yet_exist_protocol_version_11() { + run_document_delete_that_does_not_yet_exist_at_protocol_version(11, 516040).await; + } + + /// Helper for the paired delete-that-does-not-yet-exist fee test. + /// PROTOCOL_VERSION_11 yields the pre-fix bump-only fee (transformer + /// reads dropped); PROTOCOL_VERSION_12+ adds the reads. + async fn run_document_delete_that_does_not_yet_exist_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -744,7 +829,12 @@ mod deletion_tests { assert_eq!(processing_result.valid_count(), 0); - assert_eq!(processing_result.aggregated_fees().processing_fee, 516040); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: delete-that-does-not-yet-exist processing fee must match the version-specific baseline", + protocol_version, + ); } #[tokio::test] async fn test_document_deletion_that_needs_a_token() { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs index 36433c4df60..51afcb0fd7d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs @@ -21,9 +21,38 @@ mod dpns_tests { #[tokio::test] async fn test_dpns_contract_references_with_no_contested_unique_index() { - let platform_version = PlatformVersion::latest(); + run_dpns_contract_references_with_no_contested_unique_index_at_protocol_version( + PlatformVersion::latest().protocol_version, + 6_010_380, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-T1/T2 fee — `create_domain_data_trigger_v0` + /// runs the same parent-domain + preorder queries but discards their + /// cost (epoch=None → `query_documents` cost short-circuits to 0, + /// trigger context's add_operation never called on v0). Pinned so + /// v11 chain history stays bit-for-bit reproducible. + /// + /// Delta vs PV12: 6_010_380 - 5_978_080 = 32_300 credits = T1 + T2 + /// query costs across 3 subdomain creates (~10,767 per transition, + /// or ~5,383 per query). + #[tokio::test] + async fn test_dpns_contract_references_with_no_contested_unique_index_protocol_version_11() { + run_dpns_contract_references_with_no_contested_unique_index_at_protocol_version( + 11, 5_978_080, + ) + .await; + } + + async fn run_dpns_contract_references_with_no_contested_unique_index_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -385,6 +414,18 @@ mod dpns_tests { assert_eq!(processing_result.valid_count(), 3); + // T1/T2 regression pin: the DPNS `create_domain_data_trigger` + // runs two `query_documents` calls per transition (parent-domain + // + preorder). On PV12+ (`transform_into_action: 1`) the + // accumulated cost is billed via the trigger's returned + // `FeeResult`. On PV11 the cost is discarded. + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: DPNS domain create fee must match the version-specific baseline (T1 parent-domain + T2 preorder query costs billed only at PV12+)", + protocol_version, + ); + let mut order_by = IndexMap::new(); order_by.insert( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs index 9498b00ed25..02ec456bb21 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs @@ -168,8 +168,28 @@ mod nft_tests { #[tokio::test] async fn test_document_set_price() { - let platform_version = PlatformVersion::latest(); + run_document_set_price_at_protocol_version( + PlatformVersion::latest().protocol_version, + 2485600, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4 fee — query_documents cost was discarded. + /// Pinned so v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_set_price_protocol_version_11() { + run_document_set_price_at_protocol_version(11, 2473880).await; + } + + async fn run_document_set_price_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let (mut platform, contract) = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure() .with_crypto_card_game_nft(TradeMode::DirectPurchase); @@ -346,7 +366,12 @@ mod nft_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2473880); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: set-price processing fee must match the version-specific baseline", + protocol_version, + ); let query_sender_results = platform .drive @@ -379,8 +404,36 @@ mod nft_tests { #[tokio::test] async fn test_document_set_price_and_purchase() { - let platform_version = PlatformVersion::latest(); + run_document_set_price_and_purchase_at_protocol_version( + PlatformVersion::latest().protocol_version, + 126440160, + 2485600, + 4092360, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4/B7 fees — query_documents cost was + /// discarded on the set-price and purchase transitions, and the + /// transformer-phase grovedb reads on the prior create transition were + /// dropped into a local context. Pinned so v11 chain history stays + /// bit-for-bit reproducible. + #[tokio::test] + async fn test_document_set_price_and_purchase_protocol_version_11() { + run_document_set_price_and_purchase_at_protocol_version(11, 126435860, 2473880, 4080480) + .await; + } + + async fn run_document_set_price_and_purchase_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + original_creation_cost: dpp::fee::Credits, + expected_set_price_fee: dpp::fee::Credits, + expected_purchase_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let (mut platform, contract) = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure() .with_crypto_card_game_nft(TradeMode::DirectPurchase); @@ -469,12 +522,10 @@ mod nft_tests { .change(), &BalanceChange::RemoveFromBalance { required_removed_balance: 123579000, - desired_removed_balance: 126435860, + desired_removed_balance: original_creation_cost, } ); - let original_creation_cost = 126435860; - platform .drive .grove @@ -602,7 +653,12 @@ mod nft_tests { None ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2473880); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_set_price_fee, + "PROTOCOL_VERSION_{}: set-price processing fee must match the version-specific baseline", + protocol_version, + ); let seller_balance = platform .drive @@ -613,7 +669,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.1) - original_creation_cost - 2689880 + dash_to_credits!(0.1) - original_creation_cost - expected_set_price_fee - 216000 ); let query_sender_results = platform @@ -709,7 +765,12 @@ mod nft_tests { assert_eq!(processing_result.aggregated_fees().storage_fee, 64611000); - assert_eq!(processing_result.aggregated_fees().processing_fee, 4080480); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_purchase_fee, + "PROTOCOL_VERSION_{}: purchase processing fee must match the version-specific baseline", + protocol_version, + ); assert_eq!( processing_result @@ -743,7 +804,9 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.2) - original_creation_cost + 20014623 + dash_to_credits!(0.2) - original_creation_cost + 22704503 + - expected_set_price_fee + - 216000 ); let buyers_balance = platform @@ -753,13 +816,49 @@ mod nft_tests { .expect("expected that purchaser exists"); // the buyer paid 0.1, but also storage and processing fees - assert_eq!(buyers_balance, dash_to_credits!(0.9) - 68691480); + assert_eq!( + buyers_balance, + dash_to_credits!(0.9) - expected_purchase_fee - 64611000 + ); } #[tokio::test] async fn test_document_set_price_and_purchase_different_epoch_documents_mutable() { - let platform_version = PlatformVersion::latest(); + run_document_set_price_and_purchase_different_epoch_documents_mutable_at_protocol_version( + PlatformVersion::latest().protocol_version, + 141238960, + 2729120, + 2733160, + 4357440, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4/B7 fees — query_documents cost was + /// discarded on each set-price and the purchase transition, and the + /// transformer-phase grovedb reads on the prior create transition were + /// dropped into a local context. Pinned so v11 chain history stays + /// bit-for-bit reproducible. + #[tokio::test] + async fn test_document_set_price_and_purchase_different_epoch_documents_mutable_protocol_version_11( + ) { + run_document_set_price_and_purchase_different_epoch_documents_mutable_at_protocol_version( + 11, 141234660, 2717400, 2721160, 4345280, + ) + .await; + } + + async fn run_document_set_price_and_purchase_different_epoch_documents_mutable_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + original_creation_cost: dpp::fee::Credits, + expected_set_price_fee_1: dpp::fee::Credits, + expected_set_price_fee_2: dpp::fee::Credits, + expected_purchase_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure(); @@ -864,12 +963,10 @@ mod nft_tests { .change(), &BalanceChange::RemoveFromBalance { required_removed_balance: 138159000, - desired_removed_balance: 141234660, + desired_removed_balance: original_creation_cost, } ); - let original_creation_cost = 141234660; - platform .drive .grove @@ -1017,7 +1114,12 @@ mod nft_tests { None ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2717400); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_set_price_fee_1, + "PROTOCOL_VERSION_{}: first set-price processing fee must match the version-specific baseline", + protocol_version, + ); let seller_balance = platform .drive @@ -1028,7 +1130,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.1) - original_creation_cost - 2717400 - 378000 + dash_to_credits!(0.1) - original_creation_cost - expected_set_price_fee_1 - 378000 ); // now let's update price, but first go to next epoch @@ -1106,7 +1208,12 @@ mod nft_tests { None ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2721160); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_set_price_fee_2, + "PROTOCOL_VERSION_{}: second set-price processing fee must match the version-specific baseline", + protocol_version, + ); let seller_balance = platform .drive @@ -1117,7 +1224,12 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.1) - original_creation_cost - 2717400 - 378000 - 2721160 - 216000 + dash_to_credits!(0.1) + - original_creation_cost + - expected_set_price_fee_1 + - 378000 + - expected_set_price_fee_2 + - 216000 ); let query_sender_results = platform @@ -1230,7 +1342,12 @@ mod nft_tests { assert_eq!(processing_result.aggregated_fees().storage_fee, 64611000); - assert_eq!(processing_result.aggregated_fees().processing_fee, 4345280); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_purchase_fee, + "PROTOCOL_VERSION_{}: purchase processing fee must match the version-specific baseline", + protocol_version, + ); assert_eq!( processing_result @@ -1264,7 +1381,11 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.2) - original_creation_cost + 46955162 + dash_to_credits!(0.2) - original_creation_cost + 52987722 + - expected_set_price_fee_1 + - 378000 + - expected_set_price_fee_2 + - 216000 ); let buyers_balance = platform @@ -1274,13 +1395,46 @@ mod nft_tests { .expect("expected that purchaser exists"); // the buyer paid 0.1, but also storage and processing fees - assert_eq!(buyers_balance, dash_to_credits!(0.9) - 68956280); + assert_eq!( + buyers_balance, + dash_to_credits!(0.9) - expected_purchase_fee - 64611000 + ); } #[tokio::test] async fn test_document_set_price_and_purchase_different_epoch() { - let platform_version = PlatformVersion::latest(); + run_document_set_price_and_purchase_different_epoch_at_protocol_version( + PlatformVersion::latest().protocol_version, + 126440160, + 2485600, + 4092360, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4/B7 fees — query_documents cost was + /// discarded on the set-price and purchase transitions, and the + /// transformer-phase grovedb reads on the prior create transition were + /// dropped into a local context. Pinned so v11 chain history stays + /// bit-for-bit reproducible. + #[tokio::test] + async fn test_document_set_price_and_purchase_different_epoch_protocol_version_11() { + run_document_set_price_and_purchase_different_epoch_at_protocol_version( + 11, 126435860, 2473880, 4080480, + ) + .await; + } + + async fn run_document_set_price_and_purchase_different_epoch_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + original_creation_cost: dpp::fee::Credits, + expected_set_price_fee: dpp::fee::Credits, + expected_purchase_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let (mut platform, contract) = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure() .with_crypto_card_game_nft(TradeMode::DirectPurchase); @@ -1369,12 +1523,10 @@ mod nft_tests { .change(), &BalanceChange::RemoveFromBalance { required_removed_balance: 123579000, - desired_removed_balance: 126435860, + desired_removed_balance: original_creation_cost, } ); - let original_creation_cost = 126435860; - platform .drive .grove @@ -1506,7 +1658,12 @@ mod nft_tests { None ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2473880); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_set_price_fee, + "PROTOCOL_VERSION_{}: set-price processing fee must match the version-specific baseline", + protocol_version, + ); let seller_balance = platform .drive @@ -1517,7 +1674,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.1) - original_creation_cost - 2689880 + dash_to_credits!(0.1) - original_creation_cost - expected_set_price_fee - 216000 ); let query_sender_results = platform @@ -1615,7 +1772,12 @@ mod nft_tests { assert_eq!(processing_result.aggregated_fees().storage_fee, 64611000); - assert_eq!(processing_result.aggregated_fees().processing_fee, 4080480); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_purchase_fee, + "PROTOCOL_VERSION_{}: purchase processing fee must match the version-specific baseline", + protocol_version, + ); assert_eq!( processing_result @@ -1649,7 +1811,9 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.2) - original_creation_cost + 20014623 + dash_to_credits!(0.2) - original_creation_cost + 22704503 + - expected_set_price_fee + - 216000 ); let buyers_balance = platform @@ -1659,7 +1823,10 @@ mod nft_tests { .expect("expected that purchaser exists"); // the buyer paid 0.1, but also storage and processing fees - assert_eq!(buyers_balance, dash_to_credits!(0.9) - 68691480); + assert_eq!( + buyers_balance, + dash_to_credits!(0.9) - expected_purchase_fee - 64611000 + ); } /// Helper for the paired Purchase-at-wrong-price test. Same scenario at @@ -2434,8 +2601,32 @@ mod nft_tests { #[tokio::test] async fn test_document_set_price_and_purchase_with_enough_credits_to_buy_but_not_enough_to_pay_for_processing( ) { - let platform_version = PlatformVersion::latest(); + run_document_set_price_and_purchase_with_enough_credits_to_buy_but_not_enough_to_pay_for_processing_at_protocol_version( + PlatformVersion::latest().protocol_version, + 2485600, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4 fee — query_documents cost was discarded. + /// Pinned so v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_set_price_and_purchase_with_enough_credits_to_buy_but_not_enough_to_pay_for_processing_protocol_version_11( + ) { + run_document_set_price_and_purchase_with_enough_credits_to_buy_but_not_enough_to_pay_for_processing_at_protocol_version( + 11, 2473880, + ) + .await; + } + + async fn run_document_set_price_and_purchase_with_enough_credits_to_buy_but_not_enough_to_pay_for_processing_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_set_price_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let (mut platform, contract) = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure() .with_crypto_card_game_nft(TradeMode::DirectPurchase); @@ -2613,7 +2804,12 @@ mod nft_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2473880); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_set_price_fee, + "PROTOCOL_VERSION_{}: set-price processing fee must match the version-specific baseline", + protocol_version, + ); let query_sender_results = platform .drive @@ -2922,7 +3118,7 @@ mod nft_tests { async fn test_document_set_price_on_not_owned_document() { run_document_set_price_on_not_owned_document_at_protocol_version( PlatformVersion::latest().protocol_version, - 571240, + 582960, ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index 8a16213a46b..a126a6ff38a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -11,9 +11,31 @@ mod replacement_tests { #[tokio::test] async fn test_document_replace_on_document_type_that_is_mutable() { - let platform_version = PlatformVersion::latest(); + run_document_replace_on_document_type_that_is_mutable_at_protocol_version( + PlatformVersion::latest().protocol_version, + 1411320, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B7 happy-path fee — transformer's local + /// execution context was dropped, so per-transition grovedb reads + /// were not billed. Pinned so v11 chain history stays bit-for-bit + /// reproducible. + #[tokio::test] + async fn test_document_replace_on_document_type_that_is_mutable_protocol_version_11() { + run_document_replace_on_document_type_that_is_mutable_at_protocol_version(11, 1399260) + .await; + } + + async fn run_document_replace_on_document_type_that_is_mutable_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -146,7 +168,12 @@ mod replacement_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 1399260); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: happy-path replace processing fee must match the version-specific baseline", + protocol_version, + ); let issues = platform .drive @@ -671,7 +698,7 @@ mod replacement_tests { async fn test_document_replace_on_document_type_that_is_not_mutable() { run_document_replace_on_document_type_that_is_not_mutable_at_protocol_version( PlatformVersion::latest().protocol_version, - 445700, + 460920, ) .await; } @@ -917,8 +944,34 @@ mod replacement_tests { #[tokio::test] async fn test_document_replace_on_document_type_that_is_not_mutable_but_is_transferable() { - let platform_version = PlatformVersion::latest(); + run_document_replace_on_document_type_that_is_not_mutable_but_is_transferable_at_protocol_version( + PlatformVersion::latest().protocol_version, + 457660, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B7 bump-only fee (transformer's local + /// execution context dropped the per-transition reads). Pinned so + /// v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_replace_on_document_type_that_is_not_mutable_but_is_transferable_protocol_version_11( + ) { + run_document_replace_on_document_type_that_is_not_mutable_but_is_transferable_at_protocol_version( + 11, + 445700, + ) + .await; + } + + async fn run_document_replace_on_document_type_that_is_not_mutable_but_is_transferable_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let (mut platform, contract) = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure() .with_crypto_card_game_transfer_only(Transferable::Always); @@ -1094,7 +1147,12 @@ mod replacement_tests { assert_eq!(processing_result.valid_count(), 0); - assert_eq!(processing_result.aggregated_fees().processing_fee, 445700); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: paid-error replace processing fee must match the version-specific baseline", + protocol_version, + ); let query_sender_results = platform .drive @@ -1224,13 +1282,14 @@ mod replacement_tests { ); } - /// PROTOCOL_VERSION_12+ — same fee as v11 because the bump emission for - /// this specific path is unconditional (pre-existing legacy behavior). + /// PROTOCOL_VERSION_12+ — bump emission for this specific path is + /// unconditional (pre-existing legacy behavior), but the document + /// query now bills its cost on top of v11's bump-only fee. #[tokio::test] async fn test_document_replace_that_does_not_yet_exist() { run_document_replace_that_does_not_yet_exist_at_protocol_version( PlatformVersion::latest().protocol_version, - 516040, + 520340, ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs index 2df43222264..74cb2289418 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs @@ -7,8 +7,32 @@ mod transfer_tests { #[tokio::test] async fn test_document_transfer_on_document_type_that_is_transferable_that_has_no_owner_indices( + ) { + run_document_transfer_on_document_type_that_is_transferable_that_has_no_owner_indices_at_protocol_version( + PlatformVersion::latest().protocol_version, + 1997120, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B7 happy-path fee. Pinned so v11 chain + /// history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_transfer_on_document_type_that_is_transferable_that_has_no_owner_indices_protocol_version_11( + ) { + run_document_transfer_on_document_type_that_is_transferable_that_has_no_owner_indices_at_protocol_version( + 11, + 1985420, + ) + .await; + } + + async fn run_document_transfer_on_document_type_that_is_transferable_that_has_no_owner_indices_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, ) { let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure(); @@ -160,7 +184,12 @@ mod transfer_tests { assert_eq!(processing_result.aggregated_fees().storage_fee, 0); // There is no storage fee, as there are no indexes that will change - assert_eq!(processing_result.aggregated_fees().processing_fee, 1985420); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: happy-path transfer (no owner indices) processing fee must match the version-specific baseline", + protocol_version, + ); let issues = platform .drive @@ -182,16 +211,48 @@ mod transfer_tests { #[tokio::test] async fn test_document_transfer_on_document_type_that_is_transferable_before_creator_id() { - let platform_version = PlatformVersion::get(9).unwrap(); + run_document_transfer_on_document_type_that_is_transferable_before_creator_id_at_protocol_version( + PlatformVersion::latest().protocol_version, + 3380960, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4 fee — query_documents cost was discarded. + /// Pinned so v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_transfer_on_document_type_that_is_transferable_before_creator_id_protocol_version_11( + ) { + run_document_transfer_on_document_type_that_is_transferable_before_creator_id_at_protocol_version( + 11, + 3369260, + ) + .await; + } + + async fn run_document_transfer_on_document_type_that_is_transferable_before_creator_id_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + // Contract loader uses the PV9 platform version to materialize the + // format-version-0 contract bytes. Everything else — state + // transition decoding, validation, and the batch state transition + // version gate — runs against the runtime `platform_version` + // derived from the parameterized `protocol_version` (controlled by + // `with_initial_protocol_version` below). + let contract_platform_version = PlatformVersion::get(9).unwrap(); + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure(); let card_game_path = "tests/supporting_files/contract/crypto-card-game/crypto-card-game-all-transferable-format-version-0.json"; // let's construct the grovedb structure for the card game data contract - let contract = json_document_to_contract(card_game_path, true, platform_version) + let contract = json_document_to_contract(card_game_path, true, contract_platform_version) .expect("expected to get data contract"); assert!(contract.system_version_type() > 0); @@ -391,7 +452,12 @@ mod transfer_tests { Some(14992395) ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 3369260); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: transfer-before-creator-id processing fee must match the version-specific baseline", + protocol_version, + ); let query_sender_results = platform .drive @@ -428,8 +494,31 @@ mod transfer_tests { #[tokio::test] async fn test_document_transfer_on_document_type_that_is_transferable() { - let platform_version = PlatformVersion::latest(); + run_document_transfer_on_document_type_that_is_transferable_at_protocol_version( + PlatformVersion::latest().protocol_version, + 3643400, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4 fee — query_documents cost was discarded. + /// Pinned so v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_transfer_on_document_type_that_is_transferable_protocol_version_11() { + run_document_transfer_on_document_type_that_is_transferable_at_protocol_version( + 11, 3631040, + ) + .await; + } + + async fn run_document_transfer_on_document_type_that_is_transferable_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let (mut platform, contract) = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure() .with_crypto_card_game_transfer_only(Transferable::Always); @@ -639,7 +728,12 @@ mod transfer_tests { Some(14992395) ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 3631040); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: happy-path transfer (transferable) processing fee must match the version-specific baseline", + protocol_version, + ); let query_sender_results = platform .drive @@ -683,12 +777,43 @@ mod transfer_tests { #[tokio::test] async fn test_document_transfer_on_document_type_that_is_transferable_contract_v0() { + run_document_transfer_on_document_type_that_is_transferable_contract_v0_at_protocol_version( + PlatformVersion::latest().protocol_version, + 3380960, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4 fee — query_documents cost was discarded. + /// Pinned so v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_transfer_on_document_type_that_is_transferable_contract_v0_protocol_version_11( + ) { + run_document_transfer_on_document_type_that_is_transferable_contract_v0_at_protocol_version( + 11, 3369260, + ) + .await; + } + + async fn run_document_transfer_on_document_type_that_is_transferable_contract_v0_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { // With a contract v0 we should not be adding the creator id // We do this because the creator id can not be serialized in document serialization v0 // And document serialization v0 is necessary when the data contract is v0 or the data // contract config is v0. - let platform_version = PlatformVersion::latest(); + // + // The platform_version used for test data + transition encoding + + // dispatch all come from the parameterized `protocol_version` so + // that the runtime validation gate (which uses + // `platform.state.current_platform_version()`) and the data + // serialization stay aligned with the version the test claims to + // exercise. + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure(); @@ -893,7 +1018,12 @@ mod transfer_tests { Some(14992395) ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 3369260); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: transferable contract-v0 processing fee must match the version-specific baseline", + protocol_version, + ); let query_sender_results = platform .drive @@ -930,8 +1060,32 @@ mod transfer_tests { #[tokio::test] async fn test_document_transfer_on_document_type_that_is_not_transferable() { - let platform_version = PlatformVersion::latest(); + run_document_transfer_on_document_type_that_is_not_transferable_at_protocol_version( + PlatformVersion::latest().protocol_version, + 457000, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B7 fee — transformer reads were unbilled. + /// Pinned so v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_transfer_on_document_type_that_is_not_transferable_protocol_version_11() + { + run_document_transfer_on_document_type_that_is_not_transferable_at_protocol_version( + 11, 445700, + ) + .await; + } + + async fn run_document_transfer_on_document_type_that_is_not_transferable_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let (mut platform, contract) = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure() .with_crypto_card_game_transfer_only(Transferable::Never); @@ -1105,7 +1259,12 @@ mod transfer_tests { assert_eq!(processing_result.valid_count(), 0); - assert_eq!(processing_result.aggregated_fees().processing_fee, 445700); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: not-transferable rejection processing fee must match the version-specific baseline", + protocol_version, + ); let query_sender_results = platform .drive @@ -1292,7 +1451,7 @@ mod transfer_tests { async fn test_document_transfer_that_does_not_yet_exist() { run_document_transfer_that_does_not_yet_exist_at_protocol_version( PlatformVersion::latest().protocol_version, - 517400, + 521700, ) .await; } @@ -1306,8 +1465,28 @@ mod transfer_tests { #[tokio::test] async fn test_document_delete_after_transfer() { - let platform_version = PlatformVersion::latest(); + run_document_delete_after_transfer_at_protocol_version( + PlatformVersion::latest().protocol_version, + 4004260, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4 fee — query_documents cost was discarded. + /// Pinned so v11 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_document_delete_after_transfer_protocol_version_11() { + run_document_delete_after_transfer_at_protocol_version(11, 3991900).await; + } + + async fn run_document_delete_after_transfer_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let (mut platform, contract) = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_initial_state_structure() .with_crypto_card_game_transfer_only(Transferable::Always); @@ -1484,7 +1663,12 @@ mod transfer_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 3991900); + assert_eq!( + processing_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: transfer-then-delete (transfer step) processing fee must match the version-specific baseline", + protocol_version, + ); let query_sender_results = platform .drive diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs index b55b7e13523..eaac0f4e5f2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs @@ -3666,4 +3666,223 @@ mod token_burn_tests { .expect("expected to fetch total supply"); assert_eq!(total_supply, Some(103000)); } + + /// Pins the confirmer-step processing fee for a token group burn. + /// + /// The confirmer (action_is_proposer=false) triggers three drive reads + /// inside `try_from_borrowed_base_transition_with_contract_lookup`: + /// `fetch_action_is_closed`, + /// `fetch_action_id_signers_power_and_add_operations`, + /// `fetch_active_action_info_and_add_operations`. Their cost is + /// accumulated into a `FeeResult` and added to the + /// `execution_context`. + /// + /// Under `transform_into_action: 1` (PROTOCOL_VERSION_12+) the outer + /// `execution_context` is threaded through the transformer, so this + /// fee_result reaches the user's bill. Under v0 (PROTOCOL_VERSION_11 + /// and below) the fee_result lands in a dropped local ctx — verified + /// empirically by toggling the version field and re-running this test + /// (see commit message of the version bump for the recorded delta). + #[tokio::test] + async fn test_token_burn_group_action_confirmer_fee_includes_transformer_reads() { + run_token_burn_group_action_confirmer_fee_includes_transformer_reads_at_protocol_version( + PlatformVersion::latest().protocol_version, + 4_319_240, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B7 fee — the transformer's local execution + /// context was dropped, so the three group-action drive reads + /// (fetch_action_is_closed + + /// fetch_action_id_signers_power_and_add_operations + + /// fetch_active_action_info_and_add_operations) cost 30_820 credits + /// that were not billed. Pinned so v11 chain history stays bit-for-bit + /// reproducible. + #[tokio::test] + async fn test_token_burn_group_action_confirmer_fee_includes_transformer_reads_protocol_version_11( + ) { + run_token_burn_group_action_confirmer_fee_includes_transformer_reads_at_protocol_version( + 11, 4_288_420, + ) + .await; + } + + async fn run_token_burn_group_action_confirmer_fee_includes_transformer_reads_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_processing_fee: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + let platform_state = platform.state.load(); + + let (identity1, signer1, key1) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity2, signer2, key2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity1.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration.set_manual_burning_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(0), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + Some( + [( + 0, + Group::V0(GroupV0 { + members: [(identity1.id(), 1), (identity2.id(), 1)].into(), + required_power: 2, + }), + )] + .into(), + ), + None, + platform_version, + ); + + add_tokens_to_identity(&platform, token_id, identity1.id(), 100000); + + // Step 1: identity1 proposes the burn — action_is_proposer=true, so + // `try_from_borrowed_base_transition_with_contract_lookup` skips the + // group-action drive reads (the only path that adds non-empty + // fee_results inside the transformer). + let propose_transition = BatchTransition::new_token_burn_transition( + token_id, + identity1.id(), + contract.id(), + 0, + 100000, + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &key1, + 2, + 0, + &signer1, + platform_version, + None, + ) + .await + .expect("expected to create proposer burn transition"); + + let propose_serialized = propose_transition + .serialize_to_bytes() + .expect("expected to serialize proposer burn"); + + let transaction = platform.drive.grove.start_transaction(); + let proposer_result = platform + .platform + .process_raw_state_transitions( + &[propose_serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process proposer burn"); + assert_eq!(proposer_result.valid_count(), 1); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit proposer burn"); + + // Step 2: identity2 confirms the burn — action_is_proposer=false. + // The confirmer's `try_from_borrowed_base_transition_with_contract_lookup` + // does the three group-action drive reads whose cost we now bill. + let action_id = TokenBurnTransition::calculate_action_id_with_fields( + token_id.as_bytes(), + identity1.id().as_bytes(), + 2, + 100000, + ); + + let confirm_transition = BatchTransition::new_token_burn_transition( + token_id, + identity2.id(), + contract.id(), + 0, + 100000, + None, + Some( + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id, + action_is_proposer: false, + }, + ), + ), + &key2, + 2, + 0, + &signer2, + platform_version, + None, + ) + .await + .expect("expected to create confirmer burn transition"); + + let confirm_serialized = confirm_transition + .serialize_to_bytes() + .expect("expected to serialize confirmer burn"); + + let transaction = platform.drive.grove.start_transaction(); + let confirmer_result = platform + .platform + .process_raw_state_transitions( + &[confirm_serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process confirmer burn"); + assert_eq!(confirmer_result.valid_count(), 1); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit confirmer burn"); + + // Pin the confirmer's fee, which now includes the three + // group-action read costs previously dropped via the local + // execution_context. + // + // Empirical values captured during development: + // * `transform_into_action: 0` (legacy, dropped local ctx): 4_288_420 + // * `transform_into_action: 1` (current, threaded outer ctx): 4_319_240 + // * delta = 30_820 credits = the three transformer-phase reads + // (fetch_action_is_closed + + // fetch_action_id_signers_power_and_add_operations + + // fetch_active_action_info_and_add_operations) that were + // previously billed to a dropped context. + assert_eq!( + confirmer_result.aggregated_fees().processing_fee, + expected_processing_fee, + "PROTOCOL_VERSION_{}: confirmer step processing fee must match the version-specific baseline (transformer-phase group-action reads billed at PV12+, dropped at PV11)", + protocol_version, + ); + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs index 20ec6ed5377..acf2b340287 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs @@ -17,9 +17,30 @@ mod token_selling_tests { #[tokio::test] async fn test_successful_direct_purchase_single_price() { - let platform_version = PlatformVersion::latest(); + run_successful_direct_purchase_single_price_at_protocol_version( + PlatformVersion::latest().protocol_version, + 699_868_122_220, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4/B7 buyer balance — query_documents + + /// transformer-phase reads were dropped, so the buyer paid 7,900 + /// credits less in fees. Pinned so v11 chain history stays + /// bit-for-bit reproducible. + #[tokio::test] + async fn test_successful_direct_purchase_single_price_protocol_version_11() { + run_successful_direct_purchase_single_price_at_protocol_version(11, 699_868_130_120).await; + } + + async fn run_successful_direct_purchase_single_price_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_buyer_credit_balance: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -155,7 +176,12 @@ mod token_selling_tests { .drive .fetch_identity_balance(buyer.id().to_buffer(), None, platform_version) .expect("expected to fetch credit balance"); - assert_eq!(buyer_credit_balance, Some(699_868_130_120)); // 10.0 - 3.0 spent - fees =~ 7 dash left + assert_eq!( + buyer_credit_balance, + Some(expected_buyer_credit_balance), + "PROTOCOL_VERSION_{}: buyer credit balance after direct purchase must match the version-specific baseline (10.0 - 3.0 spent - fees =~ 7 dash left)", + protocol_version, + ); } #[tokio::test] @@ -244,9 +270,34 @@ mod token_selling_tests { #[tokio::test] async fn test_direct_purchase_single_price_not_paying_full_price() { - let platform_version = PlatformVersion::latest(); + run_direct_purchase_single_price_not_paying_full_price_at_protocol_version( + PlatformVersion::latest().protocol_version, + 999_987_864_860, + ) + .await; + } + + /// PROTOCOL_VERSION_11: pre-B4/B7 bump-only buyer balance — under v0 + /// the failed purchase still bumps the nonce but doesn't bill the + /// extra read costs (7,900 credits). Pinned so v11 chain history + /// stays bit-for-bit reproducible. + #[tokio::test] + async fn test_direct_purchase_single_price_not_paying_full_price_protocol_version_11() { + run_direct_purchase_single_price_not_paying_full_price_at_protocol_version( + 11, + 999_987_872_760, + ) + .await; + } + + async fn run_direct_purchase_single_price_not_paying_full_price_at_protocol_version( + protocol_version: dpp::version::ProtocolVersion, + expected_buyer_credit_balance: dpp::fee::Credits, + ) { + let platform_version = PlatformVersion::get(protocol_version) + .expect("expected platform version for the requested protocol_version"); let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -362,7 +413,12 @@ mod token_selling_tests { .drive .fetch_identity_balance(buyer.id().to_buffer(), None, platform_version) .expect("expected to fetch credit balance"); - assert_eq!(buyer_credit_balance, Some(999_987_872_760)); // 10.0 - bump action fees + assert_eq!( + buyer_credit_balance, + Some(expected_buyer_credit_balance), + "PROTOCOL_VERSION_{}: buyer credit balance after failed direct purchase must match the version-specific baseline (10.0 - bump action fees)", + protocol_version, + ); } #[tokio::test] diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index b22235fdb29..8220f9f6a02 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -504,16 +504,25 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { .collect::>(); // We fetch documents only for replace and transfer transitions - // since we need them to create transition actions - // Below we also perform state validation for replace and transfer transitions only - // other transitions are validated in their validate_state functions + // since we need them to create transition actions. Below we also + // perform state validation for replace and transfer transitions + // only; other transitions are validated in their validate_state + // functions. // TODO: Think more about this architecture + // + // PROTOCOL_VERSION_11 consensus-safety: the fetch fn now takes + // `&mut execution_context` and bills the query cost internally + // — but only when `transform_into_action: 1`. On v0 it forces + // `epoch=None` (zero cost) and skips `add_operation`, matching + // pre-PR exactly. let fetched_documents_validation_result = fetch_documents_for_transitions_knowing_contract_and_document_type( platform.drive, data_contract, document_type, replace_and_transfer_transitions.as_slice(), + &block_info.epoch, + execution_context, transaction, platform_version, )?; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 8b3b18edcae..75a72eb680d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -142,6 +142,17 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// /// [`transform_document_transition`]: crate pub failed_per_transition_action: FeatureVersion, + /// Versions the + /// `fetch_documents_for_transitions_knowing_contract_and_document_type` + /// helper. v0 (PROTOCOL_VERSION_11 and below) passes `epoch=None` + /// to `query_documents` and doesn't bill the cost. v1 + /// (PROTOCOL_VERSION_12+) passes `Some(epoch)` and bills via + /// `execution_context.add_operation`. + pub fetch_documents_for_transitions_knowing_contract_and_document_type: FeatureVersion, + /// Versions the `fetch_document_with_id` helper. Same v0 vs v1 + /// semantics as + /// `fetch_documents_for_transitions_knowing_contract_and_document_type`. + pub fetch_document_with_id: FeatureVersion, pub data_triggers: DriveAbciValidationDataTriggerAndBindingVersions, pub is_allowed: FeatureVersion, pub document_create_transition_structure_validation: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index fce75c16330..1ff25e46d71 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -107,6 +107,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index ab2d160f2a3..b50a183fa5e 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -107,6 +107,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index c80ed9f6e0d..db1b6eb9c6f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -107,6 +107,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index a986d603a1a..1731709db20 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -110,6 +110,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index bb9673de70b..825e41c20df 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -111,6 +111,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 21838220e8f..f0f2307635c 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -114,6 +114,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 5e23882714d..eb2518ff40d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -108,6 +108,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index f7e83c01ee8..2194b742f31 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -120,7 +120,30 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = advanced_structure: 0, state: 0, revision: 0, - transform_into_action: 0, + // PROTOCOL_VERSION_12 (v3.1 hard fork): batch state transition + // fee accounting fixes. This single field gates multiple + // related billing changes so they all activate together at + // the same hard fork. On v0 every behavior below is the + // legacy under-billing, preserved verbatim for + // PROTOCOL_VERSION_11 chain replay. + // + // Gated by `transform_into_action: 1`: + // * B7 — outer `execution_context` is threaded through the + // batch transformer (was a dropped local) so per- + // transition fee_results in `try_into_action_v0` are + // billed. + // * B4 — `query_documents` cost in + // `fetch_documents_for_transitions_knowing_contract_and_document_type` + // is added to `execution_context`. + // * B5 — `query_documents` cost in `fetch_document_with_id` + // is added to `execution_context`. + // * T1 — DPNS data trigger parent-domain + // `query_documents` cost. + // * T2 — DPNS data trigger preorder `query_documents` cost. + // * T3 — DashPay data trigger recipient identity-balance + // fetch cost (switched to `fetch_identity_balance_with_costs`). + // * T4 — withdrawals data trigger `query_documents` cost. + transform_into_action: 1, // PROTOCOL_VERSION_12 (v3.1 hard fork): per-transition // failure paths in `transform_document_transition` now emit // a `BumpIdentityDataContractNonce` action so the user pays @@ -128,15 +151,28 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = // ownership/revision check). v0 stays for chain // reproducibility on PROTOCOL_VERSION_11 and below. failed_per_transition_action: 1, + // PROTOCOL_VERSION_12 (v3.1 hard fork): fetch_documents + // helpers bumped to v1 which bill the grovedb cost of + // their query_documents calls. v0 stays for PV11 chain + // replay (the v0 helpers pass epoch=None and never call + // add_operation — byte-identical to pre-PR behavior). + fetch_documents_for_transitions_knowing_contract_and_document_type: 1, + fetch_document_with_id: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { - create_contact_request_data_trigger: 0, - create_domain_data_trigger: 0, + // PROTOCOL_VERSION_12 (v3.1 hard fork): triggers + // that perform drive reads now have `_v1` versions + // that bill the cost via add_operation on the + // outer execution_context. v0 versions remain + // byte-identical to PV11 (don't bill). + create_contact_request_data_trigger: 1, + create_domain_data_trigger: 1, create_identity_data_trigger: 0, create_feature_flag_data_trigger: 0, create_masternode_reward_shares_data_trigger: 0, - delete_withdrawal_data_trigger: 0, + delete_withdrawal_data_trigger: 1, + // Reject does no drive reads — stays at v0. reject_data_trigger: 0, }, },