From a8c5c3587df12125005b099c17000bb2c59f7945 Mon Sep 17 00:00:00 2001 From: Kariy Date: Mon, 27 Nov 2023 14:14:47 +0800 Subject: [PATCH] refactor(katana-core): refactor out execution logic into separate crate --- Cargo.lock | 15 ++ Cargo.toml | 1 + crates/katana/executor/Cargo.toml | 21 ++ crates/katana/executor/src/blockifier/mod.rs | 167 +++++++++++++++ .../katana/executor/src/blockifier/outcome.rs | 69 ++++++ .../katana/executor/src/blockifier/state.rs | 198 ++++++++++++++++++ .../katana/executor/src/blockifier/utils.rs | 171 +++++++++++++++ crates/katana/executor/src/lib.rs | 1 + 8 files changed, 643 insertions(+) create mode 100644 crates/katana/executor/Cargo.toml create mode 100644 crates/katana/executor/src/blockifier/mod.rs create mode 100644 crates/katana/executor/src/blockifier/outcome.rs create mode 100644 crates/katana/executor/src/blockifier/state.rs create mode 100644 crates/katana/executor/src/blockifier/utils.rs create mode 100644 crates/katana/executor/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index aacf426407..0ce16c044d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4929,6 +4929,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "katana-executor" +version = "0.3.12" +dependencies = [ + "blockifier", + "convert_case 0.6.0", + "katana-primitives", + "katana-provider", + "parking_lot 0.12.1", + "starknet", + "starknet_api", + "tokio", + "tracing", +] + [[package]] name = "katana-primitives" version = "0.3.14" diff --git a/Cargo.toml b/Cargo.toml index 2f27361be3..dcd1e198a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/dojo-world", "crates/katana", "crates/katana/core", + "crates/katana/executor", "crates/katana/primitives", "crates/katana/rpc", "crates/katana/storage/db", diff --git a/crates/katana/executor/Cargo.toml b/crates/katana/executor/Cargo.toml new file mode 100644 index 0000000000..f0df3ad3d6 --- /dev/null +++ b/crates/katana/executor/Cargo.toml @@ -0,0 +1,21 @@ +[package] +description = "Katana execution engine" +edition.workspace = true +name = "katana-executor" +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +katana-primitives = { path = "../primitives" } +katana-provider = { path = "../storage/provider" } + +convert_case.workspace = true +parking_lot.workspace = true +starknet.workspace = true +tracing.workspace = true + +# blockifier deps +blockifier.workspace = true +starknet_api.workspace = true +tokio.workspace = true diff --git a/crates/katana/executor/src/blockifier/mod.rs b/crates/katana/executor/src/blockifier/mod.rs new file mode 100644 index 0000000000..35a7d7951d --- /dev/null +++ b/crates/katana/executor/src/blockifier/mod.rs @@ -0,0 +1,167 @@ +pub mod outcome; +pub mod state; +pub mod utils; + +use std::sync::Arc; + +use blockifier::block_context::BlockContext; +use blockifier::state::state_api::StateReader; +use blockifier::transaction::errors::TransactionExecutionError; +use blockifier::transaction::objects::TransactionExecutionInfo; +use blockifier::transaction::transaction_execution::Transaction as BlockifierExecuteTx; +use blockifier::transaction::transactions::ExecutableTransaction; +use katana_primitives::transaction::{DeclareTxWithClasses, ExecutionTx}; +use parking_lot::RwLock; +use tracing::{trace, warn}; + +use self::outcome::ExecutedTx; +use self::state::{CachedStateWrapper, StateRefDb}; +use self::utils::events_from_exec_info; +use crate::blockifier::utils::{ + pretty_print_resources, trace_events, warn_message_transaction_error_exec_error, +}; + +/// The result of a transaction execution. +type TxExecutionResult = Result; + +/// A transaction executor. +/// +/// The transactions will be executed in an iterator fashion, sequentially, in the +/// exact order they are provided to the executor. The execution is done within its +/// implementation of the [`Iterator`] trait. +pub struct TransactionExecutor<'a, S: StateReader> { + /// A flag to enable/disable fee charging. + charge_fee: bool, + /// The block context the transactions will be executed on. + block_context: &'a BlockContext, + /// The transactions to be executed (in the exact order they are in the iterator). + transactions: std::vec::IntoIter, + /// The state the transactions will be executed on. + state: &'a mut CachedStateWrapper, + + // logs flags + error_log: bool, + events_log: bool, + resources_log: bool, +} + +impl<'a, S: StateReader> TransactionExecutor<'a, S> { + pub fn new( + state: &'a mut CachedStateWrapper, + block_context: &'a BlockContext, + charge_fee: bool, + transactions: Vec, + ) -> Self { + Self { + state, + charge_fee, + block_context, + error_log: false, + events_log: false, + resources_log: false, + transactions: transactions.into_iter(), + } + } + + pub fn with_events_log(self) -> Self { + Self { events_log: true, ..self } + } + + pub fn with_error_log(self) -> Self { + Self { error_log: true, ..self } + } + + pub fn with_resources_log(self) -> Self { + Self { resources_log: true, ..self } + } + + /// A method to conveniently execute all the transactions and return their results. + pub fn execute(self) -> Vec { + self.collect() + } +} + +impl<'a, S: StateReader> Iterator for TransactionExecutor<'a, S> { + type Item = TxExecutionResult; + fn next(&mut self) -> Option { + self.transactions.next().map(|tx| { + let res = execute_tx(tx, &mut self.state, self.block_context, self.charge_fee); + + match res { + Ok(info) => { + if self.error_log { + if let Some(err) = &info.revert_error { + let formatted_err = format!("{err:?}").replace("\\n", "\n"); + warn!(target: "executor", "Transaction execution error: {formatted_err}"); + } + } + + if self.resources_log { + trace!( + target: "executor", + "Transaction resource usage: {}", + pretty_print_resources(&info.actual_resources) + ); + } + + if self.events_log { + trace_events(&events_from_exec_info(&info)); + } + + Ok(info) + } + + Err(err) => { + if self.error_log { + warn_message_transaction_error_exec_error(&err); + } + + Err(err) + } + } + }) + } +} + +pub struct PendingState { + pub state: RwLock>, + /// The transactions that have been executed. + pub executed_transactions: RwLock>>, +} + +fn execute_tx( + tx: ExecutionTx, + state: &mut CachedStateWrapper, + block_context: &BlockContext, + charge_fee: bool, +) -> TxExecutionResult { + let sierra = if let ExecutionTx::Declare(DeclareTxWithClasses { + tx, + sierra_class: Some(sierra_class), + .. + }) = &tx + { + Some((tx.class_hash, sierra_class.clone())) + } else { + None + }; + + let res = match tx.into() { + BlockifierExecuteTx::AccountTransaction(tx) => { + tx.execute(&mut state.inner_mut(), block_context, charge_fee) + } + BlockifierExecuteTx::L1HandlerTransaction(tx) => { + tx.execute(&mut state.inner_mut(), block_context, charge_fee) + } + }; + + if let res @ Ok(_) = res { + if let Some((class_hash, sierra_class)) = sierra { + state.sierra_class_mut().insert(class_hash, sierra_class); + } + + res + } else { + res + } +} diff --git a/crates/katana/executor/src/blockifier/outcome.rs b/crates/katana/executor/src/blockifier/outcome.rs new file mode 100644 index 0000000000..27c98a185d --- /dev/null +++ b/crates/katana/executor/src/blockifier/outcome.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; + +use blockifier::state::cached_state::CommitmentStateDiff; +use blockifier::transaction::objects::TransactionExecutionInfo; +use katana_primitives::contract::{ClassHash, CompiledContractClass, ContractAddress, SierraClass}; +use katana_primitives::transaction::{Receipt, Tx}; + +use super::utils::{events_from_exec_info, l2_to_l1_messages_from_exec_info}; + +pub struct ExecutedTx { + pub tx: Tx, + pub receipt: Receipt, + pub execution_info: TransactionExecutionInfo, +} + +impl ExecutedTx { + pub(super) fn new(tx: Tx, execution_info: TransactionExecutionInfo) -> Self { + let actual_fee = execution_info.actual_fee.0; + let events = events_from_exec_info(&execution_info); + let revert_error = execution_info.revert_error.clone(); + let messages_sent = l2_to_l1_messages_from_exec_info(&execution_info); + let actual_resources = execution_info.actual_resources.0.clone(); + + let contract_address = if let Tx::DeployAccount(ref tx) = tx { + Some(ContractAddress(tx.contract_address.into())) + } else { + None + }; + + Self { + tx, + execution_info, + receipt: Receipt { + events, + actual_fee, + revert_error, + messages_sent, + actual_resources, + contract_address, + }, + } + } +} + +/// The outcome that after executing a list of transactions. +pub struct ExecutionOutcome { + pub transactions: Vec, + pub state_diff: CommitmentStateDiff, + pub declared_classes: HashMap, + pub declared_sierra_classes: HashMap, +} + +impl Default for ExecutionOutcome { + fn default() -> Self { + let state_diff = CommitmentStateDiff { + storage_updates: Default::default(), + address_to_nonce: Default::default(), + address_to_class_hash: Default::default(), + class_hash_to_compiled_class_hash: Default::default(), + }; + + Self { + state_diff, + transactions: Default::default(), + declared_classes: Default::default(), + declared_sierra_classes: Default::default(), + } + } +} diff --git a/crates/katana/executor/src/blockifier/state.rs b/crates/katana/executor/src/blockifier/state.rs new file mode 100644 index 0000000000..4bdb053fb7 --- /dev/null +++ b/crates/katana/executor/src/blockifier/state.rs @@ -0,0 +1,198 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use blockifier::execution::contract_class::ContractClass; +use blockifier::state::cached_state::{CachedState, CommitmentStateDiff}; +use blockifier::state::errors::StateError; +use blockifier::state::state_api::{State, StateReader, StateResult}; +use katana_primitives::contract::SierraClass; +use katana_provider::traits::state::StateProvider; +use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce}; +use starknet_api::hash::StarkFelt; +use starknet_api::state::StorageKey; +use tokio::sync::RwLock as AsyncRwLock; + +pub struct StateRefDb(Box); + +impl StateRefDb { + pub fn new(provider: impl StateProvider + 'static) -> Self { + Self(Box::new(provider)) + } +} + +impl From for StateRefDb +where + T: StateProvider + 'static, +{ + fn from(provider: T) -> Self { + Self::new(provider) + } +} + +impl StateReader for StateRefDb { + fn get_nonce_at( + &mut self, + contract_address: starknet_api::core::ContractAddress, + ) -> blockifier::state::state_api::StateResult { + StateProvider::nonce(&self.0, contract_address.into()) + .map(|n| Nonce(n.unwrap_or_default().into())) + .map_err(|e| StateError::StateReadError(e.to_string())) + } + + fn get_storage_at( + &mut self, + contract_address: starknet_api::core::ContractAddress, + key: starknet_api::state::StorageKey, + ) -> blockifier::state::state_api::StateResult { + StateProvider::storage(&self.0, contract_address.into(), (*key.0.key()).into()) + .map(|v| v.unwrap_or_default().into()) + .map_err(|e| StateError::StateReadError(e.to_string())) + } + + fn get_class_hash_at( + &mut self, + contract_address: starknet_api::core::ContractAddress, + ) -> blockifier::state::state_api::StateResult { + StateProvider::class_hash_of_contract(&self.0, contract_address.into()) + .map(|v| ClassHash(v.unwrap_or_default().into())) + .map_err(|e| StateError::StateReadError(e.to_string())) + } + + fn get_compiled_class_hash( + &mut self, + class_hash: starknet_api::core::ClassHash, + ) -> blockifier::state::state_api::StateResult { + if let Some(hash) = + StateProvider::compiled_class_hash_of_class_hash(&self.0, class_hash.0.into()) + .map_err(|e| StateError::StateReadError(e.to_string()))? + { + Ok(CompiledClassHash(hash.into())) + } else { + Err(StateError::UndeclaredClassHash(class_hash)) + } + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &starknet_api::core::ClassHash, + ) -> blockifier::state::state_api::StateResult< + blockifier::execution::contract_class::ContractClass, + > { + if let Some(class) = StateProvider::class(&self.0, class_hash.0.into()) + .map_err(|e| StateError::StateReadError(e.to_string()))? + { + Ok(class) + } else { + Err(StateError::UndeclaredClassHash(*class_hash)) + } + } +} + +#[derive(Clone)] +pub struct CachedStateWrapper { + inner: Arc>>, + sierra_class: Arc>>, +} + +impl CachedStateWrapper { + pub fn new(db: S) -> Self { + Self { + sierra_class: Default::default(), + inner: Arc::new(AsyncRwLock::new(CachedState::new(db))), + } + } + + pub fn inner_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, CachedState> { + tokio::task::block_in_place(|| self.inner.blocking_write()) + } + + pub fn sierra_class( + &self, + ) -> tokio::sync::RwLockReadGuard< + '_, + HashMap, + > { + tokio::task::block_in_place(|| self.sierra_class.blocking_read()) + } + + pub fn sierra_class_mut( + &self, + ) -> tokio::sync::RwLockWriteGuard< + '_, + HashMap, + > { + tokio::task::block_in_place(|| self.sierra_class.blocking_write()) + } +} + +impl State for CachedStateWrapper { + fn increment_nonce(&mut self, contract_address: ContractAddress) -> StateResult<()> { + self.inner_mut().increment_nonce(contract_address) + } + + fn set_class_hash_at( + &mut self, + contract_address: ContractAddress, + class_hash: ClassHash, + ) -> StateResult<()> { + self.inner_mut().set_class_hash_at(contract_address, class_hash) + } + + fn set_compiled_class_hash( + &mut self, + class_hash: ClassHash, + compiled_class_hash: CompiledClassHash, + ) -> StateResult<()> { + self.inner_mut().set_compiled_class_hash(class_hash, compiled_class_hash) + } + + fn set_contract_class( + &mut self, + class_hash: &ClassHash, + contract_class: ContractClass, + ) -> StateResult<()> { + self.inner_mut().set_contract_class(class_hash, contract_class) + } + + fn set_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + value: StarkFelt, + ) { + self.inner_mut().set_storage_at(contract_address, key, value) + } + + fn to_state_diff(&self) -> CommitmentStateDiff { + self.inner_mut().to_state_diff() + } +} + +impl StateReader for CachedStateWrapper { + fn get_class_hash_at(&mut self, contract_address: ContractAddress) -> StateResult { + self.inner_mut().get_class_hash_at(contract_address) + } + + fn get_compiled_class_hash(&mut self, class_hash: ClassHash) -> StateResult { + self.inner_mut().get_compiled_class_hash(class_hash) + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &ClassHash, + ) -> StateResult { + self.inner_mut().get_compiled_contract_class(class_hash) + } + + fn get_nonce_at(&mut self, contract_address: ContractAddress) -> StateResult { + self.inner_mut().get_nonce_at(contract_address) + } + + fn get_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + ) -> StateResult { + self.inner_mut().get_storage_at(contract_address, key) + } +} diff --git a/crates/katana/executor/src/blockifier/utils.rs b/crates/katana/executor/src/blockifier/utils.rs new file mode 100644 index 0000000000..1d0f1a8bf7 --- /dev/null +++ b/crates/katana/executor/src/blockifier/utils.rs @@ -0,0 +1,171 @@ +use std::collections::HashMap; + +use blockifier::execution::entry_point::CallInfo; +use blockifier::execution::errors::EntryPointExecutionError; +use blockifier::state::state_api::{State, StateReader}; +use blockifier::transaction::errors::TransactionExecutionError; +use blockifier::transaction::objects::{ResourcesMapping, TransactionExecutionInfo}; +use convert_case::{Case, Casing}; +use katana_primitives::transaction::Tx; +use katana_primitives::FieldElement; +use starknet::core::types::{Event, MsgToL1}; +use starknet::core::utils::parse_cairo_short_string; +use tracing::trace; + +use super::outcome::{ExecutedTx, ExecutionOutcome}; +use super::state::{CachedStateWrapper, StateRefDb}; + +pub(crate) fn warn_message_transaction_error_exec_error(err: &TransactionExecutionError) { + match err { + TransactionExecutionError::EntryPointExecutionError(ref eperr) + | TransactionExecutionError::ExecutionError(ref eperr) => match eperr { + EntryPointExecutionError::ExecutionFailed { error_data } => { + let mut reasons: Vec = vec![]; + error_data.iter().for_each(|felt| { + if let Ok(s) = parse_cairo_short_string(&FieldElement::from(*felt)) { + reasons.push(s); + } + }); + + tracing::warn!(target: "executor", + "Transaction validation error: {}", reasons.join(" ")); + } + _ => tracing::warn!(target: "executor", + "Transaction validation error: {:?}", err), + }, + _ => tracing::warn!(target: "executor", + "Transaction validation error: {:?}", err), + } +} + +pub(crate) fn pretty_print_resources(resources: &ResourcesMapping) -> String { + let mut mapped_strings: Vec<_> = resources + .0 + .iter() + .filter_map(|(k, v)| match k.as_str() { + "l1_gas_usage" => Some(format!("L1 Gas: {}", v)), + "range_check_builtin" => Some(format!("Range Checks: {}", v)), + "ecdsa_builtin" => Some(format!("ECDSA: {}", v)), + "n_steps" => None, + "pedersen_builtin" => Some(format!("Pedersen: {}", v)), + "bitwise_builtin" => Some(format!("Bitwise: {}", v)), + "keccak_builtin" => Some(format!("Keccak: {}", v)), + _ => Some(format!("{}: {}", k.to_case(Case::Title), v)), + }) + .collect::>(); + + // Sort the strings alphabetically + mapped_strings.sort(); + + // Prepend "Steps" if it exists, so it is always first + if let Some(steps) = resources.0.get("n_steps") { + mapped_strings.insert(0, format!("Steps: {}", steps)); + } + + mapped_strings.join(" | ") +} + +pub(crate) fn trace_events(events: &[Event]) { + for e in events { + let formatted_keys = + e.keys.iter().map(|k| format!("{k:#x}")).collect::>().join(", "); + + trace!(target: "executor", "Event emitted keys=[{}]", formatted_keys); + } +} + +pub fn create_execution_outcome( + state: &mut CachedStateWrapper, + executed_txs: Vec<(Tx, TransactionExecutionInfo)>, +) -> ExecutionOutcome { + let transactions = executed_txs.into_iter().map(|(tx, res)| ExecutedTx::new(tx, res)).collect(); + let state_diff = state.to_state_diff(); + let declared_classes = state_diff + .class_hash_to_compiled_class_hash + .iter() + .map(|(class_hash, _)| { + let contract_class = state + .get_compiled_contract_class(class_hash) + .expect("qed; class must exist if declared"); + (class_hash.0.into(), contract_class) + }) + .collect::>(); + + ExecutionOutcome { + state_diff, + transactions, + declared_classes, + declared_sierra_classes: state.sierra_class().clone(), + } +} + +pub(crate) fn events_from_exec_info(execution_info: &TransactionExecutionInfo) -> Vec { + let mut events: Vec = vec![]; + + fn get_events_recursively(call_info: &CallInfo) -> Vec { + let mut events: Vec = vec![]; + + events.extend(call_info.execution.events.iter().map(|e| Event { + from_address: (*call_info.call.storage_address.0.key()).into(), + data: e.event.data.0.iter().map(|d| (*d).into()).collect(), + keys: e.event.keys.iter().map(|k| k.0.into()).collect(), + })); + + call_info.inner_calls.iter().for_each(|call| { + events.extend(get_events_recursively(call)); + }); + + events + } + + if let Some(ref call) = execution_info.validate_call_info { + events.extend(get_events_recursively(call)); + } + + if let Some(ref call) = execution_info.execute_call_info { + events.extend(get_events_recursively(call)); + } + + if let Some(ref call) = execution_info.fee_transfer_call_info { + events.extend(get_events_recursively(call)); + } + + events +} + +pub(crate) fn l2_to_l1_messages_from_exec_info( + execution_info: &TransactionExecutionInfo, +) -> Vec { + let mut messages = vec![]; + + fn get_messages_recursively(info: &CallInfo) -> Vec { + let mut messages = vec![]; + + messages.extend(info.execution.l2_to_l1_messages.iter().map(|m| MsgToL1 { + to_address: + FieldElement::from_byte_slice_be(m.message.to_address.0.as_bytes()).unwrap(), + from_address: (*info.call.caller_address.0.key()).into(), + payload: m.message.payload.0.iter().map(|p| (*p).into()).collect(), + })); + + info.inner_calls.iter().for_each(|call| { + messages.extend(get_messages_recursively(call)); + }); + + messages + } + + if let Some(ref info) = execution_info.validate_call_info { + messages.extend(get_messages_recursively(info)); + } + + if let Some(ref info) = execution_info.execute_call_info { + messages.extend(get_messages_recursively(info)); + } + + if let Some(ref info) = execution_info.fee_transfer_call_info { + messages.extend(get_messages_recursively(info)); + } + + messages +} diff --git a/crates/katana/executor/src/lib.rs b/crates/katana/executor/src/lib.rs new file mode 100644 index 0000000000..22486ef41d --- /dev/null +++ b/crates/katana/executor/src/lib.rs @@ -0,0 +1 @@ +pub mod blockifier;