From 7f544595dd09f93390b7dd3ba47453cd9758fa58 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sat, 2 Mar 2024 14:18:14 +0800 Subject: [PATCH] test(katana-executor): add test for transaction simulation (#1593) --- Cargo.lock | 2 + crates/katana/executor/Cargo.toml | 2 + .../src/implementation/blockifier/utils.rs | 30 ++++++-- crates/katana/executor/tests/fixtures/mod.rs | 60 ++++++++++++++-- crates/katana/executor/tests/simulate.rs | 71 +++++++++++++++++++ .../katana/rpc/rpc-types/src/transaction.rs | 6 +- 6 files changed, 158 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5927cd62cd..04d00e1881 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6895,6 +6895,7 @@ dependencies = [ "futures", "katana-primitives", "katana-provider", + "katana-rpc-types", "parking_lot 0.12.1", "rstest", "serde_json", @@ -6904,6 +6905,7 @@ dependencies = [ "starknet_api", "starknet_in_rust", "thiserror", + "tokio", "tracing", ] diff --git a/crates/katana/executor/Cargo.toml b/crates/katana/executor/Cargo.toml index e1820e9fa0..3cf0872a56 100644 --- a/crates/katana/executor/Cargo.toml +++ b/crates/katana/executor/Cargo.toml @@ -30,9 +30,11 @@ starknet-types-core = { version = "0.0.9", optional = true } [dev-dependencies] cairo-vm.workspace = true katana-provider.workspace = true +katana-rpc-types.workspace = true rstest.workspace = true serde_json.workspace = true similar-asserts.workspace = true +tokio.workspace = true [features] default = [ "blockifier", "sir" ] diff --git a/crates/katana/executor/src/implementation/blockifier/utils.rs b/crates/katana/executor/src/implementation/blockifier/utils.rs index d5fe9b926f..b92e6626ca 100644 --- a/crates/katana/executor/src/implementation/blockifier/utils.rs +++ b/crates/katana/executor/src/implementation/blockifier/utils.rs @@ -8,13 +8,13 @@ use blockifier::execution::entry_point::{ CallEntryPoint, EntryPointExecutionContext, ExecutionResources, }; use blockifier::execution::errors::EntryPointExecutionError; -use blockifier::fee::fee_utils::calculate_tx_l1_gas_usages; +use blockifier::fee::fee_utils::{self, calculate_tx_l1_gas_usages}; use blockifier::state::cached_state::{self}; use blockifier::state::state_api::{State, StateReader}; use blockifier::transaction::account_transaction::AccountTransaction; use blockifier::transaction::errors::{TransactionExecutionError, TransactionFeeError}; use blockifier::transaction::objects::{ - AccountTransactionContext, DeprecatedAccountTransactionContext, + AccountTransactionContext, DeprecatedAccountTransactionContext, FeeType, HasRelatedFeeType, }; use blockifier::transaction::transaction_execution::Transaction; use blockifier::transaction::transactions::{ @@ -65,7 +65,10 @@ pub(super) fn transact( let validate = !simulation_flags.skip_validate; let charge_fee = !simulation_flags.skip_fee_transfer; - let res = match to_executor_tx(tx) { + let transaction = to_executor_tx(tx); + let fee_type = get_fee_type_from_tx(&transaction); + + let mut info = match transaction { Transaction::AccountTransaction(tx) => { tx.execute(state, block_context, charge_fee, validate) } @@ -74,8 +77,17 @@ pub(super) fn transact( } }?; - let gas_used = calculate_tx_l1_gas_usages(&res.actual_resources, block_context)?.gas_usage; - Ok(TransactionExecutionInfo { inner: res, gas_used }) + // There are a few case where the `actual_fee` field of the transaction info is not set where + // the fee is skipped and thus not charged for the transaction (e.g. when the `skip_fee_transfer` is + // explicitly set, or when the transaction `max_fee` is set to 0). In these cases, we still want to + // calculate the fee. + if info.actual_fee == Fee(0) { + let fee = fee_utils::calculate_tx_fee(&info.actual_resources, block_context, &fee_type)?; + info.actual_fee = fee; + } + + let gas_used = calculate_tx_l1_gas_usages(&info.actual_resources, block_context)?.gas_usage; + Ok(TransactionExecutionInfo { inner: info, gas_used }) } /// Perform a function call on a contract and retrieve the return values. @@ -442,3 +454,11 @@ fn to_api_resource_bounds( ResourceBoundsMapping(BTreeMap::from([(Resource::L1Gas, l1_gas), (Resource::L2Gas, l2_gas)])) } + +/// Get the fee type of a transaction. The fee type determines the token used to pay for the transaction. +fn get_fee_type_from_tx(transaction: &Transaction) -> FeeType { + match transaction { + Transaction::AccountTransaction(tx) => tx.fee_type(), + Transaction::L1HandlerTransaction(tx) => tx.fee_type(), + } +} diff --git a/crates/katana/executor/tests/fixtures/mod.rs b/crates/katana/executor/tests/fixtures/mod.rs index bc6281661d..dd4cd220d5 100644 --- a/crates/katana/executor/tests/fixtures/mod.rs +++ b/crates/katana/executor/tests/fixtures/mod.rs @@ -11,7 +11,7 @@ use katana_primitives::block::{ }; use katana_primitives::chain::ChainId; use katana_primitives::class::{CompiledClass, FlattenedSierraClass}; -use katana_primitives::contract::ContractAddress; +use katana_primitives::contract::{ContractAddress, Nonce}; use katana_primitives::env::{CfgEnv, FeeTokenAddressses}; use katana_primitives::genesis::allocation::DevAllocationsGenerator; use katana_primitives::genesis::constant::{ @@ -28,7 +28,11 @@ use katana_primitives::FieldElement; use katana_provider::providers::in_memory::InMemoryProvider; use katana_provider::traits::block::BlockWriter; use katana_provider::traits::state::{StateFactoryProvider, StateProvider}; -use starknet::macros::felt; +use starknet::accounts::{Account, Call, ExecutionEncoding, SingleOwnerAccount}; +use starknet::macros::{felt, selector}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{JsonRpcClient, Url}; +use starknet::signers::{LocalWallet, SigningKey}; // TODO: remove support for legacy contract declaration #[allow(unused)] @@ -48,9 +52,9 @@ pub fn contract_class() -> (CompiledClass, FlattenedSierraClass) { (compiled, sierra) } -/// Returns a state provider with some prefilled states. #[rstest::fixture] -pub fn state_provider() -> Box { +#[once] +pub fn genesis() -> Genesis { let mut seed = [0u8; 32]; seed[0] = b'0'; @@ -61,10 +65,56 @@ pub fn state_provider() -> Box { let mut genesis = Genesis::default(); genesis.extend_allocations(accounts.into_iter().map(|(k, v)| (k, v.into()))); + genesis +} - let provider = InMemoryProvider::new(); +#[allow(unused)] +pub fn signed_invoke_executable_tx( + address: ContractAddress, + private_key: FieldElement, + chain_id: ChainId, + nonce: Nonce, + max_fee: FieldElement, +) -> ExecutableTxWithHash { + let url = "http://localhost:5050"; + let provider = JsonRpcClient::new(HttpTransport::new(Url::try_from(url).unwrap())); + let signer = LocalWallet::from_signing_key(SigningKey::from_secret_scalar(private_key)); + + let account = SingleOwnerAccount::new( + provider, + signer, + address.into(), + chain_id.into(), + ExecutionEncoding::New, + ); + + let calls = vec![Call { + to: DEFAULT_FEE_TOKEN_ADDRESS.into(), + selector: selector!("transfer"), + calldata: vec![felt!("0x1"), felt!("0x99"), felt!("0x0")], + }]; + + let tx = account.execute(calls).nonce(nonce).max_fee(max_fee).prepared().unwrap(); + + let broadcasted_tx = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(tx.get_invoke_request(false)) + .unwrap(); + let tx = katana_rpc_types::transaction::BroadcastedInvokeTx(broadcasted_tx) + .into_tx_with_chain_id(chain_id); + + ExecutableTxWithHash::new(tx.into()) +} + +/// Returns a state provider with some prefilled states. +#[rstest::fixture] +pub fn state_provider(genesis: &Genesis) -> Box { let states = genesis.state_updates(); + let provider = InMemoryProvider::new(); + let block = SealedBlockWithStatus { status: FinalityStatus::AcceptedOnL2, block: Block::default().seal_with_hash(123u64.into()), diff --git a/crates/katana/executor/tests/simulate.rs b/crates/katana/executor/tests/simulate.rs index 681564e4bf..43e47f0a9d 100644 --- a/crates/katana/executor/tests/simulate.rs +++ b/crates/katana/executor/tests/simulate.rs @@ -1 +1,72 @@ mod fixtures; + +use fixtures::cfg; +use fixtures::genesis; +use fixtures::signed_invoke_executable_tx; +use fixtures::state_provider; +use katana_executor::ExecutionOutput; +use katana_executor::ExecutorFactory; +use katana_executor::SimulationFlag; +use katana_primitives::block::GasPrices; +use katana_primitives::env::BlockEnv; +use katana_primitives::env::CfgEnv; +use katana_primitives::genesis::allocation::GenesisAllocation; +use katana_primitives::genesis::Genesis; +use katana_primitives::transaction::ExecutableTxWithHash; +use katana_primitives::FieldElement; +use katana_provider::traits::state::StateProvider; +use starknet::macros::felt; + +#[rstest::fixture] +fn block_env() -> BlockEnv { + let l1_gas_prices = GasPrices { eth: 1000, strk: 1000 }; + BlockEnv { l1_gas_prices, sequencer_address: felt!("0x1").into(), ..Default::default() } +} + +#[rstest::fixture] +fn executable_tx_without_max_fee(genesis: &Genesis, cfg: CfgEnv) -> ExecutableTxWithHash { + let (addr, alloc) = genesis.allocations.first_key_value().expect("should have account"); + + let GenesisAllocation::Account(account) = alloc else { + panic!("should be account"); + }; + + signed_invoke_executable_tx( + *addr, + account.private_key().unwrap(), + cfg.chain_id, + FieldElement::ZERO, + FieldElement::ZERO, + ) +} + +#[rstest::rstest] +// TODO: uncomment after fixing the invalid validate entry point retdata issue +// #[cfg_attr(feature = "sir", case::sir(fixtures::sir::factory::default()))] +#[cfg_attr(feature = "blockifier", case::blockifier(fixtures::blockifier::factory::default()))] +fn test_simulate_tx( + #[case] factory: EF, + block_env: BlockEnv, + #[from(state_provider)] state: Box, + #[from(executable_tx_without_max_fee)] transaction: ExecutableTxWithHash, +) { + let mut executor = factory.with_state_and_block_env(state, block_env); + + let res = executor.simulate(transaction, SimulationFlag::default()).expect("must simulate"); + assert!(res.gas_used() != 0, "gas must be consumed"); + assert!(res.actual_fee() != 0, "actual fee must be computed"); + + // check that the underlying state is not modified + let ExecutionOutput { states, transactions } = + executor.take_execution_output().expect("must take output"); + + assert!(transactions.is_empty(), "simulated tx should not be stored"); + + assert!(states.state_updates.nonce_updates.is_empty(), "no state updates"); + assert!(states.state_updates.storage_updates.is_empty(), "no state updates"); + assert!(states.state_updates.contract_updates.is_empty(), "no state updates"); + assert!(states.state_updates.declared_classes.is_empty(), "no state updates"); + + assert!(states.declared_sierra_classes.is_empty(), "no new classes should be declared"); + assert!(states.declared_compiled_classes.is_empty(), "no new classes should be declared"); +} diff --git a/crates/katana/rpc/rpc-types/src/transaction.rs b/crates/katana/rpc/rpc-types/src/transaction.rs index ea273dec50..0da613f0c1 100644 --- a/crates/katana/rpc/rpc-types/src/transaction.rs +++ b/crates/katana/rpc/rpc-types/src/transaction.rs @@ -26,7 +26,7 @@ use crate::receipt::MaybePendingTxReceipt; #[derive(Debug, Clone, Serialize, Deserialize, Deref)] #[serde(transparent)] -pub struct BroadcastedInvokeTx(BroadcastedInvokeTransaction); +pub struct BroadcastedInvokeTx(pub BroadcastedInvokeTransaction); impl BroadcastedInvokeTx { pub fn is_query(&self) -> bool { @@ -66,7 +66,7 @@ impl BroadcastedInvokeTx { #[derive(Debug, Clone, Serialize, Deserialize, Deref)] #[serde(transparent)] -pub struct BroadcastedDeclareTx(BroadcastedDeclareTransaction); +pub struct BroadcastedDeclareTx(pub BroadcastedDeclareTransaction); impl BroadcastedDeclareTx { /// Validates that the provided compiled class hash is computed correctly from the class @@ -168,7 +168,7 @@ impl BroadcastedDeclareTx { #[derive(Debug, Clone, Serialize, Deserialize, Deref)] #[serde(transparent)] -pub struct BroadcastedDeployAccountTx(BroadcastedDeployAccountTransaction); +pub struct BroadcastedDeployAccountTx(pub BroadcastedDeployAccountTransaction); impl BroadcastedDeployAccountTx { pub fn is_query(&self) -> bool {