From 79e288c205a9456ce6c033f3bdf0d28b24a7a591 Mon Sep 17 00:00:00 2001 From: marioiordanov Date: Tue, 22 Oct 2024 11:46:10 +0300 Subject: [PATCH] SimulateTransactions integration tests (#628) * remove handle_transaction_result, refactor errors * integration tests * added new error type * typo * fixed unit tests * fixed integration test * remove fully qualified names in errors file * changed test name * reuse variable * replace declare_deploy_v1 with declare_v3_deploy_v3 * add simulate transaction validation check * add functionality * refactor if else with match * remove trait implementation * changed error variant * commment --- crates/starknet-devnet-core/src/error.rs | 66 +- .../src/starknet/add_declare_transaction.rs | 40 +- .../add_deploy_account_transaction.rs | 30 +- .../src/starknet/add_invoke_transaction.rs | 38 +- .../starknet/add_l1_handler_transaction.rs | 8 +- .../src/starknet/estimations.rs | 25 +- .../src/starknet/events.rs | 8 +- .../starknet-devnet-core/src/starknet/mod.rs | 107 +-- .../src/api/json_rpc/endpoints.rs | 14 +- .../src/api/json_rpc/error.rs | 10 + .../src/rpc/transactions.rs | 58 +- crates/starknet-devnet/tests/common/utils.rs | 36 +- .../tests/test_account_impersonation.rs | 2 +- .../tests/test_estimate_fee.rs | 219 ++++- crates/starknet-devnet/tests/test_fork.rs | 30 +- .../tests/test_gas_modification.rs | 16 +- .../starknet-devnet/tests/test_old_state.rs | 38 +- .../tests/test_simulate_transactions.rs | 773 ++++++++++++++++-- .../tests/test_v3_transactions.rs | 34 +- 19 files changed, 1202 insertions(+), 350 deletions(-) diff --git a/crates/starknet-devnet-core/src/error.rs b/crates/starknet-devnet-core/src/error.rs index 52c217da2..acffa49b7 100644 --- a/crates/starknet-devnet-core/src/error.rs +++ b/crates/starknet-devnet-core/src/error.rs @@ -1,3 +1,7 @@ +use blockifier::fee::fee_checks::FeeCheckError; +use blockifier::transaction::errors::{ + TransactionExecutionError, TransactionFeeError, TransactionPreValidationError, +}; use starknet_rs_core::types::{BlockId, Felt}; use starknet_types; use starknet_types::contract_address::ContractAddress; @@ -13,11 +17,11 @@ pub enum Error { #[error(transparent)] BlockifierStateError(#[from] blockifier::state::errors::StateError), #[error(transparent)] - BlockifierTransactionError(#[from] blockifier::transaction::errors::TransactionExecutionError), + BlockifierTransactionError(TransactionExecutionError), #[error(transparent)] BlockifierExecutionError(#[from] blockifier::execution::errors::EntryPointExecutionError), - #[error("{revert_error}")] - ExecutionError { revert_error: String }, + #[error("{execution_error}")] + ExecutionError { execution_error: String, index: usize }, #[error("Types error: {0}")] TypesError(#[from] starknet_types::error::Error), #[error("I/O error: {0}")] @@ -56,14 +60,10 @@ pub enum Error { SerializationError { origin: String }, #[error("Serialization not supported: {obj_name}")] SerializationNotSupported { obj_name: String }, - #[error( - "{tx_type}: max_fee cannot be zero (exception is v3 transaction where l2 gas must be zero)" - )] - MaxFeeZeroError { tx_type: String }, #[error(transparent)] TransactionValidationError(#[from] TransactionValidationError), #[error(transparent)] - TransactionFeeError(#[from] blockifier::transaction::errors::TransactionFeeError), + TransactionFeeError(blockifier::transaction::errors::TransactionFeeError), #[error(transparent)] MessagingError(#[from] MessagingError), #[error("Transaction has no trace")] @@ -104,6 +104,56 @@ pub enum TransactionValidationError { ValidationFailure { reason: String }, } +impl From for Error { + fn from(value: TransactionExecutionError) -> Self { + match value { + TransactionExecutionError::TransactionPreValidationError( + TransactionPreValidationError::InvalidNonce { .. }, + ) => TransactionValidationError::InvalidTransactionNonce.into(), + TransactionExecutionError::FeeCheckError(err) => err.into(), + TransactionExecutionError::TransactionPreValidationError( + TransactionPreValidationError::TransactionFeeError(err), + ) => err.into(), + TransactionExecutionError::TransactionFeeError(err) => err.into(), + TransactionExecutionError::ValidateTransactionError { .. } => { + TransactionValidationError::ValidationFailure { reason: value.to_string() }.into() + } + other => Self::BlockifierTransactionError(other), + } + } +} + +impl From for Error { + fn from(value: FeeCheckError) -> Self { + match value { + FeeCheckError::MaxL1GasAmountExceeded { .. } | FeeCheckError::MaxFeeExceeded { .. } => { + TransactionValidationError::InsufficientMaxFee.into() + } + FeeCheckError::InsufficientFeeTokenBalance { .. } => { + TransactionValidationError::InsufficientAccountBalance.into() + } + } + } +} + +impl From for Error { + fn from(value: TransactionFeeError) -> Self { + match value { + TransactionFeeError::FeeTransferError { .. } + | TransactionFeeError::MaxFeeTooLow { .. } + | TransactionFeeError::MaxL1GasPriceTooLow { .. } + | TransactionFeeError::MaxL1GasAmountTooLow { .. } => { + TransactionValidationError::InsufficientMaxFee.into() + } + TransactionFeeError::MaxFeeExceedsBalance { .. } + | TransactionFeeError::L1GasBoundsExceedBalance { .. } => { + TransactionValidationError::InsufficientAccountBalance.into() + } + err => Error::TransactionFeeError(err), + } + } +} + #[derive(Debug, Error)] pub enum MessagingError { #[error( diff --git a/crates/starknet-devnet-core/src/starknet/add_declare_transaction.rs b/crates/starknet-devnet-core/src/starknet/add_declare_transaction.rs index 540b1dc05..d003c2aee 100644 --- a/crates/starknet-devnet-core/src/starknet/add_declare_transaction.rs +++ b/crates/starknet-devnet-core/src/starknet/add_declare_transaction.rs @@ -8,7 +8,7 @@ use starknet_types::rpc::transactions::{ BroadcastedDeclareTransaction, DeclareTransaction, Transaction, TransactionWithHash, }; -use crate::error::{DevnetResult, Error}; +use crate::error::{DevnetResult, Error, TransactionValidationError}; use crate::starknet::Starknet; use crate::state::CustomState; use crate::utils::calculate_casm_hash; @@ -18,9 +18,7 @@ pub fn add_declare_transaction( broadcasted_declare_transaction: BroadcastedDeclareTransaction, ) -> DevnetResult<(TransactionHash, ClassHash)> { if broadcasted_declare_transaction.is_max_fee_zero_value() { - return Err(Error::MaxFeeZeroError { - tx_type: broadcasted_declare_transaction.to_string(), - }); + return Err(TransactionValidationError::InsufficientMaxFee.into()); } if broadcasted_declare_transaction.is_only_query() { @@ -79,7 +77,7 @@ pub fn add_declare_transaction( )?); let transaction = TransactionWithHash::new(transaction_hash, declare_transaction); - let blockifier_execution_result = + let blockifier_execution_info = blockifier::transaction::account_transaction::AccountTransaction::Declare( blockifier_declare_transaction, ) @@ -88,16 +86,15 @@ pub fn add_declare_transaction( &starknet.block_context, true, validate, - ); + )?; // if tx successful, store the class - if blockifier_execution_result.as_ref().is_ok_and(|res| !res.is_reverted()) { + if !blockifier_execution_info.is_reverted() { let state = starknet.get_state(); state.declare_contract_class(class_hash, casm_hash, contract_class)?; } - // do the steps required in all transactions - starknet.handle_transaction_result(transaction, blockifier_execution_result)?; + starknet.handle_accepted_transaction(transaction, blockifier_execution_info)?; Ok((transaction_hash, class_hash)) } @@ -149,6 +146,7 @@ mod tests { use starknet_types::rpc::transactions::BroadcastedDeclareTransaction; use starknet_types::traits::HashProducer; + use crate::error::{Error, TransactionValidationError}; use crate::starknet::tests::setup_starknet_with_no_signature_check_account; use crate::starknet::Starknet; use crate::state::{BlockNumberOrPending, CustomStateReader}; @@ -221,13 +219,7 @@ mod tests { assert!(result.is_err()); match result.err().unwrap() { - err @ crate::error::Error::MaxFeeZeroError { .. } => { - assert_eq!( - err.to_string(), - "Declare transaction V3: max_fee cannot be zero (exception is v3 transaction \ - where l2 gas must be zero)" - ) - } + Error::TransactionValidationError(TransactionValidationError::InsufficientMaxFee) => {} _ => panic!("Wrong error type"), } } @@ -250,13 +242,7 @@ mod tests { assert!(result.is_err()); match result.err().unwrap() { - err @ crate::error::Error::MaxFeeZeroError { .. } => { - assert_eq!( - err.to_string(), - "Declare transaction V2: max_fee cannot be zero (exception is v3 transaction \ - where l2 gas must be zero)" - ) - } + Error::TransactionValidationError(TransactionValidationError::InsufficientMaxFee) => {} _ => panic!("Wrong error type"), } } @@ -397,13 +383,7 @@ mod tests { assert!(result.is_err()); match result.err().unwrap() { - err @ crate::error::Error::MaxFeeZeroError { .. } => { - assert_eq!( - err.to_string(), - "Declare transaction V1: max_fee cannot be zero (exception is v3 transaction \ - where l2 gas must be zero)" - ) - } + Error::TransactionValidationError(TransactionValidationError::InsufficientMaxFee) => {} _ => panic!("Wrong error type"), } } diff --git a/crates/starknet-devnet-core/src/starknet/add_deploy_account_transaction.rs b/crates/starknet-devnet-core/src/starknet/add_deploy_account_transaction.rs index ec1849b71..ae1be0a24 100644 --- a/crates/starknet-devnet-core/src/starknet/add_deploy_account_transaction.rs +++ b/crates/starknet-devnet-core/src/starknet/add_deploy_account_transaction.rs @@ -8,7 +8,7 @@ use starknet_types::rpc::transactions::{ }; use super::Starknet; -use crate::error::{DevnetResult, Error}; +use crate::error::{DevnetResult, Error, TransactionValidationError}; use crate::state::CustomStateReader; pub fn add_deploy_account_transaction( @@ -16,9 +16,7 @@ pub fn add_deploy_account_transaction( broadcasted_deploy_account_transaction: BroadcastedDeployAccountTransaction, ) -> DevnetResult<(TransactionHash, ContractAddress)> { if broadcasted_deploy_account_transaction.is_max_fee_zero_value() { - return Err(Error::MaxFeeZeroError { - tx_type: broadcasted_deploy_account_transaction.to_string(), - }); + return Err(TransactionValidationError::InsufficientMaxFee.into()); } if broadcasted_deploy_account_transaction.is_only_query() { @@ -57,13 +55,13 @@ pub fn add_deploy_account_transaction( let transaction_hash = blockifier_deploy_account_transaction.tx_hash.0; let transaction = TransactionWithHash::new(transaction_hash, deploy_account_transaction); - let blockifier_execution_result = + let blockifier_execution_info = blockifier::transaction::account_transaction::AccountTransaction::DeployAccount( blockifier_deploy_account_transaction, ) - .execute(&mut starknet.pending_state.state, &starknet.block_context, true, true); + .execute(&mut starknet.pending_state.state, &starknet.block_context, true, true)?; - starknet.handle_transaction_result(transaction, blockifier_execution_result)?; + starknet.handle_accepted_transaction(transaction, blockifier_execution_info)?; Ok((transaction_hash, address)) } @@ -90,7 +88,7 @@ mod tests { self, DEVNET_DEFAULT_CHAIN_ID, DEVNET_DEFAULT_STARTING_BLOCK_NUMBER, ETH_ERC20_CONTRACT_ADDRESS, STRK_ERC20_CONTRACT_ADDRESS, }; - use crate::error::Error; + use crate::error::{Error, TransactionValidationError}; use crate::starknet::{predeployed, Starknet}; use crate::state::CustomState; use crate::traits::{Deployed, HashIdentifiedMut}; @@ -157,13 +155,7 @@ mod tests { assert!(result.is_err()); match result.err().unwrap() { - err @ crate::error::Error::MaxFeeZeroError { .. } => { - assert_eq!( - err.to_string(), - "Deploy account transaction V1: max_fee cannot be zero (exception is v3 \ - transaction where l2 gas must be zero)" - ) - } + Error::TransactionValidationError(TransactionValidationError::InsufficientMaxFee) => {} _ => panic!("Wrong error type"), } } @@ -180,13 +172,7 @@ mod tests { )) .unwrap_err(); match txn_err { - err @ crate::error::Error::MaxFeeZeroError { .. } => { - assert_eq!( - err.to_string(), - "Deploy account transaction V3: max_fee cannot be zero (exception is v3 \ - transaction where l2 gas must be zero)" - ) - } + Error::TransactionValidationError(TransactionValidationError::InsufficientMaxFee) => {} _ => panic!("Wrong error type"), } } diff --git a/crates/starknet-devnet-core/src/starknet/add_invoke_transaction.rs b/crates/starknet-devnet-core/src/starknet/add_invoke_transaction.rs index 4203245c4..96d17c785 100644 --- a/crates/starknet-devnet-core/src/starknet/add_invoke_transaction.rs +++ b/crates/starknet-devnet-core/src/starknet/add_invoke_transaction.rs @@ -8,14 +8,14 @@ use starknet_types::rpc::transactions::{ }; use super::Starknet; -use crate::error::{DevnetResult, Error}; +use crate::error::{DevnetResult, Error, TransactionValidationError}; pub fn add_invoke_transaction( starknet: &mut Starknet, broadcasted_invoke_transaction: BroadcastedInvokeTransaction, ) -> DevnetResult { if broadcasted_invoke_transaction.is_max_fee_zero_value() { - return Err(Error::MaxFeeZeroError { tx_type: broadcasted_invoke_transaction.to_string() }); + return Err(TransactionValidationError::InsufficientMaxFee.into()); } if broadcasted_invoke_transaction.is_only_query() { @@ -48,15 +48,15 @@ pub fn add_invoke_transaction( let state = &mut starknet.get_state().state; - let blockifier_execution_result = + let blockifier_execution_info = blockifier::transaction::account_transaction::AccountTransaction::Invoke( blockifier_invoke_transaction, ) - .execute(state, &block_context, true, validate); + .execute(state, &block_context, true, validate)?; let transaction = TransactionWithHash::new(transaction_hash, invoke_transaction); - starknet.handle_transaction_result(transaction, blockifier_execution_result)?; + starknet.handle_accepted_transaction(transaction, blockifier_execution_info)?; Ok(transaction_hash) } @@ -89,6 +89,7 @@ mod tests { self, DEVNET_DEFAULT_CHAIN_ID, DEVNET_DEFAULT_STARTING_BLOCK_NUMBER, ETH_ERC20_CONTRACT_ADDRESS, }; + use crate::error::{Error, TransactionValidationError}; use crate::starknet::{predeployed, Starknet}; use crate::state::CustomState; use crate::traits::{Accounted, Deployed, HashIdentifiedMut}; @@ -206,14 +207,7 @@ mod tests { .expect_err("Expected MaxFeeZeroError"); match invoke_v3_txn_error { - err @ crate::error::Error::MaxFeeZeroError { .. } => { - assert_eq!( - err.to_string(), - "Invoke transaction V3: max_fee cannot be zero (exception is v3 transaction \ - where l2 gas must be zero)" - .to_string() - ); - } + Error::TransactionValidationError(TransactionValidationError::InsufficientMaxFee) => {} _ => panic!("Wrong error type"), } } @@ -282,13 +276,9 @@ mod tests { assert!(transaction.is_err()); match transaction.err().unwrap() { - err @ crate::error::Error::MaxFeeZeroError { .. } => { - assert_eq!( - err.to_string(), - "Invoke transaction V3: max_fee cannot be zero (exception is v3 \ - transaction where l2 gas must be zero)" - ) - } + Error::TransactionValidationError( + TransactionValidationError::InsufficientMaxFee, + ) => {} _ => { panic!("Wrong error type") } @@ -389,13 +379,7 @@ mod tests { assert!(result.is_err()); match result.err().unwrap() { - err @ crate::error::Error::MaxFeeZeroError { .. } => { - assert_eq!( - err.to_string(), - "Invoke transaction V1: max_fee cannot be zero (exception is v3 transaction \ - where l2 gas must be zero)" - ) - } + Error::TransactionValidationError(TransactionValidationError::InsufficientMaxFee) => {} _ => panic!("Wrong error type"), } } diff --git a/crates/starknet-devnet-core/src/starknet/add_l1_handler_transaction.rs b/crates/starknet-devnet-core/src/starknet/add_l1_handler_transaction.rs index 60c7bde80..5f1c01a46 100644 --- a/crates/starknet-devnet-core/src/starknet/add_l1_handler_transaction.rs +++ b/crates/starknet-devnet-core/src/starknet/add_l1_handler_transaction.rs @@ -22,16 +22,16 @@ pub fn add_l1_handler_transaction( let charge_fee = false; let validate = true; - let blockifier_execution_result = blockifier_transaction.execute( + let blockifier_execution_info = blockifier_transaction.execute( &mut starknet.pending_state.state, &starknet.block_context, charge_fee, validate, - ); + )?; - starknet.handle_transaction_result( + starknet.handle_accepted_transaction( TransactionWithHash::new(transaction_hash, Transaction::L1Handler(transaction.clone())), - blockifier_execution_result, + blockifier_execution_info, )?; Ok(transaction_hash) diff --git a/crates/starknet-devnet-core/src/starknet/estimations.rs b/crates/starknet-devnet-core/src/starknet/estimations.rs index b864a3918..c868720bc 100644 --- a/crates/starknet-devnet-core/src/starknet/estimations.rs +++ b/crates/starknet-devnet-core/src/starknet/estimations.rs @@ -21,6 +21,7 @@ pub fn estimate_fee( transactions: &[BroadcastedTransaction], charge_fee: Option, validate: Option, + return_error_on_reverted_execution: bool, ) -> DevnetResult> { let chain_id = starknet.chain_id().to_felt(); let block_context = starknet.block_context.clone(); @@ -45,8 +46,9 @@ pub fn estimate_fee( transactions .into_iter() - .map(|(transaction, skip_validate_due_to_impersonation)| { - estimate_transaction_fee( + .enumerate() + .map(|(idx,(transaction, skip_validate_due_to_impersonation))| { + let estimate_fee_result = estimate_transaction_fee( &mut transactional_state, &block_context, blockifier::transaction::transaction_execution::Transaction::AccountTransaction( @@ -58,7 +60,15 @@ pub fn estimate_fee( * has to skip validation, because * the sender is impersonated. * Otherwise use the validate parameter that is passed to the estimateFee request */ - ) + return_error_on_reverted_execution + ); + + match estimate_fee_result { + Ok(estimated_fee) => Ok(estimated_fee), + // reverted transactions are failing with ExecutionError, but index is set to 0, so we override the index property + Err(Error::ExecutionError { execution_error , ..}) => Err(Error::ExecutionError { execution_error, index: idx }), + Err(err) => Err(Error::ExecutionError { execution_error: err.to_string(), index: idx }), + } }) .collect() } @@ -88,6 +98,7 @@ pub fn estimate_message_fee( ), None, None, + true, ) } @@ -97,6 +108,7 @@ fn estimate_transaction_fee( transaction: blockifier::transaction::transaction_execution::Transaction, charge_fee: Option, validate: Option, + return_error_on_reverted_execution: bool, ) -> DevnetResult { let fee_type = match transaction { blockifier::transaction::transaction_execution::Transaction::AccountTransaction(ref tx) => { @@ -114,8 +126,11 @@ fn estimate_transaction_fee( validate.unwrap_or(true), )?; - if let Some(revert_error) = transaction_execution_info.revert_error { - return Err(Error::ExecutionError { revert_error }); + // reverted transactions can only be Invoke transactions + if let (true, Some(revert_error)) = + (return_error_on_reverted_execution, transaction_execution_info.revert_error) + { + return Err(Error::ExecutionError { execution_error: revert_error, index: 0 }); } let gas_vector = transaction_execution_info diff --git a/crates/starknet-devnet-core/src/starknet/events.rs b/crates/starknet-devnet-core/src/starknet/events.rs index 186a87800..34e2b263a 100644 --- a/crates/starknet-devnet-core/src/starknet/events.rs +++ b/crates/starknet-devnet-core/src/starknet/events.rs @@ -128,6 +128,7 @@ mod tests { use starknet_rs_core::types::{BlockId, Felt}; use starknet_types::contract_address::ContractAddress; use starknet_types::emitted_event::Event; + use starknet_types::rpc::transactions::TransactionWithHash; use super::{check_if_filter_applies_for_event, get_events}; use crate::starknet::events::check_if_filter_applies_for_event_keys; @@ -391,7 +392,7 @@ mod tests { // each transaction should have events count equal to the order of the transaction let mut starknet = Starknet::new(&StarknetConfig::default()).unwrap(); - let transaction = dummy_declare_transaction_v1(); + let mut transaction = dummy_declare_transaction_v1(); for idx in 0..5 { let txn_info = blockifier::transaction::objects::TransactionExecutionInfo { @@ -399,10 +400,9 @@ mod tests { ..Default::default() }; let transaction_hash = Felt::from(idx as u128 + 100); + transaction = TransactionWithHash::new(transaction_hash, transaction.transaction); - starknet - .handle_accepted_transaction(&transaction_hash, &transaction, txn_info) - .unwrap(); + starknet.handle_accepted_transaction(transaction.clone(), txn_info).unwrap(); } assert_eq!(starknet.blocks.get_blocks(None, None).unwrap().len(), 6); diff --git a/crates/starknet-devnet-core/src/starknet/mod.rs b/crates/starknet-devnet-core/src/starknet/mod.rs index 7bcec217d..f89b8de23 100644 --- a/crates/starknet-devnet-core/src/starknet/mod.rs +++ b/crates/starknet-devnet-core/src/starknet/mod.rs @@ -7,7 +7,6 @@ use blockifier::execution::entry_point::CallEntryPoint; use blockifier::state::cached_state::CachedState; use blockifier::state::state_api::StateReader; use blockifier::transaction::account_transaction::AccountTransaction; -use blockifier::transaction::errors::TransactionPreValidationError; use blockifier::transaction::objects::TransactionExecutionInfo; use blockifier::transaction::transactions::ExecutableTransaction; use parking_lot::RwLock; @@ -381,76 +380,15 @@ impl Starknet { Ok(state_diff) } - /// Handles transaction result either Ok or Error and updates the state accordingly. - /// - /// # Arguments - /// - /// * `transaction` - Transaction to be added in the collection of transactions. - /// * `contract_class` - Contract class to be added in the state cache. Only in declare - /// transactions. - /// * `transaction_result` - Result with transaction_execution_info - pub(crate) fn handle_transaction_result( - &mut self, - transaction: TransactionWithHash, - transaction_result: Result< - TransactionExecutionInfo, - blockifier::transaction::errors::TransactionExecutionError, - >, - ) -> DevnetResult<()> { - let transaction_hash = *transaction.get_transaction_hash(); - - match transaction_result { - Ok(tx_info) => { - self.handle_accepted_transaction(&transaction_hash, &transaction, tx_info) - } - Err(tx_err) => { - /// utility to avoid duplication - fn match_tx_fee_error( - err: blockifier::transaction::errors::TransactionFeeError, - ) -> DevnetResult<()> { - match err { - blockifier::transaction::errors::TransactionFeeError::FeeTransferError { .. } - | blockifier::transaction::errors::TransactionFeeError::MaxFeeTooLow { .. } => Err( - TransactionValidationError::InsufficientMaxFee.into() - ), - blockifier::transaction::errors::TransactionFeeError::MaxFeeExceedsBalance { .. } | blockifier::transaction::errors::TransactionFeeError::L1GasBoundsExceedBalance { .. } => Err( - TransactionValidationError::InsufficientAccountBalance.into() - ), - _ => Err(err.into()) - } - } - - // based on this https://community.starknet.io/t/efficient-utilization-of-sequencer-capacity-in-starknet-v0-12-1/95607#the-validation-phase-in-the-gateway-5 - // we should not save transactions that failed with one of the following errors - match tx_err { - blockifier::transaction::errors::TransactionExecutionError::TransactionPreValidationError( - TransactionPreValidationError::InvalidNonce { .. } - ) => Err(TransactionValidationError::InvalidTransactionNonce.into()), - blockifier::transaction::errors::TransactionExecutionError::FeeCheckError { .. } => - Err(TransactionValidationError::InsufficientMaxFee.into()), - blockifier::transaction::errors::TransactionExecutionError::TransactionPreValidationError( - TransactionPreValidationError::TransactionFeeError(err) - ) => match_tx_fee_error(err), - blockifier::transaction::errors::TransactionExecutionError::TransactionFeeError(err) - => match_tx_fee_error(err), - blockifier::transaction::errors::TransactionExecutionError::ValidateTransactionError { .. } => { - Err(TransactionValidationError::ValidationFailure { reason: tx_err.to_string() }.into()) - } - _ => Err(tx_err.into()) - } - } - } - } - /// Handles succeeded and reverted transactions. The tx is stored and potentially dumped. A new /// block is generated in block-generation-on-transaction mode. pub(crate) fn handle_accepted_transaction( &mut self, - transaction_hash: &TransactionHash, - transaction: &TransactionWithHash, + transaction: TransactionWithHash, tx_info: TransactionExecutionInfo, ) -> DevnetResult<()> { let state_diff = self.commit_diff()?; + let transaction_hash = transaction.get_transaction_hash(); let trace = create_trace( &mut self.pending_state.state, @@ -458,7 +396,7 @@ impl Starknet { &tx_info, state_diff.clone().into(), )?; - let transaction_to_add = StarknetTransaction::create_accepted(transaction, tx_info, trace); + let transaction_to_add = StarknetTransaction::create_accepted(&transaction, tx_info, trace); // add accepted transaction to pending block self.blocks.pending_block.add_transaction(*transaction_hash); @@ -712,7 +650,7 @@ impl Starknet { skip_validate = true; } } - estimations::estimate_fee(self, block_id, transactions, None, Some(!skip_validate)) + estimations::estimate_fee(self, block_id, transactions, None, Some(!skip_validate), true) } pub fn estimate_message_fee( @@ -1164,7 +1102,20 @@ impl Starknet { let blockifier_transactions = { transactions .iter() - .map(|txn| { + .enumerate() + .map(|(idx, txn)| { + // According to this conversation https://spaceshard.slack.com/archives/C03HL8DH52N/p1710683496750409, simulating a transaction will: + // fail if the fee provided is 0 + // succeed if the fee provided is 0 and SKIP_FEE_CHARGE is set + // succeed if the fee provided is > 0 + if txn.is_max_fee_zero_value() && !skip_fee_charge { + return Err(Error::ExecutionError { + execution_error: TransactionValidationError::InsufficientMaxFee + .to_string(), + index: idx, + }); + } + Ok(( txn.to_blockifier_account_transaction(&chain_id, true)?, txn.get_type(), @@ -1181,15 +1132,20 @@ impl Starknet { let mut transactional_state = CachedState::new(CachedState::create_transactional(&mut state.state)); - for (blockifier_transaction, transaction_type, skip_validate_due_to_impersonation) in - blockifier_transactions.into_iter() + for (idx, (blockifier_transaction, transaction_type, skip_validate_due_to_impersonation)) in + blockifier_transactions.into_iter().enumerate() { - let tx_execution_info = blockifier_transaction.execute( - &mut transactional_state, - &block_context, - !skip_fee_charge, - !(skip_validate || skip_validate_due_to_impersonation), - )?; + let tx_execution_info = blockifier_transaction + .execute( + &mut transactional_state, + &block_context, + !skip_fee_charge, + !(skip_validate || skip_validate_due_to_impersonation), + ) + .map_err(|err| Error::ExecutionError { + execution_error: Error::from(err).to_string(), + index: idx, + })?; let block_number = block_context.block_info().block_number.0; let new_classes = transactional_rpc_contract_classes.write().commit(block_number); @@ -1210,6 +1166,7 @@ impl Starknet { transactions, Some(!skip_fee_charge), Some(!skip_validate), + false, )?; // if the underlying simulation is correct, this should never be the case diff --git a/crates/starknet-devnet-server/src/api/json_rpc/endpoints.rs b/crates/starknet-devnet-server/src/api/json_rpc/endpoints.rs index 207bb7943..230bd9d9a 100644 --- a/crates/starknet-devnet-server/src/api/json_rpc/endpoints.rs +++ b/crates/starknet-devnet-server/src/api/json_rpc/endpoints.rs @@ -310,7 +310,10 @@ impl JsonRpcHandler { Err(e @ Error::NoStateAtBlock { .. }) => { Err(ApiError::NoStateAtBlock { msg: e.to_string() }) } - Err(err) => Err(ApiError::ContractError { error: err }), + Err(Error::ExecutionError { execution_error, index }) => { + Err(ApiError::ExecutionError { execution_error, index }) + } + Err(err) => Err(err.into()), } } @@ -428,14 +431,19 @@ impl JsonRpcHandler { ) -> StrictRpcResult { // borrowing as write/mutable because trace calculation requires so let mut starknet = self.api.starknet.lock().await; - match starknet.simulate_transactions(block_id.as_ref(), &transactions, simulation_flags) { + let res = + starknet.simulate_transactions(block_id.as_ref(), &transactions, simulation_flags); + match res { Ok(result) => Ok(StarknetResponse::SimulateTransactions(result).into()), Err(Error::ContractNotFound) => Err(ApiError::ContractNotFound), Err(Error::NoBlock) => Err(ApiError::BlockNotFound), Err(e @ Error::NoStateAtBlock { .. }) => { Err(ApiError::NoStateAtBlock { msg: e.to_string() }) } - Err(err) => Err(ApiError::ContractError { error: err }), + Err(Error::ExecutionError { execution_error, index }) => { + Err(ApiError::ExecutionError { execution_error, index }) + } + Err(err) => Err(err.into()), } } diff --git a/crates/starknet-devnet-server/src/api/json_rpc/error.rs b/crates/starknet-devnet-server/src/api/json_rpc/error.rs index 44728c52d..ff1a29470 100644 --- a/crates/starknet-devnet-server/src/api/json_rpc/error.rs +++ b/crates/starknet-devnet-server/src/api/json_rpc/error.rs @@ -60,6 +60,8 @@ pub enum ApiError { HttpApiError(#[from] HttpApiError), #[error("the compiled class hash did not match the one supplied in the transaction")] CompiledClassHashMismatch, + #[error("Transaction execution error")] + ExecutionError { execution_error: String, index: usize }, } impl ApiError { @@ -198,6 +200,14 @@ impl ApiError { message: error_message.into(), data: None, }, + ApiError::ExecutionError { execution_error, index } => RpcError { + code: crate::rpc_core::error::ErrorCode::ServerError(41), + message: error_message.into(), + data: Some(json!({ + "transaction_index": index, + "execution_error": execution_error + })), + }, ApiError::HttpApiError(http_api_error) => http_api_error.http_api_error_to_rpc_error(), } } diff --git a/crates/starknet-devnet-types/src/rpc/transactions.rs b/crates/starknet-devnet-types/src/rpc/transactions.rs index 6f3c775ca..bde9c9da8 100644 --- a/crates/starknet-devnet-types/src/rpc/transactions.rs +++ b/crates/starknet-devnet-types/src/rpc/transactions.rs @@ -1,4 +1,3 @@ -use core::fmt; use std::collections::BTreeMap; use std::sync::Arc; @@ -100,18 +99,6 @@ pub enum TransactionType { L1Handler, } -impl fmt::Display for TransactionType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TransactionType::Declare => write!(f, "Declare transaction"), - TransactionType::Deploy => write!(f, "Deploy transaction"), - TransactionType::DeployAccount => write!(f, "Deploy account transaction"), - TransactionType::Invoke => write!(f, "Invoke transaction"), - TransactionType::L1Handler => write!(f, "L1 handler transaction"), - } - } -} - #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", deny_unknown_fields, rename_all = "SCREAMING_SNAKE_CASE")] pub enum Transaction { @@ -494,6 +481,20 @@ impl BroadcastedTransaction { BroadcastedTransaction::DeployAccount(_) => TransactionType::DeployAccount, } } + + pub fn is_max_fee_zero_value(&self) -> bool { + match self { + BroadcastedTransaction::Invoke(broadcasted_invoke_transaction) => { + broadcasted_invoke_transaction.is_max_fee_zero_value() + } + BroadcastedTransaction::Declare(broadcasted_declare_transaction) => { + broadcasted_declare_transaction.is_max_fee_zero_value() + } + BroadcastedTransaction::DeployAccount(broadcasted_deploy_account_transaction) => { + broadcasted_deploy_account_transaction.is_max_fee_zero_value() + } + } + } } #[derive(Debug, Clone, Serialize)] @@ -504,17 +505,6 @@ pub enum BroadcastedDeclareTransaction { V3(Box), } -impl fmt::Display for BroadcastedDeclareTransaction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let txn_type = TransactionType::Declare; - match self { - BroadcastedDeclareTransaction::V1(_) => write!(f, "{} V1", txn_type), - BroadcastedDeclareTransaction::V2(_) => write!(f, "{} V2", txn_type), - BroadcastedDeclareTransaction::V3(_) => write!(f, "{} V3", txn_type), - } - } -} - impl BroadcastedDeclareTransaction { pub fn is_max_fee_zero_value(&self) -> bool { match self { @@ -658,16 +648,6 @@ pub enum BroadcastedDeployAccountTransaction { V3(BroadcastedDeployAccountTransactionV3), } -impl fmt::Display for BroadcastedDeployAccountTransaction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let txn_type = TransactionType::DeployAccount; - match self { - BroadcastedDeployAccountTransaction::V1(_) => write!(f, "{} V1", txn_type), - BroadcastedDeployAccountTransaction::V3(_) => write!(f, "{} V3", txn_type), - } - } -} - impl BroadcastedDeployAccountTransaction { pub fn is_max_fee_zero_value(&self) -> bool { match self { @@ -794,16 +774,6 @@ pub enum BroadcastedInvokeTransaction { V3(BroadcastedInvokeTransactionV3), } -impl fmt::Display for BroadcastedInvokeTransaction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let txn_type = TransactionType::Invoke; - match self { - BroadcastedInvokeTransaction::V1(_) => write!(f, "{} V1", txn_type), - BroadcastedInvokeTransaction::V3(_) => write!(f, "{} V3", txn_type), - } - } -} - impl BroadcastedInvokeTransaction { pub fn is_max_fee_zero_value(&self) -> bool { match self { diff --git a/crates/starknet-devnet/tests/common/utils.rs b/crates/starknet-devnet/tests/common/utils.rs index 64146d8dd..e9b3909bc 100644 --- a/crates/starknet-devnet/tests/common/utils.rs +++ b/crates/starknet-devnet/tests/common/utils.rs @@ -15,8 +15,8 @@ use starknet_rs_accounts::{ use starknet_rs_contract::ContractFactory; use starknet_rs_core::types::contract::SierraClass; use starknet_rs_core::types::{ - BlockId, BlockTag, ContractClass, DeployAccountTransactionResult, ExecutionResult, Felt, - FlattenedSierraClass, FunctionCall, + BlockId, BlockTag, ContractClass, DeployAccountTransactionResult, ExecutionResult, FeeEstimate, + Felt, FlattenedSierraClass, FunctionCall, NonZeroFelt, }; use starknet_rs_core::utils::{get_selector_from_name, get_udc_deployed_address}; use starknet_rs_providers::jsonrpc::HttpTransport; @@ -238,30 +238,22 @@ impl Drop for UniqueAutoDeletableFile { } /// Declares and deploys a Cairo 1 contract; returns class hash and contract address -pub async fn declare_deploy_v1( - account: Arc, LocalWallet>>, +pub async fn declare_v3_deploy_v3( + account: &SingleOwnerAccount<&JsonRpcClient, LocalWallet>, contract_class: FlattenedSierraClass, casm_hash: Felt, ctor_args: &[Felt], ) -> Result<(Felt, Felt), anyhow::Error> { - // declare the contract - let declaration_result = account - .declare_v2(Arc::new(contract_class), casm_hash) - .max_fee(Felt::from(1e18 as u128)) - .send() - .await?; + let salt = Felt::ZERO; + let declaration_result = account.declare_v3(Arc::new(contract_class), casm_hash).send().await?; // deploy the contract - let contract_factory = ContractFactory::new(declaration_result.class_hash, account.clone()); - contract_factory - .deploy_v1(ctor_args.to_vec(), Felt::ZERO, false) - .max_fee(Felt::from(1e18 as u128)) - .send() - .await?; + let contract_factory = ContractFactory::new(declaration_result.class_hash, account); + contract_factory.deploy_v3(ctor_args.to_vec(), salt, false).send().await?; // generate the address of the newly deployed contract let contract_address = get_udc_deployed_address( - Felt::ZERO, + salt, declaration_result.class_hash, &starknet_rs_core::utils::UdcUniqueness::NotUnique, ctor_args, @@ -333,6 +325,16 @@ pub fn felt_to_u256(f: Felt) -> U256 { U256::from_big_endian(&f.to_bytes_be()) } +pub fn get_gas_units_and_gas_price(fee_estimate: FeeEstimate) -> (u64, u128) { + let gas_price = + u128::from_le_bytes(fee_estimate.gas_price.to_bytes_le()[0..16].try_into().unwrap()); + let gas_units = fee_estimate + .overall_fee + .field_div(&NonZeroFelt::from_felt_unchecked(fee_estimate.gas_price)); + + (gas_units.to_le_digits().first().cloned().unwrap(), gas_price) +} + #[cfg(test)] mod test_unique_auto_deletable_file { use std::path::Path; diff --git a/crates/starknet-devnet/tests/test_account_impersonation.rs b/crates/starknet-devnet/tests/test_account_impersonation.rs index 755f3f167..a9330232e 100644 --- a/crates/starknet-devnet/tests/test_account_impersonation.rs +++ b/crates/starknet-devnet/tests/test_account_impersonation.rs @@ -211,7 +211,7 @@ mod impersonated_account_tests { } let simulation_result = - account.execute_v1(invoke_calls.clone()).simulate(!do_validate, false).await; + account.execute_v1(invoke_calls.clone()).simulate(!do_validate, true).await; if let Some(error_msg) = expected_error_message { let simulation_err = simulation_result.expect_err("Expected simulation to fail"); assert_contains(&format!("{:?}", simulation_err).to_lowercase(), error_msg); diff --git a/crates/starknet-devnet/tests/test_estimate_fee.rs b/crates/starknet-devnet/tests/test_estimate_fee.rs index c18cc5f70..2427f5e98 100644 --- a/crates/starknet-devnet/tests/test_estimate_fee.rs +++ b/crates/starknet-devnet/tests/test_estimate_fee.rs @@ -14,9 +14,11 @@ mod estimate_fee_tests { use starknet_rs_contract::ContractFactory; use starknet_rs_core::types::contract::legacy::LegacyContractClass; use starknet_rs_core::types::{ - BlockId, BlockTag, BroadcastedDeclareTransactionV1, BroadcastedInvokeTransaction, - BroadcastedInvokeTransactionV1, BroadcastedTransaction, Call, ContractErrorData, - FeeEstimate, Felt, FunctionCall, StarknetError, + BlockId, BlockTag, BroadcastedDeclareTransactionV1, BroadcastedDeclareTransactionV3, + BroadcastedInvokeTransaction, BroadcastedInvokeTransactionV1, + BroadcastedInvokeTransactionV3, BroadcastedTransaction, Call, DataAvailabilityMode, + FeeEstimate, Felt, FunctionCall, ResourceBounds, ResourceBoundsMapping, + SimulationFlagForEstimateFee, StarknetError, TransactionExecutionErrorData, }; use starknet_rs_core::utils::{ cairo_short_string_to_felt, get_selector_from_name, get_udc_deployed_address, UdcUniqueness, @@ -138,7 +140,7 @@ mod estimate_fee_tests { .expect_err("Should have failed"); match err { AccountFactoryError::Provider(ProviderError::StarknetError( - StarknetError::ContractError(_), + StarknetError::TransactionExecutionError(_), )) => (), _ => panic!("Invalid error: {err:?}"), } @@ -364,7 +366,7 @@ mod estimate_fee_tests { } #[tokio::test] - async fn message_available_if_estimation_panics() { + async fn message_available_if_estimation_reverts() { let devnet = BackgroundDevnet::spawn().await.expect("Could not start Devnet"); // get account @@ -422,9 +424,12 @@ mod estimate_fee_tests { .unwrap_err(); match invoke_err { - AccountError::Provider(ProviderError::StarknetError(StarknetError::ContractError( - ContractErrorData { revert_error }, - ))) => assert_contains(&revert_error, panic_reason), + AccountError::Provider(ProviderError::StarknetError( + StarknetError::TransactionExecutionError(TransactionExecutionErrorData { + execution_error, + .. + }), + )) => assert_contains(&execution_error, panic_reason), other => panic!("Invalid err: {other:?}"), }; } @@ -578,4 +583,202 @@ mod estimate_fee_tests { .iter() .for_each(assert_fee_estimation); } + + #[tokio::test] + async fn estimate_fee_of_declare_and_deploy_via_udc_returns_index_of_second_transaction_when_executed_with_non_existing_method() + { + let devnet = BackgroundDevnet::spawn().await.expect("Could not start devnet"); + + // get account + let (signer, account_address) = devnet.get_first_predeployed_account().await; + let mut account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + signer.clone(), + account_address, + devnet.json_rpc_client.chain_id().await.unwrap(), + ExecutionEncoding::New, + ); + + account.set_block_id(BlockId::Tag(BlockTag::Latest)); + + let (flattened_contract_artifact, casm_hash) = + get_flattened_sierra_contract_and_casm_hash(CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH); + let class_hash = flattened_contract_artifact.class_hash(); + + let estimate_fee_resource_bounds = ResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + l2_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + }; + + // call non existent method in UDC + let calls = vec![Call { + to: UDC_CONTRACT_ADDRESS, + selector: get_selector_from_name("no_such_method").unwrap(), + calldata: vec![ + class_hash, + Felt::from_hex_unchecked("0x123"), // salt + Felt::ZERO, + Felt::ZERO, + ], + }]; + + let calldata = account.encode_calls(&calls); + + let is_query = true; + let nonce_data_availability_mode = DataAvailabilityMode::L1; + let fee_data_availability_mode = DataAvailabilityMode::L1; + + let expected_error = devnet + .json_rpc_client + .estimate_fee( + [ + BroadcastedTransaction::Declare( + starknet_rs_core::types::BroadcastedDeclareTransaction::V3( + BroadcastedDeclareTransactionV3 { + sender_address: account_address, + compiled_class_hash: casm_hash, + signature: vec![], + nonce: Felt::ZERO, + contract_class: Arc::new(flattened_contract_artifact.clone()), + resource_bounds: estimate_fee_resource_bounds.clone(), + tip: 0, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }, + ), + ), + BroadcastedTransaction::Invoke(BroadcastedInvokeTransaction::V3( + BroadcastedInvokeTransactionV3 { + sender_address: account_address, + calldata, + signature: vec![], + nonce: Felt::ONE, + resource_bounds: estimate_fee_resource_bounds, + tip: 0, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }, + )), + ], + [SimulationFlagForEstimateFee::SkipValidate], + account.block_id(), + ) + .await + .unwrap_err(); + + match expected_error { + ProviderError::StarknetError(StarknetError::TransactionExecutionError( + TransactionExecutionErrorData { transaction_index, execution_error }, + )) => { + assert_eq!(transaction_index, 1); + assert_contains(&execution_error, "not found in contract"); + } + other => panic!("Unexpected error: {:?}", other), + } + } + + #[tokio::test] + async fn estimate_fee_of_multiple_failing_txs_should_return_index_of_the_first_failing_transaction() + { + let devnet = BackgroundDevnet::spawn().await.expect("Could not start devnet"); + + // get account + let (signer, account_address) = devnet.get_first_predeployed_account().await; + let mut account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + signer.clone(), + account_address, + devnet.json_rpc_client.chain_id().await.unwrap(), + ExecutionEncoding::New, + ); + + account.set_block_id(BlockId::Tag(BlockTag::Latest)); + + let (flattened_contract_artifact, casm_hash) = + get_flattened_sierra_contract_and_casm_hash(CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH); + let class_hash = flattened_contract_artifact.class_hash(); + + let estimate_fee_resource_bounds = ResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + l2_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + }; + + // call non existent method in UDC + let calls = vec![Call { + to: UDC_CONTRACT_ADDRESS, + selector: get_selector_from_name("no_such_method").unwrap(), + calldata: vec![ + class_hash, + Felt::from_hex_unchecked("0x123"), // salt + Felt::ZERO, + Felt::ZERO, + ], + }]; + + let calldata = account.encode_calls(&calls); + + let is_query = true; + let nonce_data_availability_mode = DataAvailabilityMode::L1; + let fee_data_availability_mode = DataAvailabilityMode::L1; + + let expected_error = devnet + .json_rpc_client + .estimate_fee( + [ + BroadcastedTransaction::Declare( + starknet_rs_core::types::BroadcastedDeclareTransaction::V3( + BroadcastedDeclareTransactionV3 { + sender_address: account_address, + compiled_class_hash: casm_hash, + signature: vec![], + nonce: Felt::ZERO, + contract_class: Arc::new(flattened_contract_artifact.clone()), + resource_bounds: estimate_fee_resource_bounds.clone(), + tip: 0, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }, + ), + ), + BroadcastedTransaction::Invoke(BroadcastedInvokeTransaction::V3( + BroadcastedInvokeTransactionV3 { + sender_address: account_address, + calldata, + signature: vec![], + nonce: Felt::ONE, + resource_bounds: estimate_fee_resource_bounds.clone(), + tip: 0, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }, + )), + ], + [], + account.block_id(), + ) + .await + .unwrap_err(); + + match expected_error { + ProviderError::StarknetError(StarknetError::TransactionExecutionError( + TransactionExecutionErrorData { transaction_index, execution_error }, + )) => { + assert_eq!(transaction_index, 0); + assert_contains(&execution_error, "invalid signature"); + } + other => panic!("Unexpected error: {:?}", other), + } + } } diff --git a/crates/starknet-devnet/tests/test_fork.rs b/crates/starknet-devnet/tests/test_fork.rs index 04d9aa4b8..db0e40eb0 100644 --- a/crates/starknet-devnet/tests/test_fork.rs +++ b/crates/starknet-devnet/tests/test_fork.rs @@ -31,7 +31,7 @@ mod fork_tests { MAINNET_HTTPS_URL, MAINNET_URL, }; use crate::common::utils::{ - assert_cairo1_classes_equal, assert_tx_successful, declare_deploy_v1, + assert_cairo1_classes_equal, assert_tx_successful, declare_v3_deploy_v3, get_block_reader_contract_in_sierra_and_compiled_class_hash, get_contract_balance, get_simple_contract_in_sierra_and_compiled_class_hash, send_ctrl_c_signal_and_wait, }; @@ -199,22 +199,26 @@ mod fork_tests { .unwrap(); let (signer, account_address) = origin_devnet.get_first_predeployed_account().await; - let predeployed_account = Arc::new(SingleOwnerAccount::new( - origin_devnet.clone_provider(), + let predeployed_account = SingleOwnerAccount::new( + &origin_devnet.json_rpc_client, signer.clone(), account_address, constants::CHAIN_ID, ExecutionEncoding::New, - )); + ); let (contract_class, casm_hash) = get_simple_contract_in_sierra_and_compiled_class_hash(); let initial_value = Felt::from(10_u32); let ctor_args = vec![initial_value]; - let (class_hash, contract_address) = - declare_deploy_v1(predeployed_account, contract_class.clone(), casm_hash, &ctor_args) - .await - .unwrap(); + let (class_hash, contract_address) = declare_v3_deploy_v3( + &predeployed_account, + contract_class.clone(), + casm_hash, + &ctor_args, + ) + .await + .unwrap(); let fork_devnet = origin_devnet.fork().await.unwrap(); @@ -535,19 +539,21 @@ mod fork_tests { let origin_devnet = BackgroundDevnet::spawn_forkable_devnet().await.unwrap(); let (signer, account_address) = origin_devnet.get_first_predeployed_account().await; - let predeployed_account = Arc::new(SingleOwnerAccount::new( - origin_devnet.clone_provider(), + let predeployed_account = SingleOwnerAccount::new( + &origin_devnet.json_rpc_client, signer.clone(), account_address, constants::CHAIN_ID, ExecutionEncoding::New, - )); + ); let (contract_class, casm_hash) = get_block_reader_contract_in_sierra_and_compiled_class_hash(); let (_, contract_address) = - declare_deploy_v1(predeployed_account, contract_class, casm_hash, &[]).await.unwrap(); + declare_v3_deploy_v3(&predeployed_account, contract_class, casm_hash, &[]) + .await + .unwrap(); let fork_devnet = origin_devnet.fork().await.unwrap(); diff --git a/crates/starknet-devnet/tests/test_gas_modification.rs b/crates/starknet-devnet/tests/test_gas_modification.rs index 34ef4546a..ff6263f99 100644 --- a/crates/starknet-devnet/tests/test_gas_modification.rs +++ b/crates/starknet-devnet/tests/test_gas_modification.rs @@ -75,9 +75,9 @@ mod gas_modification_tests { let chain_id = &devnet.send_custom_rpc("starknet_chainId", json!({})).await.unwrap(); assert_eq!(chain_id, expected_chain_id); - let params_no_flags = get_params(&[]); + let params_skip_fee_charge = get_params(&["SKIP_FEE_CHARGE"]); let resp_no_flags = &devnet - .send_custom_rpc("starknet_simulateTransactions", params_no_flags.clone()) + .send_custom_rpc("starknet_simulateTransactions", params_skip_fee_charge.clone()) .await .unwrap()[0]; assert_eq!( @@ -90,9 +90,13 @@ mod gas_modification_tests { ); assert_eq!(resp_no_flags["fee_estimation"]["overall_fee"], "0x7398c659d800"); - let params_skip_validation = get_params(&["SKIP_VALIDATE"]); + let params_skip_validation_and_fee_charge = + get_params(&["SKIP_VALIDATE", "SKIP_FEE_CHARGE"]); let resp_skip_validation = &devnet - .send_custom_rpc("starknet_simulateTransactions", params_skip_validation.clone()) + .send_custom_rpc( + "starknet_simulateTransactions", + params_skip_validation_and_fee_charge.clone(), + ) .await .unwrap()[0]; assert_eq!( @@ -135,7 +139,7 @@ mod gas_modification_tests { assert_eq!(chain_id, expected_chain_id); let resp_no_flags = &devnet - .send_custom_rpc("starknet_simulateTransactions", params_no_flags) + .send_custom_rpc("starknet_simulateTransactions", params_skip_fee_charge) .await .unwrap()[0]; @@ -144,7 +148,7 @@ mod gas_modification_tests { assert_eq!(resp_no_flags["fee_estimation"]["overall_fee"], "0x261b37abed7125c0000"); let resp_skip_validation = &devnet - .send_custom_rpc("starknet_simulateTransactions", params_skip_validation) + .send_custom_rpc("starknet_simulateTransactions", params_skip_validation_and_fee_charge) .await .unwrap()[0]; assert_eq!(resp_skip_validation["fee_estimation"]["gas_price"], to_hex_felt(&wei_price)); diff --git a/crates/starknet-devnet/tests/test_old_state.rs b/crates/starknet-devnet/tests/test_old_state.rs index d60d5f6b2..9d02c89a1 100644 --- a/crates/starknet-devnet/tests/test_old_state.rs +++ b/crates/starknet-devnet/tests/test_old_state.rs @@ -10,7 +10,9 @@ mod old_state { use starknet_rs_core::types::{ BlockHashAndNumber, BlockId, BlockTag, BroadcastedInvokeTransaction, BroadcastedInvokeTransactionV1, BroadcastedTransaction, Call, ContractClass, - ContractErrorData, Felt, SimulationFlag, SimulationFlagForEstimateFee, StarknetError, + ExecuteInvocation, Felt, InvokeTransactionTrace, SimulatedTransaction, SimulationFlag, + SimulationFlagForEstimateFee, StarknetError, TransactionExecutionErrorData, + TransactionTrace, }; use starknet_rs_core::utils::{get_selector_from_name, get_storage_var_address}; use starknet_rs_providers::{Provider, ProviderError}; @@ -109,8 +111,10 @@ mod old_state { } } + // estimate fee of invoke transaction that reverts must fail, but simulating the same invoke + // transaction have to produce trace of a reverted transaction #[tokio::test] - async fn estimate_fee_and_simulate_transaction_for_contract_deployment_in_an_old_block_should_produce_the_same_error() + async fn estimate_fee_and_simulate_transaction_for_contract_deployment_in_an_old_block_should_not_produce_the_same_error() { let devnet = BackgroundDevnet::spawn_with_additional_args(&["--state-archive-capacity", "full"]) @@ -173,12 +177,12 @@ mod old_state { }, ))], [SimulationFlagForEstimateFee::SkipValidate], - BlockId::Hash(block_hash), + block_id, ) .await .unwrap_err(); - let simulation_error = devnet + let SimulatedTransaction { transaction_trace, .. } = devnet .json_rpc_client .simulate_transaction( block_id, @@ -195,17 +199,27 @@ mod old_state { [SimulationFlag::SkipValidate], ) .await - .unwrap_err(); + .unwrap(); - let estimate_fee_error_string = format!("{:?}", estimate_fee_error); - match estimate_fee_error { - ProviderError::StarknetError(StarknetError::ContractError(ContractErrorData { - revert_error, - })) => assert_contains(&revert_error, "not declared"), + let estimate_fee_error_string = match estimate_fee_error { + ProviderError::StarknetError(StarknetError::TransactionExecutionError( + TransactionExecutionErrorData { execution_error, .. }, + )) => { + assert_contains(&execution_error, "not declared"); + execution_error + } other => panic!("Unexpected error: {other:?}"), + }; + + match transaction_trace { + TransactionTrace::Invoke(InvokeTransactionTrace { + execute_invocation: ExecuteInvocation::Reverted(reverted_invocation), + .. + }) => { + assert_eq!(estimate_fee_error_string, reverted_invocation.revert_reason); + } + other => panic!("Unexpected trace {other:?}"), } - - assert_eq!(estimate_fee_error_string, format!("{:?}", simulation_error)); } #[tokio::test] diff --git a/crates/starknet-devnet/tests/test_simulate_transactions.rs b/crates/starknet-devnet/tests/test_simulate_transactions.rs index 22ff6316d..4002b6640 100644 --- a/crates/starknet-devnet/tests/test_simulate_transactions.rs +++ b/crates/starknet-devnet/tests/test_simulate_transactions.rs @@ -3,33 +3,48 @@ pub mod common; mod simulation_tests { use std::sync::Arc; + use std::{u128, u64}; use serde_json::json; - use starknet_core::constants::{CAIRO_0_ACCOUNT_CONTRACT_HASH, ETH_ERC20_CONTRACT_ADDRESS}; + use server::test_utils::assert_contains; + use starknet_core::constants::{ + CAIRO_0_ACCOUNT_CONTRACT_HASH, CAIRO_1_ACCOUNT_CONTRACT_SIERRA_HASH, + ETH_ERC20_CONTRACT_ADDRESS, UDC_CONTRACT_ADDRESS, + }; use starknet_core::utils::exported_test_utils::dummy_cairo_0_contract_class; use starknet_rs_accounts::{ - Account, AccountFactory, ExecutionEncoder, ExecutionEncoding, OpenZeppelinAccountFactory, - SingleOwnerAccount, + Account, AccountError, AccountFactory, ConnectedAccount, ExecutionEncoder, + ExecutionEncoding, OpenZeppelinAccountFactory, SingleOwnerAccount, }; use starknet_rs_contract::ContractFactory; use starknet_rs_core::types::contract::legacy::LegacyContractClass; - use starknet_rs_core::types::{BlockId, BlockTag, Call, Felt, FunctionCall}; + use starknet_rs_core::types::{ + BlockId, BlockTag, BroadcastedDeclareTransaction, BroadcastedDeclareTransactionV3, + BroadcastedDeployAccountTransaction, BroadcastedDeployAccountTransactionV3, + BroadcastedInvokeTransaction, BroadcastedInvokeTransactionV3, BroadcastedTransaction, Call, + DataAvailabilityMode, ExecuteInvocation, Felt, FunctionCall, InvokeTransactionTrace, + ResourceBounds, ResourceBoundsMapping, SimulatedTransaction, SimulationFlag, StarknetError, + TransactionExecutionErrorData, TransactionTrace, + }; use starknet_rs_core::utils::{ - get_selector_from_name, get_udc_deployed_address, UdcUniqueness, + cairo_short_string_to_felt, get_selector_from_name, get_udc_deployed_address, UdcUniqueness, }; - use starknet_rs_providers::Provider; - use starknet_rs_signers::Signer; + use starknet_rs_providers::{Provider, ProviderError}; + use starknet_rs_signers::{LocalWallet, Signer, SigningKey}; use starknet_types::constants::QUERY_VERSION_OFFSET; use starknet_types::felt::felt_from_prefixed_hex; use crate::common::background_devnet::BackgroundDevnet; use crate::common::constants::{ - CAIRO_1_CONTRACT_PATH, CAIRO_1_VERSION_ASSERTER_SIERRA_PATH, CHAIN_ID, + self, CAIRO_1_CONTRACT_PATH, CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH, + CAIRO_1_VERSION_ASSERTER_SIERRA_PATH, CHAIN_ID, }; use crate::common::fees::{assert_difference_if_validation, assert_fee_in_resp_at_least_equal}; use crate::common::utils::{ - get_deployable_account_signer, get_flattened_sierra_contract_and_casm_hash, - iter_to_hex_felt, to_hex_felt, to_num_as_hex, + declare_v3_deploy_v3, get_deployable_account_signer, + get_flattened_sierra_contract_and_casm_hash, get_gas_units_and_gas_price, + get_simple_contract_in_sierra_and_compiled_class_hash, iter_to_hex_felt, to_hex_felt, + to_num_as_hex, }; #[tokio::test] @@ -81,15 +96,16 @@ mod simulation_tests { }) }; - let params_no_flags = get_params(&[]); let resp_no_flags = &devnet - .send_custom_rpc("starknet_simulateTransactions", params_no_flags) + .send_custom_rpc("starknet_simulateTransactions", get_params(&["SKIP_FEE_CHARGE"])) .await .unwrap()[0]; - let params_skip_validation = get_params(&["SKIP_VALIDATE"]); let resp_skip_validation = &devnet - .send_custom_rpc("starknet_simulateTransactions", params_skip_validation) + .send_custom_rpc( + "starknet_simulateTransactions", + get_params(&["SKIP_VALIDATE", "SKIP_FEE_CHARGE"]), + ) .await .unwrap()[0]; @@ -97,7 +113,7 @@ mod simulation_tests { resp_no_flags, resp_skip_validation, &sender_address_hex, - max_fee == Felt::ZERO, + true, ); } @@ -153,15 +169,16 @@ mod simulation_tests { }) }; - let params_no_flags = get_params(&[]); let resp_no_flags = &devnet - .send_custom_rpc("starknet_simulateTransactions", params_no_flags) + .send_custom_rpc("starknet_simulateTransactions", get_params(&["SKIP_FEE_CHARGE"])) .await .unwrap()[0]; - let params_skip_validation = get_params(&["SKIP_VALIDATE"]); let resp_skip_validation = &devnet - .send_custom_rpc("starknet_simulateTransactions", params_skip_validation) + .send_custom_rpc( + "starknet_simulateTransactions", + get_params(&["SKIP_VALIDATE", "SKIP_FEE_CHARGE"]), + ) .await .unwrap()[0]; @@ -169,7 +186,7 @@ mod simulation_tests { resp_no_flags, resp_skip_validation, &sender_address_hex, - max_fee == Felt::ZERO, + true, ); } @@ -409,42 +426,24 @@ mod simulation_tests { // get account let (signer, account_address) = devnet.get_first_predeployed_account().await; - let account = Arc::new(SingleOwnerAccount::new( - devnet.clone_provider(), + let account = SingleOwnerAccount::new( + &devnet.json_rpc_client, signer.clone(), account_address, CHAIN_ID, ExecutionEncoding::New, - )); + ); // get class let (flattened_contract_artifact, casm_hash) = get_flattened_sierra_contract_and_casm_hash(CAIRO_1_VERSION_ASSERTER_SIERRA_PATH); let class_hash = flattened_contract_artifact.class_hash(); - // declare class - let declaration_result = account - .declare_v2(Arc::new(flattened_contract_artifact), casm_hash) - .send() - .await - .unwrap(); - assert_eq!(declaration_result.class_hash, class_hash); - - // deploy instance of class - let contract_factory = ContractFactory::new(class_hash, account.clone()); - let salt = Felt::from_hex_unchecked("0x123"); - let constructor_calldata = vec![]; - let contract_address = get_udc_deployed_address( - salt, - class_hash, - &UdcUniqueness::NotUnique, - &constructor_calldata, - ); - contract_factory - .deploy_v1(constructor_calldata, salt, false) - .send() - .await - .expect("Cannot deploy"); + let (generated_class_hash, contract_address) = + declare_v3_deploy_v3(&account, flattened_contract_artifact, casm_hash, &[]) + .await + .unwrap(); + assert_eq!(generated_class_hash, class_hash); let calls = vec![Call { to: contract_address, @@ -479,4 +478,686 @@ mod simulation_tests { .await .unwrap(); } + + #[tokio::test] + async fn simulate_of_multiple_txs_shouldnt_return_an_error_if_invoke_transaction_reverts() { + let devnet = BackgroundDevnet::spawn().await.expect("Could not start devnet"); + + // get account + let (signer, account_address) = devnet.get_first_predeployed_account().await; + let mut account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + signer.clone(), + account_address, + devnet.json_rpc_client.chain_id().await.unwrap(), + ExecutionEncoding::New, + ); + + account.set_block_id(BlockId::Tag(BlockTag::Latest)); + + let (flattened_contract_artifact, casm_hash) = + get_flattened_sierra_contract_and_casm_hash(CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH); + let class_hash = flattened_contract_artifact.class_hash(); + + let estimate_fee_resource_bounds = ResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + l2_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + }; + + // call non existent method in UDC + let calls = vec![Call { + to: UDC_CONTRACT_ADDRESS, + selector: get_selector_from_name("no_such_method").unwrap(), + calldata: vec![ + class_hash, + Felt::from_hex_unchecked("0x123"), // salt + Felt::ZERO, + Felt::ZERO, + ], + }]; + + let calldata = account.encode_calls(&calls); + + let is_query = true; + let nonce_data_availability_mode = DataAvailabilityMode::L1; + let fee_data_availability_mode = DataAvailabilityMode::L1; + + let simulation_result = devnet + .json_rpc_client + .simulate_transactions( + account.block_id(), + [ + BroadcastedTransaction::Declare( + starknet_rs_core::types::BroadcastedDeclareTransaction::V3( + BroadcastedDeclareTransactionV3 { + sender_address: account_address, + compiled_class_hash: casm_hash, + signature: vec![], + nonce: Felt::ZERO, + contract_class: Arc::new(flattened_contract_artifact.clone()), + resource_bounds: estimate_fee_resource_bounds.clone(), + tip: 0, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }, + ), + ), + BroadcastedTransaction::Invoke(BroadcastedInvokeTransaction::V3( + BroadcastedInvokeTransactionV3 { + sender_address: account_address, + calldata, + signature: vec![], + nonce: Felt::ONE, + resource_bounds: estimate_fee_resource_bounds, + tip: 0, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }, + )), + ], + [SimulationFlag::SkipValidate, SimulationFlag::SkipFeeCharge], + ) + .await + .unwrap(); + + match &simulation_result[1].transaction_trace { + TransactionTrace::Invoke(InvokeTransactionTrace { + execute_invocation: ExecuteInvocation::Reverted(reverted_invocation), + .. + }) => { + assert_contains(&reverted_invocation.revert_reason, "not found in contract"); + } + other_trace => panic!("Unexpected trace {:?}", other_trace), + } + } + + #[tokio::test] + async fn simulate_of_multiple_txs_should_return_index_of_first_failing_transaction() { + let devnet = BackgroundDevnet::spawn().await.expect("Could not start devnet"); + + // get account + let (signer, account_address) = devnet.get_first_predeployed_account().await; + let mut account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + signer.clone(), + account_address, + devnet.json_rpc_client.chain_id().await.unwrap(), + ExecutionEncoding::New, + ); + + account.set_block_id(BlockId::Tag(BlockTag::Latest)); + + let (flattened_contract_artifact, casm_hash) = + get_flattened_sierra_contract_and_casm_hash(CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH); + let class_hash = flattened_contract_artifact.class_hash(); + + let estimate_fee_resource_bounds = ResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + l2_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + }; + + // call non existent method in UDC + let calls = vec![Call { + to: UDC_CONTRACT_ADDRESS, + selector: get_selector_from_name("no_such_method").unwrap(), + calldata: vec![ + class_hash, + Felt::from_hex_unchecked("0x123"), // salt + Felt::ZERO, + Felt::ZERO, + ], + }]; + + let calldata = account.encode_calls(&calls); + + let is_query = true; + let nonce_data_availability_mode = DataAvailabilityMode::L1; + let fee_data_availability_mode = DataAvailabilityMode::L1; + + let simulation_err = devnet + .json_rpc_client + .simulate_transactions( + account.block_id(), + [ + BroadcastedTransaction::Declare( + starknet_rs_core::types::BroadcastedDeclareTransaction::V3( + BroadcastedDeclareTransactionV3 { + sender_address: account_address, + compiled_class_hash: casm_hash, + signature: vec![], + nonce: Felt::ZERO, + contract_class: Arc::new(flattened_contract_artifact.clone()), + resource_bounds: estimate_fee_resource_bounds.clone(), + tip: 0, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }, + ), + ), + BroadcastedTransaction::Invoke(BroadcastedInvokeTransaction::V3( + BroadcastedInvokeTransactionV3 { + sender_address: account_address, + calldata, + signature: vec![], + nonce: Felt::ONE, + resource_bounds: estimate_fee_resource_bounds, + tip: 0, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }, + )), + ], + [], + ) + .await + .unwrap_err(); + + match simulation_err { + ProviderError::StarknetError(StarknetError::TransactionExecutionError( + TransactionExecutionErrorData { transaction_index, .. }, + )) => { + assert_eq!(transaction_index, 0); + } + other_error => panic!("Unexpected error {:?}", other_error), + } + } + + #[tokio::test] + async fn simulate_with_max_fee_exceeding_account_balance_returns_error_if_fee_charge_is_not_skipped() + { + let devnet = BackgroundDevnet::spawn().await.expect("Could not start Devnet"); + let (sierra_artifact, casm_hash) = + get_flattened_sierra_contract_and_casm_hash(CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH); + + let (signer, account_address) = devnet.get_first_predeployed_account().await; + + let mut account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + signer, + account_address, + constants::CHAIN_ID, + ExecutionEncoding::New, + ); + account.set_block_id(BlockId::Tag(BlockTag::Latest)); + + let declaration = account + .declare_v3(Arc::new(sierra_artifact), casm_hash) + .gas(u64::MAX) + .gas_price(u128::MAX); + + match declaration.simulate(false, false).await.unwrap_err() { + AccountError::Provider(ProviderError::StarknetError( + StarknetError::TransactionExecutionError(TransactionExecutionErrorData { + execution_error, + .. + }), + )) => { + assert_contains( + &execution_error, + "Account balance is not enough to cover the transaction cost.", + ); + } + other => panic!("Unexpected error {other:?}"), + } + + // should not fail because fee transfer is skipped + declaration.simulate(false, true).await.unwrap(); + } + + #[tokio::test] + async fn simulate_v3_with_skip_fee_charge_deploy_account_declare_deploy_via_invoke_to_udc_happy_path() + { + let devnet = BackgroundDevnet::spawn_with_additional_args(&["--account-class", "cairo1"]) + .await + .expect("Could not start Devnet"); + + let new_account_private_key = Felt::from(7777); + let signer = + LocalWallet::from_signing_key(SigningKey::from_secret_scalar(new_account_private_key)); + + let latest = BlockId::Tag(BlockTag::Latest); + + let public_key = signer.get_public_key().await.unwrap(); + let salt = Felt::from_hex_unchecked("0x123"); + let account_class_hash = Felt::from_hex_unchecked(CAIRO_1_ACCOUNT_CONTRACT_SIERRA_HASH); + + let resource_bounds = ResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + l2_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + }; + + let nonce_data_availability_mode = DataAvailabilityMode::L1; + let fee_data_availability_mode = DataAvailabilityMode::L1; + let is_query = true; + let chain_id = devnet.json_rpc_client.chain_id().await.unwrap(); + let paymaster_data = vec![]; + let tip = 0; + let gas = 0; + let gas_price = 0; + + let account_factory = OpenZeppelinAccountFactory::new( + account_class_hash, + chain_id, + &signer, + &devnet.json_rpc_client, + ) + .await + .unwrap(); + + let nonce = Felt::ZERO; + let account_deployment = + account_factory.deploy_v3(salt).nonce(nonce).gas(gas).gas_price(gas_price); + + let account_address = account_deployment.address(); + let txn_hash = account_deployment.prepared().unwrap().transaction_hash(is_query); + let signature = signer.sign_hash(&txn_hash).await.unwrap(); + + let deploy_account_transaction = BroadcastedDeployAccountTransactionV3 { + nonce, + signature: vec![signature.r, signature.s], + contract_address_salt: salt, + constructor_calldata: vec![public_key.scalar()], + class_hash: account_class_hash, + resource_bounds: resource_bounds.clone(), + tip, + paymaster_data: paymaster_data.clone(), + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }; + + let account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + &signer, + account_address, + chain_id, + ExecutionEncoding::New, + ); + + let (sierra_artifact, casm_hash) = + get_flattened_sierra_contract_and_casm_hash(CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH); + + let contract_class_hash = sierra_artifact.class_hash(); + let nonce = Felt::ONE; + let declare_txn_hash = account + .declare_v3(Arc::new(sierra_artifact.clone()), casm_hash) + .nonce(nonce) + .gas(gas) + .gas_price(gas_price) + .prepared() + .unwrap() + .transaction_hash(is_query); + + let declare_signature = signer.sign_hash(&declare_txn_hash).await.unwrap(); + + let declare_transaction = BroadcastedDeclareTransactionV3 { + sender_address: account_address, + compiled_class_hash: casm_hash, + signature: vec![declare_signature.r, declare_signature.s], + nonce, + contract_class: Arc::new(sierra_artifact), + resource_bounds: resource_bounds.clone(), + tip, + paymaster_data: paymaster_data.clone(), + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }; + + // call non existent method in UDC + let calls = vec![Call { + to: UDC_CONTRACT_ADDRESS, + selector: get_selector_from_name("deployContract").unwrap(), + calldata: vec![ + contract_class_hash, + Felt::from_hex_unchecked("0x123"), // salt + Felt::ZERO, + Felt::ZERO, + ], + }]; + + let calldata = account.encode_calls(&calls); + let nonce = Felt::TWO; + let invoke_transaction_hash = account + .execute_v3(calls) + .gas(gas) + .gas_price(gas_price) + .nonce(nonce) + .prepared() + .unwrap() + .transaction_hash(is_query); + + let invoke_signature = signer.sign_hash(&invoke_transaction_hash).await.unwrap(); + + let invoke_transaction = BroadcastedInvokeTransactionV3 { + sender_address: account_address, + calldata, + signature: vec![invoke_signature.r, invoke_signature.s], + nonce, + resource_bounds: resource_bounds.clone(), + tip, + paymaster_data: paymaster_data.clone(), + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }; + + devnet + .json_rpc_client + .simulate_transactions( + latest, + [ + BroadcastedTransaction::DeployAccount(BroadcastedDeployAccountTransaction::V3( + deploy_account_transaction, + )), + BroadcastedTransaction::Declare(BroadcastedDeclareTransaction::V3( + declare_transaction, + )), + BroadcastedTransaction::Invoke(BroadcastedInvokeTransaction::V3( + invoke_transaction, + )), + ], + [SimulationFlag::SkipFeeCharge], + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn simulate_invoke_v3_with_fee_just_below_estimated_should_return_a_trace_of_reverted_transaction() + { + let devnet = BackgroundDevnet::spawn().await.expect("Could not start Devnet"); + let (sierra_artifact, casm_hash) = + get_flattened_sierra_contract_and_casm_hash(CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH); + + let (signer, account_address) = devnet.get_first_predeployed_account().await; + + let mut account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + signer, + account_address, + constants::CHAIN_ID, + ExecutionEncoding::New, + ); + account.set_block_id(BlockId::Tag(BlockTag::Latest)); + + let declare_result = + account.declare_v3(Arc::new(sierra_artifact.clone()), casm_hash).send().await.unwrap(); + + let salt = Felt::from_hex_unchecked("0x123"); + let execution = account.execute_v3(vec![Call { + to: UDC_CONTRACT_ADDRESS, + selector: get_selector_from_name("deployContract").unwrap(), + calldata: vec![ + declare_result.class_hash, + salt, + Felt::ZERO, // is_unique + Felt::ZERO, // constructor data length + ], + }]); + let fee_estimate = execution.estimate_fee().await.unwrap(); + + let (gas_units, gas_price) = get_gas_units_and_gas_price(fee_estimate); + + let SimulatedTransaction { transaction_trace, .. } = + execution.gas(gas_units - 1).gas_price(gas_price).simulate(false, false).await.unwrap(); + + match transaction_trace { + TransactionTrace::Invoke(InvokeTransactionTrace { + execute_invocation: ExecuteInvocation::Reverted(reverted_invocation), + .. + }) => assert_contains(&reverted_invocation.revert_reason, "Insufficient"), + other => panic!("Unexpected trace {other:?}"), + } + } + + #[tokio::test] + async fn simulate_invoke_declare_deploy_account_with_either_gas_or_gas_price_set_to_zero_or_both_will_revert_if_skip_fee_charge_is_not_set() + { + let devnet = BackgroundDevnet::spawn_with_additional_args(&["--account-class", "cairo1"]) + .await + .expect("Could not start Devnet"); + + let (signer, account_address) = devnet.get_first_predeployed_account().await; + + let account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + signer, + account_address, + constants::CHAIN_ID, + ExecutionEncoding::New, + ); + + let call = Call { + to: ETH_ERC20_CONTRACT_ADDRESS, + selector: get_selector_from_name("transfer").unwrap(), + calldata: vec![ + Felt::ONE, // recipient + Felt::from(1_000), // low part of uint256 + Felt::ZERO, // high part of uint256 + ], + }; + + let calldata = account.encode_calls(&[call]); + + let new_account_private_key = Felt::from(7777); + let signer = + LocalWallet::from_signing_key(SigningKey::from_secret_scalar(new_account_private_key)); + let public_key = signer.get_public_key().await.unwrap().scalar(); + + let (sierra_artifact, casm_hash) = + get_flattened_sierra_contract_and_casm_hash(CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH); + + let account_class_hash = Felt::from_hex_unchecked(CAIRO_1_ACCOUNT_CONTRACT_SIERRA_HASH); + + let nonce_data_availability_mode = DataAvailabilityMode::L1; + let fee_data_availability_mode = DataAvailabilityMode::L1; + let tip = 0; + + let nonce = Felt::ZERO; + let sierra_artifact = Arc::new(sierra_artifact); + let block_id = BlockId::Tag(BlockTag::Latest); + let is_query = true; + + for (gas_units, gas_price) in [(0, 0), (0, 1e18 as u128), (1e18 as u64, 0)] { + let resource_bounds = ResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: gas_units, max_price_per_unit: gas_price }, + l2_gas: ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + }; + + let invoke_transaction = BroadcastedInvokeTransactionV3 { + sender_address: account_address, + calldata: calldata.clone(), + signature: vec![], + nonce, + resource_bounds: resource_bounds.clone(), + tip, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }; + + let invoke_transaction = BroadcastedTransaction::Invoke( + BroadcastedInvokeTransaction::V3(invoke_transaction), + ); + + let deploy_account_transaction = BroadcastedDeployAccountTransactionV3 { + signature: vec![], + nonce, + contract_address_salt: Felt::ZERO, + constructor_calldata: vec![public_key], + class_hash: account_class_hash, + resource_bounds: resource_bounds.clone(), + tip, + paymaster_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }; + + let deploy_account_transaction = BroadcastedTransaction::DeployAccount( + BroadcastedDeployAccountTransaction::V3(deploy_account_transaction), + ); + + let declare_transaction = BroadcastedDeclareTransactionV3 { + sender_address: account_address, + compiled_class_hash: casm_hash, + signature: vec![], + nonce, + contract_class: sierra_artifact.clone(), + resource_bounds, + tip, + paymaster_data: vec![], + account_deployment_data: vec![], + nonce_data_availability_mode, + fee_data_availability_mode, + is_query, + }; + + let declare_transaction = BroadcastedTransaction::Declare( + BroadcastedDeclareTransaction::V3(declare_transaction), + ); + + for transaction in [deploy_account_transaction, declare_transaction, invoke_transaction] + { + let simulation_error = devnet + .json_rpc_client + .simulate_transaction(block_id, &transaction, [SimulationFlag::SkipValidate]) + .await + .unwrap_err(); + + match simulation_error { + ProviderError::StarknetError(StarknetError::TransactionExecutionError( + TransactionExecutionErrorData { execution_error, .. }, + )) => { + assert_eq!( + execution_error, + "Provided max fee is not enough to cover the transaction cost." + ); + } + other => panic!("Unexpected error: {:?}", other), + } + + devnet + .json_rpc_client + .simulate_transaction( + block_id, + &transaction, + [SimulationFlag::SkipValidate, SimulationFlag::SkipFeeCharge], + ) + .await + .unwrap(); + } + } + } + + #[tokio::test] + async fn simulate_invoke_v3_with_failing_execution_should_return_a_trace_of_reverted_transaction() + { + let devnet = BackgroundDevnet::spawn().await.expect("Could not start Devnet"); + let (sierra_artifact, casm_hash) = + get_flattened_sierra_contract_and_casm_hash(CAIRO_1_PANICKING_CONTRACT_SIERRA_PATH); + + let (signer, account_address) = devnet.get_first_predeployed_account().await; + + let account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + signer, + account_address, + constants::CHAIN_ID, + ExecutionEncoding::New, + ); + + let (_, contract_address) = + declare_v3_deploy_v3(&account, sierra_artifact, casm_hash, &[]).await.unwrap(); + + let panic_reason = "custom little reason"; + + let SimulatedTransaction { transaction_trace, .. } = account + .execute_v3(vec![Call { + to: contract_address, + selector: get_selector_from_name("create_panic").unwrap(), + calldata: vec![cairo_short_string_to_felt(panic_reason).unwrap()], + }]) + .simulate(false, true) + .await + .unwrap(); + + match transaction_trace { + TransactionTrace::Invoke(InvokeTransactionTrace { + execute_invocation: ExecuteInvocation::Reverted(reverted_invocation), + .. + }) => assert_contains(&reverted_invocation.revert_reason, panic_reason), + other => panic!("Unexpected trace {other:?}"), + } + } + + /// Test with lower than (estimated_gas_units * gas_price) using two flags. With + /// skip_fee_transfer shouldnt fail, without it should fail. + #[tokio::test] + async fn simulate_declare_v3_with_less_than_estimated_fee_should_revert_if_fee_charge_is_not_skipped() + { + let devnet = BackgroundDevnet::spawn().await.expect("Could not start Devnet"); + let (sierra_artifact, casm_hash) = get_simple_contract_in_sierra_and_compiled_class_hash(); + + let (signer, account_address) = devnet.get_first_predeployed_account().await; + + let mut account = SingleOwnerAccount::new( + &devnet.json_rpc_client, + signer, + account_address, + constants::CHAIN_ID, + ExecutionEncoding::New, + ); + account.set_block_id(BlockId::Tag(BlockTag::Latest)); + + let fee_estimate = account + .declare_v3(Arc::new(sierra_artifact.clone()), casm_hash) + .estimate_fee() + .await + .unwrap(); + + let (gas_units, gas_price) = get_gas_units_and_gas_price(fee_estimate); + + for skip_fee_charge in [true, false] { + let simulation_result = account + .declare_v3(Arc::new(sierra_artifact.clone()), casm_hash) + .gas(gas_units) + .gas_price(gas_price - 1) + .simulate(false, skip_fee_charge) + .await; + + match (simulation_result, skip_fee_charge) { + (Ok(_), true) => {} + ( + Err(AccountError::Provider(ProviderError::StarknetError( + StarknetError::TransactionExecutionError(TransactionExecutionErrorData { + execution_error, + .. + }), + ))), + false, + ) => { + assert_contains(&execution_error, "max fee is not enough"); + } + invalid_combination => panic!("Invalid combination: {invalid_combination:?}"), + } + } + } } diff --git a/crates/starknet-devnet/tests/test_v3_transactions.rs b/crates/starknet-devnet/tests/test_v3_transactions.rs index d1b4b2dd2..9ddc64a72 100644 --- a/crates/starknet-devnet/tests/test_v3_transactions.rs +++ b/crates/starknet-devnet/tests/test_v3_transactions.rs @@ -13,7 +13,7 @@ mod test_v3_transactions { ExecutionEncoding, ExecutionV3, OpenZeppelinAccountFactory, SingleOwnerAccount, }; use starknet_rs_core::types::{ - BlockId, BlockTag, Call, ExecutionResult, FeeEstimate, Felt, FlattenedSierraClass, + BlockId, BlockTag, Call, ExecutionResult, Felt, FlattenedSierraClass, InvokeTransactionResult, NonZeroFelt, StarknetError, }; use starknet_rs_core::utils::{get_selector_from_name, get_udc_deployed_address}; @@ -27,7 +27,7 @@ mod test_v3_transactions { use crate::common::background_devnet::BackgroundDevnet; use crate::common::constants; use crate::common::utils::{ - assert_tx_successful, get_deployable_account_signer, + assert_tx_successful, get_deployable_account_signer, get_gas_units_and_gas_price, get_simple_contract_in_sierra_and_compiled_class_hash, }; @@ -291,16 +291,6 @@ mod test_v3_transactions { assert_tx_successful(&result.transaction_hash, &devnet.json_rpc_client).await; } - fn get_gas_units_and_gas_price(fee_estimate: FeeEstimate) -> (u64, u128) { - let gas_price = - u128::from_le_bytes(fee_estimate.gas_price.to_bytes_le()[0..16].try_into().unwrap()); - let gas_units = fee_estimate - .overall_fee - .field_div(&NonZeroFelt::from_felt_unchecked(fee_estimate.gas_price)); - - (gas_units.to_le_digits().first().cloned().unwrap(), gas_price) - } - /// This function sets the gas price and/or gas units to a value that is less than the estimated /// then sends the transaction. The expected result is that the transaction will either fail or /// be accepted as reverted. @@ -354,10 +344,7 @@ mod test_v3_transactions { starknet_rs_accounts::AccountError::Provider( ProviderError::StarknetError(StarknetError::InsufficientMaxFee), ) => {} - other => assert_contains( - &other.to_string(), - "is lower than the actual gas price", - ), + other => panic!("Unexpected error {:?}", other), } } Action::AccountDeployment(salt) => { @@ -373,10 +360,7 @@ mod test_v3_transactions { starknet_rs_accounts::AccountFactoryError::Provider( ProviderError::StarknetError(StarknetError::InsufficientMaxFee), ) => {} - other => assert_contains( - &other.to_string(), - "is lower than the actual gas price", - ), + other => panic!("Unexpected error {:?}", other), } } Action::Execution(calls) => { @@ -405,12 +389,10 @@ mod test_v3_transactions { other => panic!("Unexpected result: {:?}", other), } } - Err(error) => { - assert_contains( - &error.to_string(), - "is lower than the actual gas price", - ); - } + Err(starknet_rs_accounts::AccountError::Provider( + ProviderError::StarknetError(StarknetError::InsufficientMaxFee), + )) => {} + Err(error) => panic!("Unexpected error {:?}", error), } } };