diff --git a/Cargo.toml b/Cargo.toml index 6d438f0191..d179584b9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ katana-executor = { path = "crates/katana/executor", default-features = false } katana-primitives = { path = "crates/katana/primitives" } katana-provider = { path = "crates/katana/storage/provider" } katana-rpc = { path = "crates/katana/rpc/rpc" } -katana-rpc-api = { path = "crates/katana/rpc/rpc-api" } +katana-rpc-api = { path = "crates/katana/rpc/rpc-api", default-features = false } katana-rpc-types = { path = "crates/katana/rpc/rpc-types" } katana-rpc-types-builder = { path = "crates/katana/rpc/rpc-types-builder" } katana-runner = { path = "crates/katana/runner" } @@ -231,9 +231,7 @@ slot = { git = "https://github.com/cartridge-gg/slot", rev = "4c1165d" } alloy-contract = { version = "0.2", default-features = false } alloy-json-rpc = { version = "0.2", default-features = false } alloy-network = { version = "0.2", default-features = false } -alloy-provider = { version = "0.2", default-features = false, features = [ - "reqwest", -] } +alloy-provider = { version = "0.2", default-features = false, features = [ "reqwest" ] } alloy-rpc-types-eth = { version = "0.2", default-features = false } alloy-signer = { version = "0.2", default-features = false } alloy-transport = { version = "0.2", default-features = false } diff --git a/crates/katana/rpc/rpc-api/src/starknet.rs b/crates/katana/rpc/rpc-api/src/starknet.rs index 2f496462ce..ccbd909522 100644 --- a/crates/katana/rpc/rpc-api/src/starknet.rs +++ b/crates/katana/rpc/rpc-api/src/starknet.rs @@ -1,3 +1,5 @@ +//! Starknet JSON-RPC specifications: + use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; use katana_primitives::block::{BlockIdOrTag, BlockNumber}; @@ -19,12 +21,14 @@ use katana_rpc_types::{ ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlag, SimulationFlagForEstimateFee, SyncingStatus, }; -use starknet::core::types::{SimulatedTransaction, TransactionStatus}; +use starknet::core::types::{ + SimulatedTransaction, TransactionStatus, TransactionTrace, TransactionTraceWithHash, +}; /// The currently supported version of the Starknet JSON-RPC specification. pub const RPC_SPEC_VERSION: &str = "0.7.1"; -/// Starknet JSON-RPC APIs: +/// Read API. #[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] #[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] pub trait StarknetApi { @@ -172,7 +176,12 @@ pub trait StarknetApi { block_id: BlockIdOrTag, contract_address: FieldElement, ) -> RpcResult; +} +/// Write API. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] +#[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] +pub trait StarknetWriteApi { /// Submit a new transaction to be added to the chain. #[method(name = "addInvokeTransaction")] async fn add_invoke_transaction( @@ -193,13 +202,27 @@ pub trait StarknetApi { &self, deploy_account_transaction: BroadcastedDeployAccountTx, ) -> RpcResult; +} + +/// Trace API. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] +#[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] +pub trait StarknetTraceApi { + /// Returns the execution trace of the transaction designated by the input hash. + #[method(name = "traceTransaction")] + async fn trace(&self, transaction_hash: TxHash) -> RpcResult; /// Simulates a list of transactions on the provided block. #[method(name = "simulateTransactions")] - async fn simulate_transactions( + async fn simulate( &self, block_id: BlockIdOrTag, transactions: Vec, simulation_flags: Vec, ) -> RpcResult>; + + /// Returns the execution traces of all transactions included in the given block. + #[method(name = "traceBlockTransactions")] + async fn trace_block(&self, block_id: BlockIdOrTag) + -> RpcResult>; } diff --git a/crates/katana/rpc/rpc/src/lib.rs b/crates/katana/rpc/rpc/src/lib.rs index 2c51606f67..cc65fc0258 100644 --- a/crates/katana/rpc/rpc/src/lib.rs +++ b/crates/katana/rpc/rpc/src/lib.rs @@ -24,7 +24,7 @@ use katana_executor::ExecutorFactory; use katana_rpc_api::dev::DevApiServer; use katana_rpc_api::katana::KatanaApiServer; use katana_rpc_api::saya::SayaApiServer; -use katana_rpc_api::starknet::StarknetApiServer; +use katana_rpc_api::starknet::{StarknetApiServer, StarknetTraceApiServer, StarknetWriteApiServer}; use katana_rpc_api::torii::ToriiApiServer; use katana_rpc_api::ApiKind; use metrics::RpcServerMetrics; @@ -46,7 +46,11 @@ pub async fn spawn( for api in &config.apis { match api { ApiKind::Starknet => { - methods.merge(StarknetApi::new(sequencer.clone()).into_rpc())?; + // TODO: merge these into a single logic. + let server = StarknetApi::new(sequencer.clone()); + methods.merge(StarknetApiServer::into_rpc(server.clone()))?; + methods.merge(StarknetWriteApiServer::into_rpc(server.clone()))?; + methods.merge(StarknetTraceApiServer::into_rpc(server))?; } ApiKind::Katana => { methods.merge(KatanaApi::new(sequencer.clone()).into_rpc())?; diff --git a/crates/katana/rpc/rpc/src/starknet/mod.rs b/crates/katana/rpc/rpc/src/starknet/mod.rs new file mode 100644 index 0000000000..730a979fec --- /dev/null +++ b/crates/katana/rpc/rpc/src/starknet/mod.rs @@ -0,0 +1,99 @@ +//! Server implementation for the Starknet JSON-RPC API. + +mod read; +mod trace; +mod write; + +use std::sync::Arc; + +use katana_core::sequencer::KatanaSequencer; +use katana_executor::ExecutorFactory; +use katana_primitives::block::BlockIdOrTag; +use katana_primitives::transaction::ExecutableTxWithHash; +use katana_rpc_types::error::starknet::StarknetApiError; +use katana_rpc_types::FeeEstimate; +use katana_tasks::{BlockingTaskPool, TokioTaskSpawner}; + +#[allow(missing_debug_implementations)] +pub struct StarknetApi { + inner: Arc>, +} + +impl Clone for StarknetApi { + fn clone(&self) -> Self { + Self { inner: Arc::clone(&self.inner) } + } +} + +struct Inner { + sequencer: Arc>, + blocking_task_pool: BlockingTaskPool, +} + +impl StarknetApi { + pub fn new(sequencer: Arc>) -> Self { + let blocking_task_pool = + BlockingTaskPool::new().expect("failed to create blocking task pool"); + Self { inner: Arc::new(Inner { sequencer, blocking_task_pool }) } + } + + async fn on_cpu_blocking_task(&self, func: F) -> T + where + F: FnOnce(Self) -> T + Send + 'static, + T: Send + 'static, + { + let this = self.clone(); + self.inner.blocking_task_pool.spawn(move || func(this)).await.unwrap() + } + + async fn on_io_blocking_task(&self, func: F) -> T + where + F: FnOnce(Self) -> T + Send + 'static, + T: Send + 'static, + { + let this = self.clone(); + TokioTaskSpawner::new().unwrap().spawn_blocking(move || func(this)).await.unwrap() + } + + fn estimate_fee_with( + &self, + transactions: Vec, + block_id: BlockIdOrTag, + flags: katana_executor::SimulationFlag, + ) -> Result, StarknetApiError> { + let sequencer = &self.inner.sequencer; + // get the state and block env at the specified block for execution + let state = sequencer.state(&block_id).map_err(StarknetApiError::from)?; + let env = sequencer + .block_env_at(block_id) + .map_err(StarknetApiError::from)? + .ok_or(StarknetApiError::BlockNotFound)?; + + // create the executor + let executor = sequencer.backend.executor_factory.with_state_and_block_env(state, env); + let results = executor.estimate_fee(transactions, flags); + + let mut estimates = Vec::with_capacity(results.len()); + for (i, res) in results.into_iter().enumerate() { + match res { + Ok(fee) => estimates.push(FeeEstimate { + gas_price: fee.gas_price.into(), + gas_consumed: fee.gas_consumed.into(), + overall_fee: fee.overall_fee.into(), + unit: fee.unit, + data_gas_price: Default::default(), + data_gas_consumed: Default::default(), + }), + + Err(err) => { + return Err(StarknetApiError::TransactionExecutionError { + transaction_index: i, + execution_error: err.to_string(), + }); + } + } + } + + Ok(estimates) + } +} diff --git a/crates/katana/rpc/rpc/src/starknet.rs b/crates/katana/rpc/rpc/src/starknet/read.rs similarity index 62% rename from crates/katana/rpc/rpc/src/starknet.rs rename to crates/katana/rpc/rpc/src/starknet/read.rs index 19b5515c52..3d4bed493a 100644 --- a/crates/katana/rpc/rpc/src/starknet.rs +++ b/crates/katana/rpc/rpc/src/starknet/read.rs @@ -1,12 +1,8 @@ -use std::sync::Arc; - use jsonrpsee::core::{async_trait, Error, RpcResult}; use katana_core::backend::contract::StarknetContract; -use katana_core::sequencer::KatanaSequencer; -use katana_executor::{EntryPointCall, ExecutionResult, ExecutorFactory, ResultAndStates}; +use katana_executor::{EntryPointCall, ExecutionResult, ExecutorFactory}; use katana_primitives::block::{BlockHashOrNumber, BlockIdOrTag, FinalityStatus, PartialHeader}; use katana_primitives::conversion::rpc::legacy_inner_to_rpc_class; -use katana_primitives::receipt::Receipt; use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; use katana_primitives::version::CURRENT_STARKNET_VERSION; use katana_primitives::FieldElement; @@ -25,107 +21,14 @@ use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; use katana_rpc_types::message::MsgFromL1; use katana_rpc_types::receipt::{ReceiptBlock, TxReceiptWithBlockInfo}; use katana_rpc_types::state_update::StateUpdate; -use katana_rpc_types::trace::FunctionInvocation; -use katana_rpc_types::transaction::{ - BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, BroadcastedTx, - DeclareTxResult, DeployAccountTxResult, InvokeTxResult, Tx, -}; +use katana_rpc_types::transaction::{BroadcastedTx, Tx}; use katana_rpc_types::{ - ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlag, - SimulationFlagForEstimateFee, + ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlagForEstimateFee, }; use katana_rpc_types_builder::ReceiptBuilder; -use katana_tasks::{BlockingTaskPool, TokioTaskSpawner}; -use starknet::core::types::{ - BlockTag, ComputationResources, DataAvailabilityResources, DataResources, - DeclareTransactionTrace, DeployAccountTransactionTrace, ExecuteInvocation, ExecutionResources, - InvokeTransactionTrace, L1HandlerTransactionTrace, RevertedInvocation, SimulatedTransaction, - TransactionExecutionStatus, TransactionStatus, TransactionTrace, -}; - -#[allow(missing_debug_implementations)] -pub struct StarknetApi { - inner: Arc>, -} - -impl Clone for StarknetApi { - fn clone(&self) -> Self { - Self { inner: Arc::clone(&self.inner) } - } -} - -struct StarknetApiInner { - sequencer: Arc>, - blocking_task_pool: BlockingTaskPool, -} - -impl StarknetApi { - pub fn new(sequencer: Arc>) -> Self { - let blocking_task_pool = - BlockingTaskPool::new().expect("failed to create blocking task pool"); - Self { inner: Arc::new(StarknetApiInner { sequencer, blocking_task_pool }) } - } - - async fn on_cpu_blocking_task(&self, func: F) -> T - where - F: FnOnce(Self) -> T + Send + 'static, - T: Send + 'static, - { - let this = self.clone(); - self.inner.blocking_task_pool.spawn(move || func(this)).await.unwrap() - } - - async fn on_io_blocking_task(&self, func: F) -> T - where - F: FnOnce(Self) -> T + Send + 'static, - T: Send + 'static, - { - let this = self.clone(); - TokioTaskSpawner::new().unwrap().spawn_blocking(move || func(this)).await.unwrap() - } +use starknet::core::types::{BlockTag, TransactionExecutionStatus, TransactionStatus}; - fn estimate_fee_with( - &self, - transactions: Vec, - block_id: BlockIdOrTag, - flags: katana_executor::SimulationFlag, - ) -> Result, StarknetApiError> { - let sequencer = &self.inner.sequencer; - // get the state and block env at the specified block for execution - let state = sequencer.state(&block_id).map_err(StarknetApiError::from)?; - let env = sequencer - .block_env_at(block_id) - .map_err(StarknetApiError::from)? - .ok_or(StarknetApiError::BlockNotFound)?; - - // create the executor - let executor = sequencer.backend.executor_factory.with_state_and_block_env(state, env); - let results = executor.estimate_fee(transactions, flags); - - let mut estimates = Vec::with_capacity(results.len()); - for (i, res) in results.into_iter().enumerate() { - match res { - Ok(fee) => estimates.push(FeeEstimate { - gas_price: fee.gas_price.into(), - gas_consumed: fee.gas_consumed.into(), - overall_fee: fee.overall_fee.into(), - unit: fee.unit, - data_gas_price: Default::default(), - data_gas_consumed: Default::default(), - }), - - Err(err) => { - return Err(StarknetApiError::TransactionExecutionError { - transaction_index: i, - execution_error: err.to_string(), - }); - } - } - } - - Ok(estimates) - } -} +use super::StarknetApi; #[async_trait] impl StarknetApiServer for StarknetApi { @@ -594,30 +497,6 @@ impl StarknetApiServer for StarknetApi { .await } - async fn add_deploy_account_transaction( - &self, - deploy_account_transaction: BroadcastedDeployAccountTx, - ) -> RpcResult { - self.on_io_blocking_task(move |this| { - if deploy_account_transaction.is_query() { - return Err(StarknetApiError::UnsupportedTransactionVersion.into()); - } - - let chain_id = this.inner.sequencer.chain_id(); - - let tx = deploy_account_transaction.into_tx_with_chain_id(chain_id); - let contract_address = tx.contract_address(); - - let tx = ExecutableTxWithHash::new(ExecutableTx::DeployAccount(tx)); - let tx_hash = tx.hash; - - this.inner.sequencer.add_transaction_to_pool(tx); - - Ok((tx_hash, contract_address).into()) - }) - .await - } - async fn estimate_fee( &self, request: Vec, @@ -711,63 +590,6 @@ impl StarknetApiServer for StarknetApi { .await } - async fn add_declare_transaction( - &self, - declare_transaction: BroadcastedDeclareTx, - ) -> RpcResult { - self.on_io_blocking_task(move |this| { - if declare_transaction.is_query() { - return Err(StarknetApiError::UnsupportedTransactionVersion.into()); - } - - let chain_id = this.inner.sequencer.chain_id(); - - // // validate compiled class hash - // let is_valid = declare_transaction - // .validate_compiled_class_hash() - // .map_err(|_| StarknetApiError::InvalidContractClass)?; - - // if !is_valid { - // return Err(StarknetApiError::CompiledClassHashMismatch.into()); - // } - - let tx = declare_transaction - .try_into_tx_with_chain_id(chain_id) - .map_err(|_| StarknetApiError::InvalidContractClass)?; - - let class_hash = tx.class_hash(); - let tx = ExecutableTxWithHash::new(ExecutableTx::Declare(tx)); - let tx_hash = tx.hash; - - this.inner.sequencer.add_transaction_to_pool(tx); - - Ok((tx_hash, class_hash).into()) - }) - .await - } - - async fn add_invoke_transaction( - &self, - invoke_transaction: BroadcastedInvokeTx, - ) -> RpcResult { - self.on_io_blocking_task(move |this| { - if invoke_transaction.is_query() { - return Err(StarknetApiError::UnsupportedTransactionVersion.into()); - } - - let chain_id = this.inner.sequencer.chain_id(); - - let tx = invoke_transaction.into_tx_with_chain_id(chain_id); - let tx = ExecutableTxWithHash::new(ExecutableTx::Invoke(tx)); - let tx_hash = tx.hash; - - this.inner.sequencer.add_transaction_to_pool(tx); - - Ok(tx_hash.into()) - }) - .await - } - async fn transaction_status(&self, transaction_hash: TxHash) -> RpcResult { self.on_io_blocking_task(move |this| { let provider = this.inner.sequencer.backend.blockchain.provider(); @@ -820,186 +642,4 @@ impl StarknetApiServer for StarknetApi { }) .await } - - async fn simulate_transactions( - &self, - block_id: BlockIdOrTag, - transactions: Vec, - simulation_flags: Vec, - ) -> RpcResult> { - self.on_cpu_blocking_task(move |this| { - 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::::Ok(tx) - }) - .collect::, _>>()?; - - // If the node is run with transaction validation disabled, then we should not validate - // even if the `SKIP_VALIDATE` flag is not set. - let should_validate = !(simulation_flags.contains(&SimulationFlag::SkipValidate) - || this.inner.sequencer.backend.config.disable_validate); - // If the node is run with fee charge disabled, then we should disable charing fees even - // if the `SKIP_FEE_CHARGE` flag is not set. - let should_skip_fee = !(simulation_flags.contains(&SimulationFlag::SkipFeeCharge) - || this.inner.sequencer.backend.config.disable_fee); - - let flags = katana_executor::SimulationFlag { - skip_validate: !should_validate, - skip_fee_transfer: !should_skip_fee, - ..Default::default() - }; - - let sequencer = &this.inner.sequencer; - // get the state and block env at the specified block for execution - let state = sequencer.state(&block_id).map_err(StarknetApiError::from)?; - let env = sequencer - .block_env_at(block_id) - .map_err(StarknetApiError::from)? - .ok_or(StarknetApiError::BlockNotFound)?; - - // create the executor - let executor = sequencer.backend.executor_factory.with_state_and_block_env(state, env); - let results = executor.simulate(executables, flags); - - let mut simulated = Vec::with_capacity(results.len()); - for (i, ResultAndStates { result, .. }) in results.into_iter().enumerate() { - match result { - ExecutionResult::Success { trace, receipt } => { - let fee_transfer_invocation = - trace.fee_transfer_call_info.map(|f| FunctionInvocation::from(f).0); - let validate_invocation = - trace.validate_call_info.map(|f| FunctionInvocation::from(f).0); - let execute_invocation = - trace.execute_call_info.map(|f| FunctionInvocation::from(f).0); - let revert_reason = trace.revert_error; - // TODO: compute the state diff - let state_diff = None; - - let execution_resources = ExecutionResources { - computation_resources: ComputationResources { - steps: 0, - memory_holes: None, - segment_arena_builtin: None, - ecdsa_builtin_applications: None, - ec_op_builtin_applications: None, - keccak_builtin_applications: None, - bitwise_builtin_applications: None, - pedersen_builtin_applications: None, - poseidon_builtin_applications: None, - range_check_builtin_applications: None, - }, - data_resources: DataResources { - data_availability: DataAvailabilityResources { - l1_gas: 0, - l1_data_gas: 0, - }, - }, - }; - - let transaction_trace = match receipt { - Receipt::Invoke(_) => { - TransactionTrace::Invoke(InvokeTransactionTrace { - fee_transfer_invocation, - validate_invocation, - state_diff, - execute_invocation: if let Some(revert_reason) = revert_reason { - ExecuteInvocation::Reverted(RevertedInvocation { - revert_reason, - }) - } else { - ExecuteInvocation::Success( - execute_invocation - .expect("should exist if not reverted"), - ) - }, - execution_resources: execution_resources.clone(), - }) - } - - Receipt::Declare(_) => { - TransactionTrace::Declare(DeclareTransactionTrace { - fee_transfer_invocation, - validate_invocation, - state_diff, - execution_resources: execution_resources.clone(), - }) - } - - Receipt::DeployAccount(_) => { - TransactionTrace::DeployAccount(DeployAccountTransactionTrace { - fee_transfer_invocation, - validate_invocation, - state_diff, - constructor_invocation: execute_invocation - .expect("should exist bcs tx succeed"), - execution_resources: execution_resources.clone(), - }) - } - - Receipt::L1Handler(_) => { - TransactionTrace::L1Handler(L1HandlerTransactionTrace { - state_diff, - function_invocation: execute_invocation - .expect("should exist bcs tx succeed"), - execution_resources, - }) - } - }; - - let fee = receipt.fee(); - simulated.push(SimulatedTransaction { - transaction_trace, - fee_estimation: FeeEstimate { - unit: fee.unit, - gas_price: fee.gas_price.into(), - overall_fee: fee.overall_fee.into(), - gas_consumed: fee.gas_consumed.into(), - data_gas_price: Default::default(), - data_gas_consumed: Default::default(), - }, - }) - } - - ExecutionResult::Failed { error } => { - return Err(Error::from(StarknetApiError::TransactionExecutionError { - transaction_index: i, - execution_error: error.to_string(), - })); - } - } - } - - Ok(simulated) - }) - .await - } } diff --git a/crates/katana/rpc/rpc/src/starknet/trace.rs b/crates/katana/rpc/rpc/src/starknet/trace.rs new file mode 100644 index 0000000000..78b14fd891 --- /dev/null +++ b/crates/katana/rpc/rpc/src/starknet/trace.rs @@ -0,0 +1,221 @@ +use jsonrpsee::core::{async_trait, Error, RpcResult}; +use jsonrpsee::types::error::{CallError, METHOD_NOT_FOUND_CODE}; +use jsonrpsee::types::ErrorObject; +use katana_executor::{ExecutionResult, ExecutorFactory, ResultAndStates}; +use katana_primitives::block::BlockIdOrTag; +use katana_primitives::receipt::Receipt; +use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; +use katana_rpc_api::starknet::StarknetTraceApiServer; +use katana_rpc_types::error::starknet::StarknetApiError; +use katana_rpc_types::trace::FunctionInvocation; +use katana_rpc_types::transaction::BroadcastedTx; +use katana_rpc_types::{FeeEstimate, SimulationFlag}; +use starknet::core::types::{ + ComputationResources, DataAvailabilityResources, DataResources, DeclareTransactionTrace, + DeployAccountTransactionTrace, ExecuteInvocation, ExecutionResources, InvokeTransactionTrace, + L1HandlerTransactionTrace, RevertedInvocation, SimulatedTransaction, TransactionTrace, + TransactionTraceWithHash, +}; + +use super::StarknetApi; + +#[async_trait] +impl StarknetTraceApiServer for StarknetApi { + async fn trace(&self, _: TxHash) -> RpcResult { + Err(Error::Call(CallError::Custom(ErrorObject::owned( + METHOD_NOT_FOUND_CODE, + "Unsupported method - starknet_traceTransaction".to_string(), + None::<()>, + )))) + } + + async fn simulate( + &self, + block_id: BlockIdOrTag, + transactions: Vec, + simulation_flags: Vec, + ) -> RpcResult> { + self.on_cpu_blocking_task(move |this| { + 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::::Ok(tx) + }) + .collect::, _>>()?; + + // If the node is run with transaction validation disabled, then we should not validate + // even if the `SKIP_VALIDATE` flag is not set. + let should_validate = !(simulation_flags.contains(&SimulationFlag::SkipValidate) + || this.inner.sequencer.backend.config.disable_validate); + // If the node is run with fee charge disabled, then we should disable charing fees even + // if the `SKIP_FEE_CHARGE` flag is not set. + let should_skip_fee = !(simulation_flags.contains(&SimulationFlag::SkipFeeCharge) + || this.inner.sequencer.backend.config.disable_fee); + + let flags = katana_executor::SimulationFlag { + skip_validate: !should_validate, + skip_fee_transfer: !should_skip_fee, + ..Default::default() + }; + + let sequencer = &this.inner.sequencer; + // get the state and block env at the specified block for execution + let state = sequencer.state(&block_id).map_err(StarknetApiError::from)?; + let env = sequencer + .block_env_at(block_id) + .map_err(StarknetApiError::from)? + .ok_or(StarknetApiError::BlockNotFound)?; + + // create the executor + let executor = sequencer.backend.executor_factory.with_state_and_block_env(state, env); + let results = executor.simulate(executables, flags); + + let mut simulated = Vec::with_capacity(results.len()); + for (i, ResultAndStates { result, .. }) in results.into_iter().enumerate() { + match result { + ExecutionResult::Success { trace, receipt } => { + let fee_transfer_invocation = + trace.fee_transfer_call_info.map(|f| FunctionInvocation::from(f).0); + let validate_invocation = + trace.validate_call_info.map(|f| FunctionInvocation::from(f).0); + let execute_invocation = + trace.execute_call_info.map(|f| FunctionInvocation::from(f).0); + let revert_reason = trace.revert_error; + // TODO: compute the state diff + let state_diff = None; + + let execution_resources = ExecutionResources { + computation_resources: ComputationResources { + steps: 0, + memory_holes: None, + segment_arena_builtin: None, + ecdsa_builtin_applications: None, + ec_op_builtin_applications: None, + keccak_builtin_applications: None, + bitwise_builtin_applications: None, + pedersen_builtin_applications: None, + poseidon_builtin_applications: None, + range_check_builtin_applications: None, + }, + data_resources: DataResources { + data_availability: DataAvailabilityResources { + l1_gas: 0, + l1_data_gas: 0, + }, + }, + }; + + let transaction_trace = match receipt { + Receipt::Invoke(_) => { + TransactionTrace::Invoke(InvokeTransactionTrace { + fee_transfer_invocation, + validate_invocation, + state_diff, + execute_invocation: if let Some(revert_reason) = revert_reason { + ExecuteInvocation::Reverted(RevertedInvocation { + revert_reason, + }) + } else { + ExecuteInvocation::Success( + execute_invocation + .expect("should exist if not reverted"), + ) + }, + execution_resources: execution_resources.clone(), + }) + } + + Receipt::Declare(_) => { + TransactionTrace::Declare(DeclareTransactionTrace { + fee_transfer_invocation, + validate_invocation, + state_diff, + execution_resources: execution_resources.clone(), + }) + } + + Receipt::DeployAccount(_) => { + TransactionTrace::DeployAccount(DeployAccountTransactionTrace { + fee_transfer_invocation, + validate_invocation, + state_diff, + constructor_invocation: execute_invocation + .expect("should exist bcs tx succeed"), + execution_resources: execution_resources.clone(), + }) + } + + Receipt::L1Handler(_) => { + TransactionTrace::L1Handler(L1HandlerTransactionTrace { + state_diff, + function_invocation: execute_invocation + .expect("should exist bcs tx succeed"), + execution_resources, + }) + } + }; + + let fee = receipt.fee(); + simulated.push(SimulatedTransaction { + transaction_trace, + fee_estimation: FeeEstimate { + unit: fee.unit, + gas_price: fee.gas_price.into(), + overall_fee: fee.overall_fee.into(), + gas_consumed: fee.gas_consumed.into(), + data_gas_price: Default::default(), + data_gas_consumed: Default::default(), + }, + }) + } + + ExecutionResult::Failed { error } => { + return Err(Error::from(StarknetApiError::TransactionExecutionError { + transaction_index: i, + execution_error: error.to_string(), + })); + } + } + } + + Ok(simulated) + }) + .await + } + + async fn trace_block(&self, _: BlockIdOrTag) -> RpcResult> { + Err(Error::Call(CallError::Custom(ErrorObject::owned( + METHOD_NOT_FOUND_CODE, + "Unsupported method - starknet_traceBlockTransactions".to_string(), + None::<()>, + )))) + } +} diff --git a/crates/katana/rpc/rpc/src/starknet/write.rs b/crates/katana/rpc/rpc/src/starknet/write.rs new file mode 100644 index 0000000000..9d147fb292 --- /dev/null +++ b/crates/katana/rpc/rpc/src/starknet/write.rs @@ -0,0 +1,86 @@ +use jsonrpsee::core::{async_trait, RpcResult}; +use katana_executor::ExecutorFactory; +use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash}; +use katana_rpc_api::starknet::StarknetWriteApiServer; +use katana_rpc_types::error::starknet::StarknetApiError; +use katana_rpc_types::transaction::{ + BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, DeclareTxResult, + DeployAccountTxResult, InvokeTxResult, +}; + +use super::StarknetApi; + +#[async_trait] +impl StarknetWriteApiServer for StarknetApi { + async fn add_invoke_transaction( + &self, + invoke_transaction: BroadcastedInvokeTx, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + if invoke_transaction.is_query() { + return Err(StarknetApiError::UnsupportedTransactionVersion.into()); + } + + let chain_id = this.inner.sequencer.chain_id(); + + let tx = invoke_transaction.into_tx_with_chain_id(chain_id); + let tx = ExecutableTxWithHash::new(ExecutableTx::Invoke(tx)); + let tx_hash = tx.hash; + + this.inner.sequencer.add_transaction_to_pool(tx); + + Ok(tx_hash.into()) + }) + .await + } + + async fn add_declare_transaction( + &self, + declare_transaction: BroadcastedDeclareTx, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + if declare_transaction.is_query() { + return Err(StarknetApiError::UnsupportedTransactionVersion.into()); + } + + let chain_id = this.inner.sequencer.chain_id(); + + let tx = declare_transaction + .try_into_tx_with_chain_id(chain_id) + .map_err(|_| StarknetApiError::InvalidContractClass)?; + + let class_hash = tx.class_hash(); + let tx = ExecutableTxWithHash::new(ExecutableTx::Declare(tx)); + let tx_hash = tx.hash; + + this.inner.sequencer.add_transaction_to_pool(tx); + + Ok((tx_hash, class_hash).into()) + }) + .await + } + + async fn add_deploy_account_transaction( + &self, + deploy_account_transaction: BroadcastedDeployAccountTx, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + if deploy_account_transaction.is_query() { + return Err(StarknetApiError::UnsupportedTransactionVersion.into()); + } + + let chain_id = this.inner.sequencer.chain_id(); + + let tx = deploy_account_transaction.into_tx_with_chain_id(chain_id); + let contract_address = tx.contract_address(); + + let tx = ExecutableTxWithHash::new(ExecutableTx::DeployAccount(tx)); + let tx_hash = tx.hash; + + this.inner.sequencer.add_transaction_to_pool(tx); + + Ok((tx_hash, contract_address).into()) + }) + .await + } +} diff --git a/examples/rpc/starknet/starknet_trace.hurl b/examples/rpc/starknet/starknet_trace.hurl new file mode 100644 index 0000000000..56f5f9bdeb --- /dev/null +++ b/examples/rpc/starknet/starknet_trace.hurl @@ -0,0 +1,33 @@ +# starknet_traceTransaction +POST http://0.0.0.0:5050 +Content-Type: application/json +{ + "jsonrpc": "2.0", + "method": "starknet_traceTransaction", + "params": ["0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"], + "id": 2 +} + +HTTP 200 +[Asserts] +jsonpath "$.error" exists +jsonpath "$.error.code" == -32601 +jsonpath "$.error.message" == "Unsupported method - starknet_traceTransaction" +jsonpath "$.result" not exists + +# starknet_traceBlockTransactions +POST http://0.0.0.0:5050 +Content-Type: application/json +{ + "jsonrpc": "2.0", + "method": "starknet_traceBlockTransactions", + "params": ["latest"], + "id": 1 +} + +HTTP 200 +[Asserts] +jsonpath "$.error" exists +jsonpath "$.error.code" == -32601 +jsonpath "$.error.message" == "Unsupported method - starknet_traceBlockTransactions" +jsonpath "$.result" not exists