diff --git a/crates/blockifier/src/execution/entry_point.rs b/crates/blockifier/src/execution/entry_point.rs index f93bfc09a6..07678eb875 100644 --- a/crates/blockifier/src/execution/entry_point.rs +++ b/crates/blockifier/src/execution/entry_point.rs @@ -1,29 +1,24 @@ -use std::cell::RefCell; use std::cmp::min; -use std::sync::Arc; +use std::collections::HashSet; use cairo_vm::vm::runners::cairo_runner::{ ExecutionResources as VmExecutionResources, ResourceTracker, RunResources, }; -use starknet_api::core::{ClassHash, ContractAddress, EntryPointSelector}; +use starknet_api::core::{ClassHash, ContractAddress, EntryPointSelector, EthAddress}; use starknet_api::deprecated_contract_class::EntryPointType; use starknet_api::hash::StarkFelt; -use starknet_api::transaction::{Calldata, TransactionVersion}; +use starknet_api::state::StorageKey; +use starknet_api::transaction::{Calldata, EventContent, Fee, L2ToL1Payload, TransactionVersion}; use crate::abi::abi_utils::selector_from_name; use crate::abi::constants; use crate::block_context::BlockContext; -use crate::execution::call_info::CallInfo; -use crate::execution::common_hints::ExecutionMode; use crate::execution::deprecated_syscalls::hint_processor::SyscallCounter; use crate::execution::errors::{EntryPointExecutionError, PreExecutionError}; use crate::execution::execution_utils::execute_entry_point_call; -use crate::fee::os_resources::OS_RESOURCES; use crate::state::state_api::State; -use crate::transaction::objects::{ - AccountTransactionContext, HasRelatedFeeType, TransactionExecutionResult, -}; -use crate::transaction::transaction_types::TransactionType; +use crate::transaction::errors::TransactionExecutionError; +use crate::transaction::objects::{AccountTransactionContext, TransactionExecutionResult}; #[cfg(test)] #[path = "entry_point_test.rs"] @@ -42,7 +37,7 @@ pub enum CallType { Delegate = 1, } /// Represents a call to an entry point of a StarkNet contract. -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Debug, Clone, Default, Eq, PartialEq)] pub struct CallEntryPoint { // The class hash is not given if it can be deduced from the storage address. pub class_hash: Option, @@ -60,65 +55,6 @@ pub struct CallEntryPoint { pub initial_gas: u64, } -impl CallEntryPoint { - pub fn execute( - mut self, - state: &mut dyn State, - resources: &mut ExecutionResources, - context: &mut EntryPointExecutionContext, - ) -> EntryPointExecutionResult { - let mut decrement_when_dropped = RecursionDepthGuard::new( - context.current_recursion_depth.clone(), - context.max_recursion_depth, - ); - decrement_when_dropped.try_increment_and_check_depth()?; - - // Validate contract is deployed. - let storage_address = self.storage_address; - let storage_class_hash = state.get_class_hash_at(self.storage_address)?; - if storage_class_hash == ClassHash::default() { - return Err(PreExecutionError::UninitializedStorageAddress(self.storage_address).into()); - } - - let class_hash = match self.class_hash { - Some(class_hash) => class_hash, - None => storage_class_hash, // If not given, take the storage contract class hash. - }; - // Hack to prevent version 0 attack on argent accounts. - if context.account_tx_context.version() == TransactionVersion::ZERO - && class_hash - == ClassHash( - StarkFelt::try_from(FAULTY_CLASS_HASH).expect("A class hash must be a felt."), - ) - { - return Err(PreExecutionError::FraudAttempt.into()); - } - // Add class hash to the call, that will appear in the output (call info). - self.class_hash = Some(class_hash); - let contract_class = state.get_compiled_contract_class(&class_hash)?; - - execute_entry_point_call(self, contract_class, state, resources, context).map_err(|error| { - match error { - // On VM error, pack the stack trace into the propagated error. - EntryPointExecutionError::VirtualMachineExecutionError(error) => { - context.error_stack.push((storage_address, error.try_to_vm_trace())); - // TODO(Dori, 1/5/2023): Call error_trace only in the top call; as it is - // right now, each intermediate VM error is wrapped in a - // VirtualMachineExecutionErrorWithTrace error with the stringified trace - // of all errors below it. - // When that's done, remove the 10000 character limitation. - let error_trace = context.error_trace(); - EntryPointExecutionError::VirtualMachineExecutionErrorWithTrace { - trace: error_trace[..min(10000, error_trace.len())].to_string(), - source: error, - } - } - other_error => other_error, - } - }) - } -} - pub struct ConstructorContext { pub class_hash: ClassHash, // Only relevant in deploy syscall. @@ -127,13 +63,13 @@ pub struct ConstructorContext { pub caller_address: ContractAddress, } -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Debug, Clone, Default, Eq, PartialEq)] pub struct ExecutionResources { pub vm_resources: VmExecutionResources, pub syscall_counter: SyscallCounter, } -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub struct EntryPointExecutionContext { pub block_context: BlockContext, pub account_tx_context: AccountTransactionContext, @@ -146,146 +82,80 @@ pub struct EntryPointExecutionContext { /// Used to track error stack for call chain. pub error_stack: Vec<(ContractAddress, String)>, - // Managed by dedicated guard object. - current_recursion_depth: Arc>, + current_recursion_depth: usize, // Maximum depth is limited by the stack size, which is configured at `.cargo/config.toml`. max_recursion_depth: usize, - - // The execution mode affects the behavior of the hint processor. - pub execution_mode: ExecutionMode, } - impl EntryPointExecutionContext { pub fn new( - block_context: &BlockContext, - account_tx_context: &AccountTransactionContext, - mode: ExecutionMode, - limit_steps_by_resources: bool, - ) -> TransactionExecutionResult { - let max_steps = - Self::max_steps(block_context, account_tx_context, &mode, limit_steps_by_resources)?; - Ok(Self { - vm_run_resources: RunResources::new(max_steps), + block_context: BlockContext, + account_tx_context: AccountTransactionContext, + max_n_steps: usize, + ) -> Self { + Self { + vm_run_resources: RunResources::new(max_n_steps), n_emitted_events: 0, n_sent_messages_to_l1: 0, error_stack: vec![], - account_tx_context: account_tx_context.clone(), - current_recursion_depth: Default::default(), + account_tx_context, + current_recursion_depth: 0, max_recursion_depth: block_context.max_recursion_depth, - block_context: block_context.clone(), - execution_mode: mode, - }) + block_context, + } } pub fn new_validate( block_context: &BlockContext, account_tx_context: &AccountTransactionContext, - limit_steps_by_resources: bool, - ) -> TransactionExecutionResult { + ) -> Self { Self::new( - block_context, - account_tx_context, - ExecutionMode::Validate, - limit_steps_by_resources, + block_context.clone(), + account_tx_context.clone(), + block_context.validate_max_n_steps as usize, ) } pub fn new_invoke( block_context: &BlockContext, account_tx_context: &AccountTransactionContext, - limit_steps_by_resources: bool, - ) -> TransactionExecutionResult { + ) -> Self { Self::new( - block_context, - account_tx_context, - ExecutionMode::Execute, - limit_steps_by_resources, + block_context.clone(), + account_tx_context.clone(), + Self::max_invoke_steps(block_context, account_tx_context), ) } - /// Returns the maximum number of cairo steps allowed, given the max fee, gas price and the - /// execution mode. + /// Returns the maximum number of cairo steps allowed, given the max fee and gas price. /// If fee is disabled, returns the global maximum. - fn max_steps( + pub fn max_invoke_steps( block_context: &BlockContext, account_tx_context: &AccountTransactionContext, - mode: &ExecutionMode, - limit_steps_by_resources: bool, - ) -> TransactionExecutionResult { - let block_upper_bound = match mode { - ExecutionMode::Validate => min( - block_context.validate_max_n_steps as usize, - constants::MAX_VALIDATE_STEPS_PER_TX, - ), - ExecutionMode::Execute => { - min(block_context.invoke_tx_max_n_steps as usize, constants::MAX_STEPS_PER_TX) - } - }; - - if !limit_steps_by_resources || !account_tx_context.enforce_fee()? { - return Ok(block_upper_bound); + ) -> usize { + if account_tx_context.max_fee == Fee(0) { + block_context.invoke_tx_max_n_steps as usize + } else { + let gas_per_step = block_context + .vm_resource_fee_cost + .get(constants::N_STEPS_RESOURCE) + .unwrap_or_else(|| { + panic!("{} must appear in `vm_resource_fee_cost`.", constants::N_STEPS_RESOURCE) + }); + let max_gas = account_tx_context.max_fee.0 / block_context.gas_price; + ((max_gas as f64 / gas_per_step).floor() as usize) + .min(constants::MAX_STEPS_PER_TX) + .min(block_context.invoke_tx_max_n_steps as usize) } - - let gas_per_step = - block_context.vm_resource_fee_cost.get(constants::N_STEPS_RESOURCE).unwrap_or_else( - || panic!("{} must appear in `vm_resource_fee_cost`.", constants::N_STEPS_RESOURCE), - ); - - // New transactions derive the step limit by the L1 gas resource bounds; deprecated - // transactions derive this value from the `max_fee`. - let tx_gas_upper_bound = match account_tx_context { - AccountTransactionContext::Deprecated(context) => { - (context.max_fee.0 - / block_context.gas_prices.get_by_fee_type(&account_tx_context.fee_type())) - as usize - } - AccountTransactionContext::Current(context) => { - context.l1_resource_bounds()?.max_amount as usize - } - }; - - let tx_upper_bound = (tx_gas_upper_bound as f64 / gas_per_step).floor() as usize; - Ok(min(tx_upper_bound, block_upper_bound)) - } - - /// Returns the available steps in run resources. - pub fn n_remaining_steps(&self) -> usize { - self.vm_run_resources.get_n_steps().expect("The number of steps must be initialized.") } /// Subtracts the given number of steps from the currently available run resources. /// Used for limiting the number of steps available during the execution stage, to leave enough /// steps available for the fee transfer stage. - /// Returns the remaining number of steps. - pub fn subtract_steps(&mut self, steps_to_subtract: usize) -> usize { - // If remaining steps is less than the number of steps to subtract, attempting to subtrace - // would cause underflow error. - // Logically, we update remaining steps to `max(0, remaining_steps - steps_to_subtract)`. - let remaining_steps = self.n_remaining_steps(); - let new_remaining_steps = if remaining_steps < steps_to_subtract { - 0 - } else { - remaining_steps - steps_to_subtract - }; - self.vm_run_resources = RunResources::new(new_remaining_steps); - self.n_remaining_steps() - } - - /// From the total amount of steps available for execution, deduct the steps consumed during - /// validation and the overhead steps required for fee transfer. - /// Returns the remaining steps (after the subtraction). - pub fn subtract_validation_and_overhead_steps( - &mut self, - validate_call_info: &Option, - tx_type: &TransactionType, - ) -> usize { - let validate_steps = validate_call_info - .as_ref() - .map(|call_info| call_info.vm_resources.n_steps) - .unwrap_or_default(); - - let overhead_steps = OS_RESOURCES.resources_for_tx_type(tx_type).n_steps; - self.subtract_steps(validate_steps + overhead_steps) + pub fn subtract_steps(&mut self, steps_to_subtract: usize) { + let current_n_steps = + self.vm_run_resources.get_n_steps().expect("The number of steps must be initialized."); + let steps_to_subtract = min(steps_to_subtract, current_n_steps); + self.vm_run_resources = RunResources::new(current_n_steps - steps_to_subtract); } /// Combines individual errors into a single stack trace string, with contract addresses printed @@ -306,6 +176,193 @@ impl EntryPointExecutionContext { } } +impl CallEntryPoint { + pub fn execute( + mut self, + state: &mut dyn State, + resources: &mut ExecutionResources, + context: &mut EntryPointExecutionContext, + ) -> EntryPointExecutionResult { + context.current_recursion_depth += 1; + if context.current_recursion_depth > context.max_recursion_depth { + return Err(EntryPointExecutionError::RecursionDepthExceeded); + } + + // Validate contract is deployed. + let storage_address = self.storage_address; + let storage_class_hash = state.get_class_hash_at(self.storage_address)?; + if storage_class_hash == ClassHash::default() { + return Err(PreExecutionError::UninitializedStorageAddress(self.storage_address).into()); + } + + let class_hash = match self.class_hash { + Some(class_hash) => class_hash, + None => storage_class_hash, // If not given, take the storage contract class hash. + }; + // Hack to prevent version 0 attack on argent accounts. + if context.account_tx_context.version == TransactionVersion(StarkFelt::from(0_u8)) + && class_hash + == ClassHash( + StarkFelt::try_from(FAULTY_CLASS_HASH).expect("A class hash must be a felt."), + ) + { + return Err(PreExecutionError::FraudAttempt.into()); + } + // Add class hash to the call, that will appear in the output (call info). + self.class_hash = Some(class_hash); + let contract_class = state.get_compiled_contract_class(&class_hash)?; + + let result = execute_entry_point_call(self, contract_class, state, resources, context) + .map_err(|error| { + match error { + // On VM error, pack the stack trace into the propagated error. + EntryPointExecutionError::VirtualMachineExecutionError(error) => { + context.error_stack.push((storage_address, error.try_to_vm_trace())); + // TODO(Dori, 1/5/2023): Call error_trace only in the top call; as it is + // right now, each intermediate VM error is wrapped + // in a VirtualMachineExecutionErrorWithTrace error + // with the stringified trace of all errors below + // it. + EntryPointExecutionError::VirtualMachineExecutionErrorWithTrace { + trace: context.error_trace(), + source: error, + } + } + other_error => other_error, + } + }); + + context.current_recursion_depth -= 1; + result + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Retdata(pub Vec); + +#[macro_export] +macro_rules! retdata { + ( $( $x:expr ),* ) => { + Retdata(vec![$($x),*]) + }; +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub struct OrderedEvent { + pub order: usize, + pub event: EventContent, +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub struct MessageToL1 { + pub to_address: EthAddress, + pub payload: L2ToL1Payload, +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub struct OrderedL2ToL1Message { + pub order: usize, + pub message: MessageToL1, +} +#[derive(Debug, Default, Eq, PartialEq)] +pub struct CallExecution { + pub retdata: Retdata, + pub events: Vec, + pub l2_to_l1_messages: Vec, + pub failed: bool, + pub gas_consumed: u64, +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub struct CallInfo { + pub call: CallEntryPoint, + pub execution: CallExecution, + pub vm_resources: VmExecutionResources, + pub inner_calls: Vec, + + // Additional information gathered during execution. + pub storage_read_values: Vec, + pub accessed_storage_keys: HashSet, +} + +impl CallInfo { + /// Returns the set of class hashes that were executed during this call execution. + // TODO: Add unit test for this method + pub fn get_executed_class_hashes(&self) -> HashSet { + let mut class_hashes = HashSet::new(); + let calls = self.into_iter(); + for call in calls { + class_hashes + .insert(call.call.class_hash.expect("Class hash must be set after execution.")); + } + + class_hashes + } + + /// Returns a list of StarkNet L2ToL1Payload length collected during the execution, sorted + /// by the order in which they were sent. + pub fn get_sorted_l2_to_l1_payloads_length(&self) -> TransactionExecutionResult> { + let n_messages = self.into_iter().map(|call| call.execution.l2_to_l1_messages.len()).sum(); + let mut starknet_l2_to_l1_payloads_length: Vec> = vec![None; n_messages]; + + for call in self.into_iter() { + for ordered_message_content in &call.execution.l2_to_l1_messages { + let message_order = ordered_message_content.order; + if message_order >= n_messages { + return Err(TransactionExecutionError::InvalidOrder { + object: "L2-to-L1 message".to_string(), + order: message_order, + max_order: n_messages, + }); + } + starknet_l2_to_l1_payloads_length[message_order] = + Some(ordered_message_content.message.payload.0.len()); + } + } + + starknet_l2_to_l1_payloads_length.into_iter().enumerate().try_fold( + Vec::new(), + |mut acc, (i, option)| match option { + Some(value) => { + acc.push(value); + Ok(acc) + } + None => Err(TransactionExecutionError::UnexpectedHoles { + object: "L2-to-L1 message".to_string(), + order: i, + }), + }, + ) + } +} + +pub struct CallInfoIter<'a> { + call_infos: Vec<&'a CallInfo>, +} + +impl<'a> Iterator for CallInfoIter<'a> { + type Item = &'a CallInfo; + + fn next(&mut self) -> Option { + let Some(call_info) = self.call_infos.pop() else { + return None; + }; + + // Push order is right to left. + self.call_infos.extend(call_info.inner_calls.iter().rev()); + Some(call_info) + } +} + +impl<'a> IntoIterator for &'a CallInfo { + type Item = &'a CallInfo; + type IntoIter = CallInfoIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + CallInfoIter { call_infos: vec![self] } + } +} + pub fn execute_constructor_entry_point( state: &mut dyn State, resources: &mut ExecutionResources, @@ -366,32 +423,3 @@ pub fn handle_empty_constructor( Ok(empty_constructor_call_info) } - -// Ensure that the recursion depth does not exceed the maximum allowed depth. -struct RecursionDepthGuard { - current_depth: Arc>, - max_depth: usize, -} - -impl RecursionDepthGuard { - fn new(current_depth: Arc>, max_depth: usize) -> Self { - Self { current_depth: current_depth.clone(), max_depth } - } - - // Tries to increment the current recursion depth and returns an error if the maximum depth - // would be exceeded. - fn try_increment_and_check_depth(&mut self) -> EntryPointExecutionResult<()> { - *self.current_depth.borrow_mut() += 1; - if *self.current_depth.borrow() > self.max_depth { - return Err(EntryPointExecutionError::RecursionDepthExceeded); - } - Ok(()) - } -} - -// Implementing the Drop trait to decrement the recursion depth when the guard goes out of scope. -impl Drop for RecursionDepthGuard { - fn drop(&mut self) { - *self.current_depth.borrow_mut() -= 1; - } -} diff --git a/crates/blockifier/src/transaction/account_transaction.rs b/crates/blockifier/src/transaction/account_transaction.rs index 85e49912e9..f2696a939a 100644 --- a/crates/blockifier/src/transaction/account_transaction.rs +++ b/crates/blockifier/src/transaction/account_transaction.rs @@ -1,49 +1,49 @@ +use std::cmp::min; + +use cairo_vm::vm::runners::cairo_runner::ResourceTracker; use itertools::concat; use starknet_api::calldata; -use starknet_api::core::{ContractAddress, EntryPointSelector}; +use starknet_api::core::{ContractAddress, EntryPointSelector, Nonce}; use starknet_api::deprecated_contract_class::EntryPointType; use starknet_api::hash::StarkFelt; -use starknet_api::transaction::{Calldata, Fee, ResourceBounds, TransactionVersion}; +use starknet_api::transaction::{Calldata, Fee, TransactionVersion}; use crate::abi::abi_utils::selector_from_name; use crate::abi::constants as abi_constants; use crate::block_context::BlockContext; -use crate::execution::call_info::{CallInfo, Retdata}; use crate::execution::contract_class::ContractClass; use crate::execution::entry_point::{ - CallEntryPoint, CallType, EntryPointExecutionContext, ExecutionResources, + CallEntryPoint, CallInfo, CallType, EntryPointExecutionContext, ExecutionResources, Retdata, }; -use crate::fee::actual_cost::{ActualCost, ActualCostBuilder}; -use crate::fee::fee_checks::{FeeCheckReportFields, PostExecutionReport}; -use crate::fee::fee_utils::{get_fee_by_l1_gas_usage, verify_can_pay_committed_bounds}; -use crate::fee::gas_usage::estimate_minimal_l1_gas; +use crate::fee::fee_utils::calculate_tx_fee; +use crate::fee::gas_usage::estimate_minimal_fee; +use crate::fee::os_resources::OS_RESOURCES; use crate::retdata; -use crate::state::cached_state::{CachedState, TransactionalState}; +use crate::state::cached_state::{ + CachedState, StateChanges, StateChangesCount, TransactionalState, +}; use crate::state::state_api::{State, StateReader}; use crate::transaction::constants; -use crate::transaction::errors::{ - TransactionExecutionError, TransactionFeeError, TransactionPreValidationError, -}; +use crate::transaction::errors::TransactionExecutionError; use crate::transaction::objects::{ - AccountTransactionContext, HasRelatedFeeType, TransactionExecutionInfo, - TransactionExecutionResult, TransactionPreValidationResult, + AccountTransactionContext, ResourcesMapping, TransactionExecutionInfo, + TransactionExecutionResult, }; use crate::transaction::transaction_execution::Transaction; use crate::transaction::transaction_types::TransactionType; -use crate::transaction::transaction_utils::update_remaining_gas; +use crate::transaction::transaction_utils::{ + calculate_l1_gas_usage, calculate_tx_resources, update_remaining_gas, + verify_no_calls_to_other_contracts, +}; use crate::transaction::transactions::{ DeclareTransaction, DeployAccountTransaction, Executable, ExecutableTransaction, - InvokeTransaction, ValidatableTransaction, + InvokeTransaction, }; #[cfg(test)] #[path = "account_transactions_test.rs"] mod test; -#[cfg(test)] -#[path = "execution_flavors_test.rs"] -mod flavors_test; - /// Represents a paid StarkNet transaction. #[derive(Debug)] pub enum AccountTransaction { @@ -52,26 +52,47 @@ pub enum AccountTransaction { Invoke(InvokeTransaction), } -impl HasRelatedFeeType for AccountTransaction { - fn version(&self) -> TransactionVersion { - match self { - Self::Declare(tx) => tx.tx().version(), - Self::DeployAccount(tx) => tx.tx().version(), - Self::Invoke(tx) => match tx.tx { - starknet_api::transaction::InvokeTransaction::V0(_) => TransactionVersion::ZERO, - starknet_api::transaction::InvokeTransaction::V1(_) => TransactionVersion::ONE, - starknet_api::transaction::InvokeTransaction::V3(_) => TransactionVersion::THREE, - }, +struct ValidateExecuteCallInfo { + validate_call_info: Option, + execute_call_info: Option, + revert_error: Option, + final_fee: Fee, + final_resources: ResourcesMapping, +} + +impl ValidateExecuteCallInfo { + pub fn new_accepted( + validate_call_info: Option, + execute_call_info: Option, + final_fee: Fee, + final_resources: ResourcesMapping, + ) -> Self { + Self { + validate_call_info, + execute_call_info, + revert_error: None, + final_fee, + final_resources, } } - fn is_l1_handler(&self) -> bool { - false + pub fn new_reverted( + validate_call_info: Option, + revert_error: String, + final_fee: Fee, + final_resources: ResourcesMapping, + ) -> Self { + Self { + validate_call_info, + execute_call_info: None, + revert_error: Some(revert_error), + final_fee, + final_resources, + } } } impl AccountTransaction { - // TODO(nir, 01/11/2023): Consider instantiating CommonAccountFields in AccountTransaction. pub fn tx_type(&self) -> TransactionType { match self { AccountTransaction::Declare(_) => TransactionType::Declare, @@ -80,6 +101,21 @@ impl AccountTransaction { } } + pub fn get_address_of_deploy(&self) -> Option { + match self { + AccountTransaction::DeployAccount(deploy_tx) => Some(deploy_tx.contract_address), + _ => None, + } + } + + pub fn max_fee(&self) -> Fee { + match self { + AccountTransaction::Declare(declare) => declare.max_fee(), + AccountTransaction::DeployAccount(deploy_account) => deploy_account.max_fee(), + AccountTransaction::Invoke(invoke) => invoke.max_fee(), + } + } + fn validate_entry_point_selector(&self) -> EntryPointSelector { let validate_entry_point_name = match self { Self::Declare(_) => constants::VALIDATE_DECLARE_ENTRY_POINT_NAME, @@ -106,11 +142,55 @@ impl AccountTransaction { } } - pub fn get_account_tx_context(&self) -> AccountTransactionContext { + fn get_account_transaction_context(&self) -> AccountTransactionContext { match self { - Self::Declare(tx) => tx.get_account_tx_context(), - Self::DeployAccount(tx) => tx.get_account_tx_context(), - Self::Invoke(tx) => tx.get_account_tx_context(), + Self::Declare(tx) => { + let sn_api_tx = &tx.tx(); + AccountTransactionContext { + transaction_hash: tx.tx_hash(), + max_fee: sn_api_tx.max_fee(), + version: sn_api_tx.version(), + signature: sn_api_tx.signature(), + nonce: sn_api_tx.nonce(), + sender_address: sn_api_tx.sender_address(), + } + } + Self::DeployAccount(tx) => AccountTransactionContext { + transaction_hash: tx.tx_hash, + max_fee: tx.max_fee(), + version: tx.version(), + signature: tx.signature(), + nonce: tx.nonce(), + sender_address: tx.contract_address, + }, + Self::Invoke(tx) => { + let sn_api_tx = &tx.tx; + AccountTransactionContext { + transaction_hash: tx.tx_hash, + max_fee: sn_api_tx.max_fee(), + version: match sn_api_tx { + starknet_api::transaction::InvokeTransaction::V0(_) => { + TransactionVersion(StarkFelt::from(0_u8)) + } + starknet_api::transaction::InvokeTransaction::V1(_) => { + TransactionVersion(StarkFelt::from(1_u8)) + } + }, + signature: sn_api_tx.signature(), + nonce: match sn_api_tx { + starknet_api::transaction::InvokeTransaction::V0(_) => Nonce::default(), + starknet_api::transaction::InvokeTransaction::V1(tx_v1) => tx_v1.nonce, + }, + sender_address: match sn_api_tx { + starknet_api::transaction::InvokeTransaction::V0(tx_v0) => { + tx_v0.contract_address + } + starknet_api::transaction::InvokeTransaction::V1(tx_v1) => { + tx_v1.sender_address + } + }, + } + } } } @@ -119,18 +199,19 @@ impl AccountTransaction { // Support `Declare` of version 0 in order to allow bootstrapping of a new system. Self::Declare(_) => { vec![ - TransactionVersion::ZERO, - TransactionVersion::ONE, - TransactionVersion::TWO, - TransactionVersion::THREE, + TransactionVersion(StarkFelt::from(0_u8)), + TransactionVersion(StarkFelt::from(1_u8)), + TransactionVersion(StarkFelt::from(2_u8)), ] } - Self::DeployAccount(_) => { - vec![TransactionVersion::ONE, TransactionVersion::THREE] - } Self::Invoke(_) => { - vec![TransactionVersion::ZERO, TransactionVersion::ONE, TransactionVersion::THREE] + vec![ + TransactionVersion(StarkFelt::from(0_u8)), + TransactionVersion(StarkFelt::from(1_u8)), + TransactionVersion(StarkFelt::from(2_u8)), + ] } + _ => vec![TransactionVersion(StarkFelt::from(1_u8))], }; if allowed_versions.contains(&version) { Ok(()) @@ -139,124 +220,145 @@ impl AccountTransaction { } } - // Performs static checks before executing validation entry point. - // Note that nonce is incremented during these checks. - pub fn perform_pre_validation_stage( - &self, - state: &mut S, - account_tx_context: &AccountTransactionContext, - block_context: &BlockContext, - charge_fee: bool, - strict_nonce_check: bool, - ) -> TransactionPreValidationResult<()> { - Self::handle_nonce(state, account_tx_context, strict_nonce_check)?; - - if charge_fee && account_tx_context.enforce_fee()? { - self.check_fee_bounds(account_tx_context, block_context)?; - - verify_can_pay_committed_bounds(state, account_tx_context, block_context)?; - } - - Ok(()) - } - - fn check_fee_bounds( - &self, - account_tx_context: &AccountTransactionContext, - block_context: &BlockContext, - ) -> TransactionPreValidationResult<()> { - let minimal_l1_gas_amount = estimate_minimal_l1_gas(block_context, self)?; - - match account_tx_context { - AccountTransactionContext::Current(context) => { - let ResourceBounds { - max_amount: max_l1_gas_amount, - max_price_per_unit: max_l1_gas_price, - } = context.l1_resource_bounds()?; - - if (max_l1_gas_amount as u128) < minimal_l1_gas_amount { - return Err(TransactionFeeError::MaxL1GasAmountTooLow { - max_l1_gas_amount, - minimal_l1_gas_amount: (minimal_l1_gas_amount as u64), - })?; - } - - let actual_l1_gas_price = - block_context.gas_prices.get_by_fee_type(&account_tx_context.fee_type()); - if max_l1_gas_price < actual_l1_gas_price { - return Err(TransactionFeeError::MaxL1GasPriceTooLow { - max_l1_gas_price, - actual_l1_gas_price, - })?; - } - } - AccountTransactionContext::Deprecated(context) => { - let max_fee = context.max_fee; - let min_fee = get_fee_by_l1_gas_usage( - block_context, - minimal_l1_gas_amount, - &account_tx_context.fee_type(), - ); - if max_fee < min_fee { - return Err(TransactionFeeError::MaxFeeTooLow { min_fee, max_fee })?; - } - } - }; - Ok(()) - } - fn handle_nonce( - state: &mut dyn State, account_tx_context: &AccountTransactionContext, - strict: bool, - ) -> TransactionPreValidationResult<()> { - if account_tx_context.is_v0() { + state: &mut dyn State, + ) -> TransactionExecutionResult<()> { + if account_tx_context.version == TransactionVersion(StarkFelt::from(0_u8)) { return Ok(()); } - let address = account_tx_context.sender_address(); - let account_nonce = state.get_nonce_at(address)?; - let incoming_tx_nonce = account_tx_context.nonce(); - let valid_nonce = if strict { - account_nonce == incoming_tx_nonce - } else { - account_nonce <= incoming_tx_nonce - }; - if valid_nonce { - return Ok(state.increment_nonce(address)?); + let address = account_tx_context.sender_address; + let current_nonce = state.get_nonce_at(address)?; + if current_nonce != account_tx_context.nonce { + return Err(TransactionExecutionError::InvalidNonce { + address, + expected_nonce: current_nonce, + actual_nonce: account_tx_context.nonce, + }); } - Err(TransactionPreValidationError::InvalidNonce { - address, - account_nonce, - incoming_tx_nonce, - }) + + // Increment nonce. + Ok(state.increment_nonce(address)?) } - #[allow(clippy::too_many_arguments)] fn handle_validate_tx( &self, state: &mut dyn State, resources: &mut ExecutionResources, - account_tx_context: &AccountTransactionContext, remaining_gas: &mut u64, block_context: &BlockContext, validate: bool, - limit_steps_by_resources: bool, ) -> TransactionExecutionResult> { if validate { - self.validate_tx( - state, - resources, - account_tx_context, - remaining_gas, - block_context, - limit_steps_by_resources, - ) + self.validate_tx(state, resources, remaining_gas, block_context) } else { Ok(None) } } + fn validate_tx( + &self, + state: &mut dyn State, + resources: &mut ExecutionResources, + remaining_gas: &mut u64, + block_context: &BlockContext, + ) -> TransactionExecutionResult> { + let account_tx_context = self.get_account_transaction_context(); + let mut context = + EntryPointExecutionContext::new_validate(block_context, &account_tx_context); + if context.account_tx_context.is_v0() { + return Ok(None); + } + + let storage_address = account_tx_context.sender_address; + let validate_call = CallEntryPoint { + entry_point_type: EntryPointType::External, + entry_point_selector: self.validate_entry_point_selector(), + calldata: self.validate_entrypoint_calldata(), + class_hash: None, + code_address: None, + storage_address, + caller_address: ContractAddress::default(), + call_type: CallType::Call, + initial_gas: *remaining_gas, + }; + + let validate_call_info = validate_call + .execute(state, resources, &mut context) + .map_err(TransactionExecutionError::ValidateTransactionError)?; + verify_no_calls_to_other_contracts( + &validate_call_info, + String::from(constants::VALIDATE_ENTRY_POINT_NAME), + )?; + + // Validate return data. + let class_hash = state.get_class_hash_at(storage_address)?; + let contract_class = state.get_compiled_contract_class(&class_hash)?; + if let ContractClass::V1(_) = contract_class { + // The account contract class is a Cairo 1.0 contract; the `validate` entry point should + // return `VALID`. + let expected_retdata = retdata![StarkFelt::try_from(constants::VALIDATE_RETDATA)?]; + if validate_call_info.execution.retdata != expected_retdata { + return Err(TransactionExecutionError::InvalidValidateReturnData { + actual: validate_call_info.execution.retdata, + }); + } + } + + update_remaining_gas(remaining_gas, &validate_call_info); + + Ok(Some(validate_call_info)) + } + + fn enforce_fee(&self) -> bool { + self.max_fee() != Fee(0) + } + + // TODO(Dori,1/10/2023): If/when Fees can be more than 128 bit integers, this should be updated. + fn is_sufficient_fee_balance( + balance_low: StarkFelt, + balance_high: StarkFelt, + fee: Fee, + ) -> bool { + // The fee is at most 128 bits, while balance is 256 bits (split into two 128 bit words). + balance_high > StarkFelt::from(0_u8) || balance_low >= StarkFelt::from(fee.0) + } + + /// Checks that the account's balance covers max fee. + fn check_fee_balance( + &self, + state: &mut TransactionalState<'_, S>, + block_context: &BlockContext, + ) -> TransactionExecutionResult<()> { + let account_tx_context = self.get_account_transaction_context(); + let max_fee = account_tx_context.max_fee; + + // Check fee balance. + if self.enforce_fee() { + // Check max fee is at least the estimated constant overhead. + let minimal_fee = estimate_minimal_fee(block_context, self)?; + if minimal_fee > max_fee { + return Err(TransactionExecutionError::MaxFeeTooLow { + min_fee: minimal_fee, + max_fee, + }); + } + + let (balance_low, balance_high) = + state.get_fee_token_balance(block_context, &account_tx_context.sender_address)?; + if !Self::is_sufficient_fee_balance(balance_low, balance_high, max_fee) { + return Err(TransactionExecutionError::MaxFeeExceedsBalance { + max_fee, + balance_low, + balance_high, + }); + } + } + + Ok(()) + } + fn handle_fee( &self, state: &mut dyn State, @@ -270,7 +372,7 @@ impl AccountTransaction { } // Charge fee. - let account_tx_context = self.get_account_tx_context(); + let account_tx_context = self.get_account_transaction_context(); let fee_transfer_call_info = Self::execute_fee_transfer(state, block_context, account_tx_context, actual_fee)?; @@ -283,13 +385,17 @@ impl AccountTransaction { account_tx_context: AccountTransactionContext, actual_fee: Fee, ) -> TransactionExecutionResult { + let max_fee = account_tx_context.max_fee; + if actual_fee > max_fee { + return Err(TransactionExecutionError::FeeTransferError { max_fee, actual_fee }); + } + // The least significant 128 bits of the amount transferred. let lsb_amount = StarkFelt::from(actual_fee.0); // The most significant 128 bits of the amount transferred. let msb_amount = StarkFelt::from(0_u8); - // TODO(Gilad): add test that correct fee address is taken, once we add V3 test support. - let storage_address = block_context.fee_token_address(&account_tx_context.fee_type()); + let storage_address = block_context.fee_token_address; let fee_transfer_call = CallEntryPoint { class_hash: None, code_address: None, @@ -301,14 +407,14 @@ impl AccountTransaction { msb_amount ], storage_address, - caller_address: account_tx_context.sender_address(), + caller_address: account_tx_context.sender_address, call_type: CallType::Call, // The fee-token contract is a Cairo 0 contract, hence the initial gas is irrelevant. initial_gas: abi_constants::INITIAL_GAS_COST, }; let mut context = - EntryPointExecutionContext::new_invoke(block_context, &account_tx_context, true)?; + EntryPointExecutionContext::new_invoke(block_context, &account_tx_context); Ok(fee_transfer_call.execute(state, &mut ExecutionResources::default(), &mut context)?) } @@ -330,112 +436,89 @@ impl AccountTransaction { fn run_non_revertible( &self, state: &mut TransactionalState<'_, S>, - account_tx_context: &AccountTransactionContext, + resources: &mut ExecutionResources, remaining_gas: &mut u64, block_context: &BlockContext, + mut execution_context: EntryPointExecutionContext, validate: bool, - charge_fee: bool, ) -> TransactionExecutionResult { - let mut resources = ExecutionResources::default(); let validate_call_info: Option; let execute_call_info: Option; if matches!(self, Self::DeployAccount(_)) { // Handle `DeployAccount` transactions separately, due to different order of things. - // Also, the execution context required form the `DeployAccount` execute phase is - // validation context. - let mut execution_context = EntryPointExecutionContext::new_validate( - block_context, - account_tx_context, - charge_fee, - )?; execute_call_info = - self.run_execute(state, &mut resources, &mut execution_context, remaining_gas)?; - validate_call_info = self.handle_validate_tx( - state, - &mut resources, - account_tx_context, - remaining_gas, - block_context, - validate, - charge_fee, - )?; + self.run_execute(state, resources, &mut execution_context, remaining_gas)?; + validate_call_info = + self.handle_validate_tx(state, resources, remaining_gas, block_context, validate)?; } else { - let mut execution_context = EntryPointExecutionContext::new_invoke( - block_context, - account_tx_context, - charge_fee, - )?; - validate_call_info = self.handle_validate_tx( - state, - &mut resources, - account_tx_context, - remaining_gas, - block_context, - validate, - charge_fee, - )?; + validate_call_info = + self.handle_validate_tx(state, resources, remaining_gas, block_context, validate)?; execute_call_info = - self.run_execute(state, &mut resources, &mut execution_context, remaining_gas)?; + self.run_execute(state, resources, &mut execution_context, remaining_gas)?; } - - let actual_cost = self - .into_actual_cost_builder(block_context) - .with_validate_call_info(&validate_call_info) - .with_execute_call_info(&execute_call_info) - .try_add_state_changes(state)? - .build(&resources)?; - - let post_execution_report = PostExecutionReport::new( - state, + let state_changes = state.get_actual_state_changes_for_fee_charge( + block_context.fee_token_address, + Some(self.get_account_transaction_context().sender_address), + )?; + let (actual_fee, actual_resources) = self.calculate_actual_fee_and_resources( + StateChangesCount::from(&state_changes), + &execute_call_info, + &validate_call_info, + resources, block_context, - account_tx_context, - &actual_cost, - charge_fee, + false, + 0, )?; - match post_execution_report.error() { - Some(error) => Err(error.into()), - None => Ok(ValidateExecuteCallInfo::new_accepted( - validate_call_info, - execute_call_info, - actual_cost, - )), - } + Ok(ValidateExecuteCallInfo::new_accepted( + validate_call_info, + execute_call_info, + actual_fee, + actual_resources, + )) } - #[allow(clippy::too_many_arguments)] fn run_revertible( &self, state: &mut TransactionalState<'_, S>, - account_tx_context: &AccountTransactionContext, + resources: &mut ExecutionResources, remaining_gas: &mut u64, block_context: &BlockContext, + mut execution_context: EntryPointExecutionContext, validate: bool, - charge_fee: bool, ) -> TransactionExecutionResult { - let mut resources = ExecutionResources::default(); - let mut execution_context = - EntryPointExecutionContext::new_invoke(block_context, account_tx_context, charge_fee)?; - let account_tx_context = self.get_account_tx_context(); + let account_tx_context = self.get_account_transaction_context(); // Run the validation, and if execution later fails, only keep the validation diff. - let validate_call_info = self.handle_validate_tx( - state, - &mut resources, - &account_tx_context, - remaining_gas, - block_context, - validate, - charge_fee, - )?; - - let n_allotted_execution_steps = execution_context - .subtract_validation_and_overhead_steps(&validate_call_info, &self.tx_type()); + let validate_call_info = + self.handle_validate_tx(state, resources, remaining_gas, block_context, validate)?; + let validate_steps = if validate { + validate_call_info + .as_ref() + .expect("`validate` call info cannot be `None`.") + .vm_resources + .n_steps + } else { + 0 + }; + let overhead_steps = OS_RESOURCES + .execute_txs_inner() + .get(&self.tx_type()) + .expect("`OS_RESOURCES` must contain all transaction types.") + .n_steps; + + // Subtract the actual steps used for validate_tx and estimated steps required for fee + // transfer from the steps available to the run_execute context. + execution_context.subtract_steps(validate_steps + overhead_steps); + let n_allotted_steps = execution_context + .vm_run_resources + .get_n_steps() + .expect("The number of steps must be initialized."); // Save the state changes resulting from running `validate_tx`, to be used later for // resource and fee calculation. - let actual_cost_builder_with_validation_changes = self - .into_actual_cost_builder(block_context) - .with_validate_call_info(&validate_call_info) - .try_add_state_changes(state)?; + let validate_state_changes = state.get_actual_state_changes_for_fee_charge( + block_context.fee_token_address, + Some(account_tx_context.sender_address), + )?; // Create copies of state and resources for the execution. // Both will be rolled back if the execution is reverted or committed upon success. @@ -449,78 +532,117 @@ impl AccountTransaction { remaining_gas, ); - // Pre-compute cost in case of revert. - let execution_steps_consumed = - n_allotted_execution_steps - execution_context.n_remaining_steps(); - let revert_cost = actual_cost_builder_with_validation_changes - .clone() - .with_reverted_steps(execution_steps_consumed) - .build(&resources)?; - match execution_result { Ok(execute_call_info) => { // When execution succeeded, calculate the actual required fee before committing the // transactional state. If max_fee is insufficient, revert the `run_execute` part. - let actual_cost = actual_cost_builder_with_validation_changes - .with_execute_call_info(&execute_call_info) - // Fee is determined by the sum of `validate` and `execute` state changes. - // Since `execute_state_changes` are not yet committed, we merge them manually - // with `validate_state_changes` to count correctly. - .try_add_state_changes(&mut execution_state)? - .build(&execution_resources)?; - - // Post-execution checks. - let post_execution_report = PostExecutionReport::new( - &mut execution_state, + let execute_state_changes = execution_state + .get_actual_state_changes_for_fee_charge( + block_context.fee_token_address, + Some(account_tx_context.sender_address), + )?; + // Fee is determined by the sum of `validate` and `execute` state changes. + // Since `execute_state_changes` are not yet committed, we merge them manually with + // `validate_state_changes` to count correctly. + let state_changes = StateChanges::merge(vec![ + validate_state_changes.clone(), + execute_state_changes, + ]); + + let (actual_fee, actual_resources) = self.calculate_actual_fee_and_resources( + StateChangesCount::from(&state_changes), + &execute_call_info, + &validate_call_info, + &execution_resources, block_context, - &account_tx_context, - &actual_cost, - charge_fee, + false, + 0, )?; - match post_execution_report.error() { - Some(post_execution_error) => { - // Post-execution check failed. Revert the execution, compute the final fee - // to charge and recompute resources used (to be consistent with other - // revert case, compute resources by adding consumed execution steps to - // validation resources). - execution_state.abort(); - Ok(ValidateExecuteCallInfo::new_reverted( - validate_call_info, - post_execution_error.to_string(), - ActualCost { - actual_fee: post_execution_report.recommended_fee(), - actual_resources: revert_cost.actual_resources, - }, - )) - } - None => { - // Post-execution check passed, commit the execution. - execution_state.commit(); - Ok(ValidateExecuteCallInfo::new_accepted( - validate_call_info, - execute_call_info, - actual_cost, - )) - } + + // Check if as a result of tx execution the sender's fee token balance is maxed out, + // so that they can't pay fee. If so, the transaction must be reverted. + let (balance_low, balance_high) = execution_state + .get_fee_token_balance(block_context, &account_tx_context.sender_address)?; + let is_maxed_out = + !Self::is_sufficient_fee_balance(balance_low, balance_high, actual_fee); + let max_fee = account_tx_context.max_fee; + + if actual_fee > max_fee || is_maxed_out { + // Insufficient fee. Revert the execution and charge what is available. + let (final_fee, revert_error) = if actual_fee > max_fee { + ( + max_fee, + format!( + "Insufficient max fee: max_fee: {max_fee:?}, actual_fee: \ + {actual_fee:?}", + ), + ) + } else { + (actual_fee, String::from("Insufficient fee token balance")) + }; + + execution_state.abort(); + let n_remaining_steps = execution_context + .vm_run_resources + .get_n_steps() + .expect("Invalid remaining steps in RunResources."); + let n_reverted_steps = n_allotted_steps - n_remaining_steps; + + // Rerunning `calculate_actual_fee_and_resources` with only the `validate` state + // changes in order to get the correct resources, as `execute` is reverted. + let (_, final_resources) = self.calculate_actual_fee_and_resources( + StateChangesCount::from(&validate_state_changes), + &None, + &validate_call_info, + &execution_resources, + block_context, + true, + n_reverted_steps, + )?; + + return Ok(ValidateExecuteCallInfo::new_reverted( + validate_call_info, + revert_error, + final_fee, + final_resources, + )); } + + // Commit the execution. + resources.clone_from(&execution_resources); + execution_state.commit(); + Ok(ValidateExecuteCallInfo::new_accepted( + validate_call_info, + execute_call_info, + actual_fee, + actual_resources, + )) } Err(_) => { - // Error during execution. Revert, even if the error is sequencer-related. + // Error during execution. Revert. execution_state.abort(); - let post_execution_report = PostExecutionReport::new( - state, + let n_remaining_steps = execution_context + .vm_run_resources + .get_n_steps() + .expect("The number of steps must be initialized."); + let n_reverted_steps = n_allotted_steps - n_remaining_steps; + + // Fee is determined by the `validate` state changes since `execute` is reverted. + let (actual_fee, actual_resources) = self.calculate_actual_fee_and_resources( + StateChangesCount::from(&validate_state_changes), + &None, + &validate_call_info, + &execution_resources, block_context, - &account_tx_context, - &revert_cost, - charge_fee, + true, + n_reverted_steps, )?; + Ok(ValidateExecuteCallInfo::new_reverted( validate_call_info, execution_context.error_trace(), - ActualCost { - actual_fee: post_execution_report.recommended_fee(), - actual_resources: revert_cost.actual_resources, - }, + actual_fee, + actual_resources, )) } } @@ -534,7 +656,7 @@ impl AccountTransaction { Self::Invoke(_) => { // V0 transactions do not have validation; we cannot deduct fee for execution. Thus, // invoke transactions of are non-revertible iff they are of version 0. - self.get_account_tx_context().is_v0() + self.get_account_transaction_context().is_v0() } } } @@ -543,36 +665,70 @@ impl AccountTransaction { fn run_or_revert( &self, state: &mut TransactionalState<'_, S>, + resources: &mut ExecutionResources, remaining_gas: &mut u64, block_context: &BlockContext, validate: bool, - charge_fee: bool, ) -> TransactionExecutionResult { - let account_tx_context = self.get_account_tx_context(); + let account_tx_context = self.get_account_transaction_context(); + let execution_context = + EntryPointExecutionContext::new_invoke(block_context, &account_tx_context); if self.is_non_revertible() { return self.run_non_revertible( state, - &account_tx_context, + resources, remaining_gas, block_context, + execution_context, validate, - charge_fee, ); } self.run_revertible( state, - &account_tx_context, + resources, remaining_gas, block_context, + execution_context, validate, - charge_fee, ) } - pub fn into_actual_cost_builder(&self, block_context: &BlockContext) -> ActualCostBuilder<'_> { - ActualCostBuilder::new(block_context, self.get_account_tx_context(), self.tx_type()) + #[allow(clippy::too_many_arguments)] + fn calculate_actual_fee_and_resources( + &self, + state_changes_count: StateChangesCount, + execute_call_info: &Option, + validate_call_info: &Option, + execution_resources: &ExecutionResources, + block_context: &BlockContext, + is_reverted: bool, + n_reverted_steps: usize, + ) -> TransactionExecutionResult<(Fee, ResourcesMapping)> { + let account_tx_context = self.get_account_transaction_context(); + + let non_optional_call_infos = vec![validate_call_info.as_ref(), execute_call_info.as_ref()] + .into_iter() + .flatten() + .collect::>(); + let l1_gas_usage = + calculate_l1_gas_usage(&non_optional_call_infos, state_changes_count, None)?; + let mut actual_resources = + calculate_tx_resources(execution_resources, l1_gas_usage, self.tx_type())?; + + // Add reverted steps to actual_resources' n_steps for correct fee charge. + *actual_resources.0.get_mut(&abi_constants::N_STEPS_RESOURCE.to_string()).unwrap() += + n_reverted_steps; + + let mut actual_fee = calculate_tx_fee(&actual_resources, block_context)?; + + if is_reverted || account_tx_context.max_fee == Fee(0) { + // We cannot charge more than max_fee for reverted txs. + actual_fee = min(actual_fee, account_tx_context.max_fee); + } + + Ok((actual_fee, actual_resources)) } } @@ -584,28 +740,28 @@ impl ExecutableTransaction for AccountTransaction { charge_fee: bool, validate: bool, ) -> TransactionExecutionResult { - let account_tx_context = self.get_account_tx_context(); + let account_tx_context = self.get_account_transaction_context(); + self.verify_tx_version(account_tx_context.version)?; - self.verify_tx_version(account_tx_context.version())?; + let mut resources = ExecutionResources::default(); + let mut remaining_gas = Transaction::initial_gas(); // Nonce and fee check should be done before running user code. - let strict_nonce_check = true; - self.perform_pre_validation_stage( - state, - &account_tx_context, - block_context, - charge_fee, - strict_nonce_check, - )?; + if charge_fee { + self.check_fee_balance(state, block_context)?; + } + // Handle nonce. + Self::handle_nonce(&account_tx_context, state)?; // Run validation and execution. - let mut remaining_gas = Transaction::initial_gas(); let ValidateExecuteCallInfo { validate_call_info, execute_call_info, revert_error, - final_cost: ActualCost { actual_fee: final_fee, actual_resources: final_resources }, - } = self.run_or_revert(state, &mut remaining_gas, block_context, validate, charge_fee)?; + final_fee, + final_resources, + } = + self.run_or_revert(state, &mut resources, &mut remaining_gas, block_context, validate)?; let fee_transfer_call_info = self.handle_fee(state, block_context, final_fee, charge_fee)?; @@ -621,90 +777,3 @@ impl ExecutableTransaction for AccountTransaction { Ok(tx_execution_info) } } - -/// Represents a bundle of validate-execute stage execution effects. -struct ValidateExecuteCallInfo { - validate_call_info: Option, - execute_call_info: Option, - revert_error: Option, - final_cost: ActualCost, -} - -impl ValidateExecuteCallInfo { - pub fn new_accepted( - validate_call_info: Option, - execute_call_info: Option, - final_cost: ActualCost, - ) -> Self { - Self { validate_call_info, execute_call_info, revert_error: None, final_cost } - } - - pub fn new_reverted( - validate_call_info: Option, - revert_error: String, - final_cost: ActualCost, - ) -> Self { - Self { - validate_call_info, - execute_call_info: None, - revert_error: Some(revert_error), - final_cost, - } - } -} - -impl ValidatableTransaction for AccountTransaction { - fn validate_tx( - &self, - state: &mut dyn State, - resources: &mut ExecutionResources, - account_tx_context: &AccountTransactionContext, - remaining_gas: &mut u64, - block_context: &BlockContext, - limit_steps_by_resources: bool, - ) -> TransactionExecutionResult> { - let mut context = EntryPointExecutionContext::new_validate( - block_context, - account_tx_context, - limit_steps_by_resources, - )?; - if context.account_tx_context.is_v0() { - return Ok(None); - } - - let storage_address = account_tx_context.sender_address(); - let validate_call = CallEntryPoint { - entry_point_type: EntryPointType::External, - entry_point_selector: self.validate_entry_point_selector(), - calldata: self.validate_entrypoint_calldata(), - class_hash: None, - code_address: None, - storage_address, - caller_address: ContractAddress::default(), - call_type: CallType::Call, - initial_gas: *remaining_gas, - }; - - let validate_call_info = validate_call - .execute(state, resources, &mut context) - .map_err(TransactionExecutionError::ValidateTransactionError)?; - - // Validate return data. - let class_hash = state.get_class_hash_at(storage_address)?; - let contract_class = state.get_compiled_contract_class(&class_hash)?; - if let ContractClass::V1(_) = contract_class { - // The account contract class is a Cairo 1.0 contract; the `validate` entry point should - // return `VALID`. - let expected_retdata = retdata![StarkFelt::try_from(constants::VALIDATE_RETDATA)?]; - if validate_call_info.execution.retdata != expected_retdata { - return Err(TransactionExecutionError::InvalidValidateReturnData { - actual: validate_call_info.execution.retdata, - }); - } - } - - update_remaining_gas(remaining_gas, &validate_call_info); - - Ok(Some(validate_call_info)) - } -} diff --git a/crates/blockifier/src/transaction/transactions.rs b/crates/blockifier/src/transaction/transactions.rs index 5e39fb860f..1747fa424f 100644 --- a/crates/blockifier/src/transaction/transactions.rs +++ b/crates/blockifier/src/transaction/transactions.rs @@ -1,18 +1,17 @@ use std::sync::Arc; -use starknet_api::core::{ClassHash, ContractAddress, Nonce}; +use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce}; use starknet_api::deprecated_contract_class::EntryPointType; use starknet_api::transaction::{ - AccountDeploymentData, Calldata, ContractAddressSalt, DeclareTransactionV2, - DeclareTransactionV3, Fee, TransactionHash, TransactionSignature, TransactionVersion, + Calldata, ContractAddressSalt, Fee, TransactionHash, TransactionSignature, TransactionVersion, }; use crate::abi::abi_utils::selector_from_name; use crate::block_context::BlockContext; -use crate::execution::call_info::CallInfo; use crate::execution::contract_class::ContractClass; use crate::execution::entry_point::{ - CallEntryPoint, CallType, ConstructorContext, EntryPointExecutionContext, ExecutionResources, + CallEntryPoint, CallInfo, CallType, ConstructorContext, EntryPointExecutionContext, + ExecutionResources, }; use crate::execution::execution_utils::execute_deployment; use crate::state::cached_state::{CachedState, TransactionalState}; @@ -20,17 +19,23 @@ use crate::state::errors::StateError; use crate::state::state_api::{State, StateReader}; use crate::transaction::constants; use crate::transaction::errors::TransactionExecutionError; -use crate::transaction::objects::{ - AccountTransactionContext, CommonAccountFields, CurrentAccountTransactionContext, - DeprecatedAccountTransactionContext, HasRelatedFeeType, TransactionExecutionInfo, - TransactionExecutionResult, +use crate::transaction::objects::{TransactionExecutionInfo, TransactionExecutionResult}; +use crate::transaction::transaction_utils::{ + update_remaining_gas, verify_no_calls_to_other_contracts, }; -use crate::transaction::transaction_utils::{update_remaining_gas, verify_contract_class_version}; #[cfg(test)] #[path = "transactions_test.rs"] mod test; +macro_rules! implement_inner_tx_getters { + ($(($field:ident, $field_type:ty)),*) => { + $(pub fn $field(&self) -> $field_type { + self.tx.$field.clone() + })* + }; +} + macro_rules! implement_inner_tx_getter_calls { ($(($field:ident, $field_type:ty)),*) => { $(pub fn $field(&self) -> $field_type { @@ -89,58 +94,63 @@ pub trait Executable { ) -> TransactionExecutionResult>; } -/// Intended for use in sequencer pre-execution flows, like in a gateway service. -pub trait ValidatableTransaction { - fn validate_tx( - &self, - state: &mut dyn State, - resources: &mut ExecutionResources, - account_tx_context: &AccountTransactionContext, - remaining_gas: &mut u64, - block_context: &BlockContext, - limit_steps_by_resources: bool, - ) -> TransactionExecutionResult>; -} - #[derive(Debug)] pub struct DeclareTransaction { tx: starknet_api::transaction::DeclareTransaction, tx_hash: TransactionHash, contract_class: ContractClass, - // Indicates the presence of the only_query bit in the version. - only_query: bool, } impl DeclareTransaction { - fn create( - declare_tx: starknet_api::transaction::DeclareTransaction, - tx_hash: TransactionHash, - contract_class: ContractClass, - only_query: bool, - ) -> TransactionExecutionResult { - let declare_version = declare_tx.version(); - let contract_class = verify_contract_class_version(contract_class, declare_version)?; - Ok(Self { tx: declare_tx, tx_hash, contract_class, only_query }) - } - pub fn new( declare_tx: starknet_api::transaction::DeclareTransaction, tx_hash: TransactionHash, contract_class: ContractClass, ) -> TransactionExecutionResult { - Self::create(declare_tx, tx_hash, contract_class, false) - } - - pub fn new_for_query( - declare_tx: starknet_api::transaction::DeclareTransaction, - tx_hash: TransactionHash, - contract_class: ContractClass, - ) -> TransactionExecutionResult { - Self::create(declare_tx, tx_hash, contract_class, true) + let declare_version = declare_tx.version(); + match declare_tx { + starknet_api::transaction::DeclareTransaction::V0(tx) => { + let ContractClass::V0(contract_class) = contract_class else { + return Err(TransactionExecutionError::ContractClassVersionMismatch { + declare_version, + cairo_version: 0, + }); + }; + Ok(Self { + tx: starknet_api::transaction::DeclareTransaction::V0(tx), + tx_hash, + contract_class: contract_class.into(), + }) + } + starknet_api::transaction::DeclareTransaction::V1(tx) => { + let ContractClass::V0(contract_class) = contract_class else { + return Err(TransactionExecutionError::ContractClassVersionMismatch { + declare_version, + cairo_version: 0, + }); + }; + Ok(Self { + tx: starknet_api::transaction::DeclareTransaction::V1(tx), + tx_hash, + contract_class: contract_class.into(), + }) + } + starknet_api::transaction::DeclareTransaction::V2(tx) => { + let ContractClass::V1(contract_class) = contract_class else { + return Err(TransactionExecutionError::ContractClassVersionMismatch { + declare_version, + cairo_version: 1, + }); + }; + Ok(Self { + tx: starknet_api::transaction::DeclareTransaction::V2(tx), + tx_hash, + contract_class: contract_class.into(), + }) + } + } } - implement_inner_tx_getter_calls!((class_hash, ClassHash)); - pub fn tx(&self) -> &starknet_api::transaction::DeclareTransaction { &self.tx } @@ -153,48 +163,7 @@ impl DeclareTransaction { self.contract_class.clone() } - pub fn get_account_tx_context(&self) -> AccountTransactionContext { - // TODO(Nir, 01/11/2023): Consider to move this (from all get_account_tx_context methods). - let common_fields = CommonAccountFields { - transaction_hash: self.tx_hash(), - version: self.tx.version(), - signature: self.tx.signature(), - nonce: self.tx.nonce(), - sender_address: self.tx.sender_address(), - only_query: self.only_query, - }; - - match &self.tx { - starknet_api::transaction::DeclareTransaction::V0(tx) - | starknet_api::transaction::DeclareTransaction::V1(tx) => { - AccountTransactionContext::Deprecated(DeprecatedAccountTransactionContext { - common_fields, - max_fee: tx.max_fee, - }) - } - starknet_api::transaction::DeclareTransaction::V2(tx) => { - AccountTransactionContext::Deprecated(DeprecatedAccountTransactionContext { - common_fields, - max_fee: tx.max_fee, - }) - } - starknet_api::transaction::DeclareTransaction::V3(tx) => { - AccountTransactionContext::Current(CurrentAccountTransactionContext { - common_fields, - resource_bounds: tx.resource_bounds.clone(), - tip: tx.tip, - nonce_data_availability_mode: tx.nonce_data_availability_mode, - fee_data_availability_mode: tx.fee_data_availability_mode, - paymaster_data: tx.paymaster_data.clone(), - account_deployment_data: tx.account_deployment_data.clone(), - }) - } - } - } - - pub fn only_query(&self) -> bool { - self.only_query - } + implement_inner_tx_getter_calls!((class_hash, ClassHash), (max_fee, Fee)); } impl Executable for DeclareTransaction { @@ -212,24 +181,18 @@ impl Executable for DeclareTransaction { starknet_api::transaction::DeclareTransaction::V0(_) | starknet_api::transaction::DeclareTransaction::V1(_) => { state.set_contract_class(&class_hash, self.contract_class.clone())?; + state.set_compiled_class_hash(class_hash, CompiledClassHash(class_hash.0))?; Ok(None) } - starknet_api::transaction::DeclareTransaction::V2(DeclareTransactionV2 { - compiled_class_hash, - .. - }) - | starknet_api::transaction::DeclareTransaction::V3(DeclareTransactionV3 { - compiled_class_hash, - .. - }) => { + starknet_api::transaction::DeclareTransaction::V2(tx) => { match state.get_compiled_contract_class(&class_hash) { Err(StateError::UndeclaredClassHash(_)) => { // Class is undeclared; declare it. state.set_contract_class(&class_hash, self.contract_class.clone())?; - state.set_compiled_class_hash(class_hash, *compiled_class_hash)?; + state.set_compiled_class_hash(class_hash, tx.compiled_class_hash)?; Ok(None) } - Err(error) => Err(error)?, + Err(error) => Err(error).map_err(TransactionExecutionError::from), Ok(_) => { // Class is already declared, cannot redeclare // (i.e., make sure the leaf is uninitialized). @@ -246,69 +209,18 @@ pub struct DeployAccountTransaction { pub tx: starknet_api::transaction::DeployAccountTransaction, pub tx_hash: TransactionHash, pub contract_address: ContractAddress, - // Indicates the presence of the only_query bit in the version. - pub only_query: bool, } impl DeployAccountTransaction { - pub fn new( - deploy_account_tx: starknet_api::transaction::DeployAccountTransaction, - tx_hash: TransactionHash, - contract_address: ContractAddress, - ) -> Self { - Self { tx: deploy_account_tx, tx_hash, contract_address, only_query: false } - } - - pub fn new_for_query( - deploy_account_tx: starknet_api::transaction::DeployAccountTransaction, - tx_hash: TransactionHash, - contract_address: ContractAddress, - ) -> Self { - Self { tx: deploy_account_tx, tx_hash, contract_address, only_query: true } - } - - implement_inner_tx_getter_calls!( + implement_inner_tx_getters!( (class_hash, ClassHash), - (constructor_calldata, Calldata), (contract_address_salt, ContractAddressSalt), + (max_fee, Fee), + (version, TransactionVersion), (nonce, Nonce), + (constructor_calldata, Calldata), (signature, TransactionSignature) ); - - pub fn tx(&self) -> &starknet_api::transaction::DeployAccountTransaction { - &self.tx - } - - pub fn get_account_tx_context(&self) -> AccountTransactionContext { - let common_fields = CommonAccountFields { - transaction_hash: self.tx_hash, - version: self.tx.version(), - signature: self.tx.signature(), - nonce: self.tx.nonce(), - sender_address: self.contract_address, - only_query: self.only_query, - }; - - match &self.tx { - starknet_api::transaction::DeployAccountTransaction::V1(tx) => { - AccountTransactionContext::Deprecated(DeprecatedAccountTransactionContext { - common_fields, - max_fee: tx.max_fee, - }) - } - starknet_api::transaction::DeployAccountTransaction::V3(tx) => { - AccountTransactionContext::Current(CurrentAccountTransactionContext { - common_fields, - resource_bounds: tx.resource_bounds.clone(), - tip: tx.tip, - nonce_data_availability_mode: tx.nonce_data_availability_mode, - fee_data_availability_mode: tx.fee_data_availability_mode, - paymaster_data: tx.paymaster_data.clone(), - account_deployment_data: AccountDeploymentData::default(), - }) - } - } - } } impl Executable for DeployAccountTransaction { @@ -336,6 +248,7 @@ impl Executable for DeployAccountTransaction { let call_info = deployment_result .map_err(TransactionExecutionError::ContractConstructorExecutionFailed)?; update_remaining_gas(remaining_gas, &call_info); + verify_no_calls_to_other_contracts(&call_info, String::from("an account constructor"))?; Ok(Some(call_info)) } @@ -345,67 +258,14 @@ impl Executable for DeployAccountTransaction { pub struct InvokeTransaction { pub tx: starknet_api::transaction::InvokeTransaction, pub tx_hash: TransactionHash, - // Indicates the presence of the only_query bit in the version. - pub only_query: bool, } impl InvokeTransaction { - pub fn new( - invoke_tx: starknet_api::transaction::InvokeTransaction, - tx_hash: TransactionHash, - ) -> Self { - Self { tx: invoke_tx, tx_hash, only_query: false } - } - - pub fn new_for_query( - invoke_tx: starknet_api::transaction::InvokeTransaction, - tx_hash: TransactionHash, - ) -> Self { - Self { tx: invoke_tx, tx_hash, only_query: true } - } - implement_inner_tx_getter_calls!( + (max_fee, Fee), (calldata, Calldata), - (signature, TransactionSignature), - (sender_address, ContractAddress) + (signature, TransactionSignature) ); - - pub fn get_account_tx_context(&self) -> AccountTransactionContext { - let common_fields = CommonAccountFields { - transaction_hash: self.tx_hash, - version: self.tx.version(), - signature: self.tx.signature(), - nonce: self.tx.nonce(), - sender_address: self.tx.sender_address(), - only_query: self.only_query, - }; - - match &self.tx { - starknet_api::transaction::InvokeTransaction::V0(tx) => { - AccountTransactionContext::Deprecated(DeprecatedAccountTransactionContext { - common_fields, - max_fee: tx.max_fee, - }) - } - starknet_api::transaction::InvokeTransaction::V1(tx) => { - AccountTransactionContext::Deprecated(DeprecatedAccountTransactionContext { - common_fields, - max_fee: tx.max_fee, - }) - } - starknet_api::transaction::InvokeTransaction::V3(tx) => { - AccountTransactionContext::Current(CurrentAccountTransactionContext { - common_fields, - resource_bounds: tx.resource_bounds.clone(), - tip: tx.tip, - nonce_data_availability_mode: tx.nonce_data_availability_mode, - fee_data_availability_mode: tx.fee_data_availability_mode, - paymaster_data: tx.paymaster_data.clone(), - account_deployment_data: tx.account_deployment_data.clone(), - }) - } - } - } } impl Executable for InvokeTransaction { @@ -418,12 +278,11 @@ impl Executable for InvokeTransaction { ) -> TransactionExecutionResult> { let entry_point_selector = match &self.tx { starknet_api::transaction::InvokeTransaction::V0(tx) => tx.entry_point_selector, - starknet_api::transaction::InvokeTransaction::V1(_) - | starknet_api::transaction::InvokeTransaction::V3(_) => { + starknet_api::transaction::InvokeTransaction::V1(_) => { selector_from_name(constants::EXECUTE_ENTRY_POINT_NAME) } }; - let storage_address = context.account_tx_context.sender_address(); + let storage_address = context.account_tx_context.sender_address; let execute_call = CallEntryPoint { entry_point_type: EntryPointType::External, entry_point_selector, @@ -452,32 +311,6 @@ pub struct L1HandlerTransaction { pub paid_fee_on_l1: Fee, } -impl L1HandlerTransaction { - pub fn get_account_tx_context(&self) -> AccountTransactionContext { - AccountTransactionContext::Deprecated(DeprecatedAccountTransactionContext { - common_fields: CommonAccountFields { - transaction_hash: self.tx_hash, - version: self.tx.version, - signature: TransactionSignature::default(), - nonce: self.tx.nonce, - sender_address: self.tx.contract_address, - only_query: false, - }, - max_fee: Fee::default(), - }) - } -} - -impl HasRelatedFeeType for L1HandlerTransaction { - fn version(&self) -> TransactionVersion { - self.tx.version - } - - fn is_l1_handler(&self) -> bool { - true - } -} - impl Executable for L1HandlerTransaction { fn run_execute( &self,