Skip to content

Commit

Permalink
feat(katana): add simulate transactions (#1590)
Browse files Browse the repository at this point in the history
* add simulation to the RPC

* simulate transaction on sequencer

* add `new_query`

* add fee type

* lint

* update to `simulate_transactions`

* remove `simulation.rs`

* individual parameters for `simulate_transactions`

* lint rebase

* update to fit spec

* pin link
  • Loading branch information
greged93 authored Mar 14, 2024
1 parent 6f72e33 commit d441619
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 12 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

27 changes: 26 additions & 1 deletion crates/katana/core/src/sequencer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ use katana_provider::traits::state::{StateFactoryProvider, StateProvider};
use katana_provider::traits::transaction::{
ReceiptProvider, TransactionProvider, TransactionsProviderExt,
};
use starknet::core::types::{BlockTag, EmittedEvent, EventsPage, FeeEstimate};
use starknet::core::types::{
BlockTag, EmittedEvent, EventsPage, FeeEstimate, SimulatedTransaction,
};

use crate::backend::config::StarknetConfig;
use crate::backend::contract::StarknetContract;
Expand Down Expand Up @@ -215,6 +217,29 @@ impl KatanaSequencer {
.map_err(SequencerError::TransactionExecution)
}

pub fn simulate_transactions(
&self,
transactions: Vec<ExecutableTxWithHash>,
block_id: BlockIdOrTag,
validate: bool,
charge_fee: bool,
) -> SequencerResult<Vec<SimulatedTransaction>> {
let state = self.state(&block_id)?;

let block_context = self
.block_execution_context_at(block_id)?
.ok_or_else(|| SequencerError::BlockNotFound(block_id))?;

katana_executor::blockifier::utils::simulate_transactions(
transactions,
&block_context,
state,
validate,
charge_fee,
)
.map_err(SequencerError::TransactionExecution)
}

pub fn block_hash_and_number(&self) -> SequencerResult<(BlockHash, BlockNumber)> {
let provider = self.backend.blockchain.provider();
let hash = BlockHashProvider::latest_hash(provider)?;
Expand Down
1 change: 1 addition & 0 deletions crates/katana/executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ katana-primitives = { path = "../primitives" }
katana-provider = { path = "../storage/provider" }

anyhow.workspace = true
cairo-vm.workspace = true
convert_case.workspace = true
futures.workspace = true
parking_lot.workspace = true
Expand Down
167 changes: 165 additions & 2 deletions crates/katana/executor/src/blockifier/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,30 @@ use blockifier::transaction::errors::TransactionExecutionError;
use blockifier::transaction::objects::{
DeprecatedAccountTransactionContext, ResourcesMapping, TransactionExecutionInfo,
};
use cairo_vm::vm::runners::builtin_runner::{
BITWISE_BUILTIN_NAME, EC_OP_BUILTIN_NAME, HASH_BUILTIN_NAME, KECCAK_BUILTIN_NAME,
POSEIDON_BUILTIN_NAME, RANGE_CHECK_BUILTIN_NAME, SEGMENT_ARENA_BUILTIN_NAME,
SIGNATURE_BUILTIN_NAME,
};
use convert_case::{Case, Casing};
use katana_primitives::contract::ContractAddress;
use katana_primitives::env::{BlockEnv, CfgEnv};
use katana_primitives::receipt::{Event, MessageToL1};
use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses};
use katana_primitives::transaction::ExecutableTxWithHash;
use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash};
use katana_primitives::FieldElement;
use katana_provider::traits::contract::ContractClassProvider;
use katana_provider::traits::state::StateProvider;
use starknet::core::types::{FeeEstimate, PriceUnit};
use starknet::core::types::{
DeclareTransactionTrace, DeployAccountTransactionTrace, ExecuteInvocation, FeeEstimate,
FunctionInvocation, InvokeTransactionTrace, L1HandlerTransactionTrace, PriceUnit,
RevertedInvocation, SimulatedTransaction, TransactionTrace,
};
use starknet::core::utils::parse_cairo_short_string;
use starknet::macros::felt;
use starknet_api::block::{BlockNumber, BlockTimestamp};
use starknet_api::core::EntryPointSelector;
use starknet_api::hash::StarkFelt;
use starknet_api::transaction::Calldata;
use tracing::trace;

Expand Down Expand Up @@ -87,6 +97,84 @@ pub fn estimate_fee(
.collect::<Result<Vec<_>, _>>()
}

/// Simulate a transaction's execution on the state
pub fn simulate_transactions(
transactions: Vec<ExecutableTxWithHash>,
block_context: &BlockContext,
state: Box<dyn StateProvider>,
validate: bool,
charge_fee: bool,
) -> Result<Vec<SimulatedTransaction>, TransactionExecutionError> {
let state = CachedStateWrapper::new(StateRefDb(state));
let results = TransactionExecutor::new(
&state,
block_context,
charge_fee,
validate,
transactions.clone().into_iter(),
)
.with_error_log()
.execute();

results
.into_iter()
.zip(transactions)
.map(|(result, tx)| {
let result = result?;
let function_invocation = result
.execute_call_info
.as_ref()
.map(function_invocation_from_call_info)
.ok_or(TransactionExecutionError::ExecutionError(
EntryPointExecutionError::ExecutionFailed { error_data: Default::default() },
));

let validate_invocation =
result.validate_call_info.as_ref().map(function_invocation_from_call_info);

let fee_transfer_invocation =
result.fee_transfer_call_info.as_ref().map(function_invocation_from_call_info);

let transaction_trace = match &tx.transaction {
ExecutableTx::Declare(_) => TransactionTrace::Declare(DeclareTransactionTrace {
validate_invocation,
fee_transfer_invocation,
state_diff: None,
}),
ExecutableTx::DeployAccount(_) => {
TransactionTrace::DeployAccount(DeployAccountTransactionTrace {
constructor_invocation: function_invocation?,
validate_invocation,
fee_transfer_invocation,
state_diff: None,
})
}
ExecutableTx::Invoke(_) => TransactionTrace::Invoke(InvokeTransactionTrace {
validate_invocation,
execute_invocation: if let Some(revert_reason) = result.revert_error.as_ref() {
ExecuteInvocation::Reverted(RevertedInvocation {
revert_reason: revert_reason.clone(),
})
} else {
ExecuteInvocation::Success(function_invocation?)
},
fee_transfer_invocation,
state_diff: None,
}),
ExecutableTx::L1Handler(_) => {
TransactionTrace::L1Handler(L1HandlerTransactionTrace {
function_invocation: function_invocation?,
state_diff: None,
})
}
};
let fee_estimation = calculate_execution_fee(block_context, &result)?;

Ok(SimulatedTransaction { transaction_trace, fee_estimation })
})
.collect::<Result<Vec<_>, _>>()
}

/// Perform a raw entrypoint call of a contract.
pub fn raw_call(
request: EntryPointCall,
Expand Down Expand Up @@ -397,3 +485,78 @@ pub(super) fn l2_to_l1_messages_from_exec_info(

messages
}

fn function_invocation_from_call_info(info: &CallInfo) -> FunctionInvocation {
let entry_point_type = match info.call.entry_point_type {
starknet_api::deprecated_contract_class::EntryPointType::Constructor => {
starknet::core::types::EntryPointType::Constructor
}
starknet_api::deprecated_contract_class::EntryPointType::External => {
starknet::core::types::EntryPointType::External
}
starknet_api::deprecated_contract_class::EntryPointType::L1Handler => {
starknet::core::types::EntryPointType::L1Handler
}
};
let call_type = match info.call.call_type {
blockifier::execution::entry_point::CallType::Call => starknet::core::types::CallType::Call,
blockifier::execution::entry_point::CallType::Delegate => {
starknet::core::types::CallType::Delegate
}
};

let calls = info.inner_calls.iter().map(function_invocation_from_call_info).collect();
let events = info
.execution
.events
.iter()
.map(|e| starknet::core::types::OrderedEvent {
order: e.order as u64,
data: e.event.data.0.iter().map(|d| (*d).into()).collect(),
keys: e.event.keys.iter().map(|k| k.0.into()).collect(),
})
.collect();
let messages = info
.execution
.l2_to_l1_messages
.iter()
.map(|m| starknet::core::types::OrderedMessage {
order: m.order as u64,
to_address: (Into::<StarkFelt>::into(m.message.to_address)).into(),
from_address: (*info.call.storage_address.0.key()).into(),
payload: m.message.payload.0.iter().map(|p| (*p).into()).collect(),
})
.collect();

let vm_resources = info.vm_resources.filter_unused_builtins();
let get_vm_resource =
|name: &str| vm_resources.builtin_instance_counter.get(name).map(|r| *r as u64);
let execution_resources = starknet::core::types::ExecutionResources {
steps: vm_resources.n_steps as u64,
memory_holes: Some(vm_resources.n_memory_holes as u64),
range_check_builtin_applications: get_vm_resource(RANGE_CHECK_BUILTIN_NAME),
pedersen_builtin_applications: get_vm_resource(HASH_BUILTIN_NAME),
poseidon_builtin_applications: get_vm_resource(POSEIDON_BUILTIN_NAME),
ec_op_builtin_applications: get_vm_resource(EC_OP_BUILTIN_NAME),
ecdsa_builtin_applications: get_vm_resource(SIGNATURE_BUILTIN_NAME),
bitwise_builtin_applications: get_vm_resource(BITWISE_BUILTIN_NAME),
keccak_builtin_applications: get_vm_resource(KECCAK_BUILTIN_NAME),
segment_arena_builtin: get_vm_resource(SEGMENT_ARENA_BUILTIN_NAME),
};

FunctionInvocation {
contract_address: (*info.call.storage_address.0.key()).into(),
entry_point_selector: info.call.entry_point_selector.0.into(),
calldata: info.call.calldata.0.iter().map(|f| (*f).into()).collect(),
caller_address: (*info.call.caller_address.0.key()).into(),
// See <https://github.com/starkware-libs/blockifier/blob/cb464f5ac2ada88f2844d9f7d62bd6732ceb5b2c/crates/blockifier/src/execution/call_info.rs#L220>
class_hash: info.call.class_hash.expect("Class hash mut be set after execution").0.into(),
entry_point_type,
call_type,
result: info.execution.retdata.0.iter().map(|f| (*f).into()).collect(),
calls,
events,
messages,
execution_resources,
}
}
16 changes: 13 additions & 3 deletions crates/katana/rpc/rpc-api/src/starknet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ use katana_rpc_types::transaction::{
DeclareTxResult, DeployAccountTxResult, InvokeTxResult, Tx,
};
use katana_rpc_types::{
ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlags, SyncingStatus,
ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlag,
SimulationFlagForEstimateFee, SyncingStatus,
};
use starknet::core::types::TransactionStatus;
use starknet::core::types::{SimulatedTransaction, TransactionStatus};

/// The currently supported version of the Starknet JSON-RPC specification.
pub const RPC_SPEC_VERSION: &str = "0.6.0";
Expand Down Expand Up @@ -122,7 +123,7 @@ pub trait StarknetApi {
async fn estimate_fee(
&self,
request: Vec<BroadcastedTx>,
simulation_flags: Vec<SimulationFlags>,
simulation_flags: Vec<SimulationFlagForEstimateFee>,
block_id: BlockIdOrTag,
) -> RpcResult<Vec<FeeEstimate>>;

Expand Down Expand Up @@ -184,4 +185,13 @@ pub trait StarknetApi {
&self,
deploy_account_transaction: BroadcastedDeployAccountTx,
) -> RpcResult<DeployAccountTxResult>;

/// Simulates a list of transactions on the provided block.
#[method(name = "simulateTransactions")]
async fn simulate_transactions(
&self,
block_id: BlockIdOrTag,
transactions: Vec<BroadcastedTx>,
simulation_flags: Vec<SimulationFlag>,
) -> RpcResult<Vec<SimulatedTransaction>>;
}
4 changes: 3 additions & 1 deletion crates/katana/rpc/rpc-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ pub type FeeEstimate = starknet::core::types::FeeEstimate;

pub type ContractClass = starknet::core::types::ContractClass;

pub type SimulationFlags = starknet::core::types::SimulationFlagForEstimateFee;
pub type SimulationFlagForEstimateFee = starknet::core::types::SimulationFlagForEstimateFee;

pub type SimulationFlag = starknet::core::types::SimulationFlag;

pub type SyncingStatus = starknet::core::types::SyncStatusType;

Expand Down
70 changes: 65 additions & 5 deletions crates/katana/rpc/rpc/src/starknet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@ use katana_rpc_types::transaction::{
BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, BroadcastedTx,
DeclareTxResult, DeployAccountTxResult, InvokeTxResult, Tx,
};
use katana_rpc_types::{ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlags};
use katana_rpc_types::{
ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlag,
SimulationFlagForEstimateFee,
};
use katana_rpc_types_builder::ReceiptBuilder;
use katana_tasks::{BlockingTaskPool, TokioTaskSpawner};
use starknet::core::types::{BlockTag, TransactionExecutionStatus, TransactionStatus};
use starknet::core::types::{
BlockTag, SimulatedTransaction, TransactionExecutionStatus, TransactionStatus,
};

#[derive(Clone)]
pub struct StarknetApi {
Expand Down Expand Up @@ -470,7 +475,7 @@ impl StarknetApiServer for StarknetApi {
async fn estimate_fee(
&self,
request: Vec<BroadcastedTx>,
simulation_flags: Vec<SimulationFlags>,
simulation_flags: Vec<SimulationFlagForEstimateFee>,
block_id: BlockIdOrTag,
) -> RpcResult<Vec<FeeEstimate>> {
self.on_cpu_blocking_task(move |this| {
Expand Down Expand Up @@ -508,8 +513,9 @@ impl StarknetApiServer for StarknetApi {
})
.collect::<Result<Vec<_>, _>>()?;

let skip_validate =
simulation_flags.iter().any(|flag| flag == &SimulationFlags::SkipValidate);
let skip_validate = simulation_flags
.iter()
.any(|flag| flag == &SimulationFlagForEstimateFee::SkipValidate);

let res = this
.inner
Expand Down Expand Up @@ -663,4 +669,58 @@ impl StarknetApiServer for StarknetApi {
})
.await
}

async fn simulate_transactions(
&self,
block_id: BlockIdOrTag,
transactions: Vec<BroadcastedTx>,
simulation_flags: Vec<SimulationFlag>,
) -> RpcResult<Vec<SimulatedTransaction>> {
self.on_cpu_blocking_task(move |this| {
let charge_fee = !simulation_flags.contains(&SimulationFlag::SkipFeeCharge);
let validate = !simulation_flags.contains(&SimulationFlag::SkipValidate);
let chain_id = this.inner.sequencer.chain_id();
let executables = transactions
.into_iter()
.map(|tx| {
let tx = match tx {
BroadcastedTx::Invoke(tx) => {
let is_query = tx.is_query();
ExecutableTxWithHash::new_query(
ExecutableTx::Invoke(tx.into_tx_with_chain_id(chain_id)),
is_query,
)
}
BroadcastedTx::Declare(tx) => {
let is_query = tx.is_query();
ExecutableTxWithHash::new_query(
ExecutableTx::Declare(
tx.try_into_tx_with_chain_id(chain_id)
.map_err(|_| StarknetApiError::InvalidContractClass)?,
),
is_query,
)
}
BroadcastedTx::DeployAccount(tx) => {
let is_query = tx.is_query();
ExecutableTxWithHash::new_query(
ExecutableTx::DeployAccount(tx.into_tx_with_chain_id(chain_id)),
is_query,
)
}
};
Result::<ExecutableTxWithHash, StarknetApiError>::Ok(tx)
})
.collect::<Result<Vec<_>, _>>()?;

let res = this
.inner
.sequencer
.simulate_transactions(executables, block_id, validate, charge_fee)
.map_err(StarknetApiError::from)?;

Ok(res)
})
.await
}
}

0 comments on commit d441619

Please sign in to comment.