Skip to content

Commit

Permalink
test(katana-executor): add test for transaction simulation (#1593)
Browse files Browse the repository at this point in the history
  • Loading branch information
kariy committed Mar 21, 2024
1 parent bce0197 commit 96e928f
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 13 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/katana/executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]
Expand Down
30 changes: 25 additions & 5 deletions crates/katana/executor/src/implementation/blockifier/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -65,7 +65,10 @@ pub(super) fn transact<S: StateReader>(
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)
}
Expand All @@ -74,8 +77,17 @@ pub(super) fn transact<S: StateReader>(
}
}?;

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.
Expand Down Expand Up @@ -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(),
}
}
60 changes: 55 additions & 5 deletions crates/katana/executor/tests/fixtures/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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)]
Expand All @@ -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<dyn StateProvider> {
#[once]
pub fn genesis() -> Genesis {
let mut seed = [0u8; 32];
seed[0] = b'0';

Expand All @@ -61,10 +65,56 @@ pub fn state_provider() -> Box<dyn StateProvider> {

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<dyn StateProvider> {
let states = genesis.state_updates();
let provider = InMemoryProvider::new();

let block = SealedBlockWithStatus {
status: FinalityStatus::AcceptedOnL2,
block: Block::default().seal_with_hash(123u64.into()),
Expand Down
71 changes: 71 additions & 0 deletions crates/katana/executor/tests/simulate.rs
Original file line number Diff line number Diff line change
@@ -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<EF: ExecutorFactory>(
#[case] factory: EF,
block_env: BlockEnv,
#[from(state_provider)] state: Box<dyn StateProvider>,
#[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");
}
6 changes: 3 additions & 3 deletions crates/katana/rpc/rpc-types/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 96e928f

Please sign in to comment.