diff --git a/Cargo.lock b/Cargo.lock index 7d31278357..a37c62734e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10604,19 +10604,52 @@ version = "0.6.0-alpha.5" dependencies = [ "anyhow", "async-trait", + "cairo-vm 0.9.2", "celestia-rpc", "celestia-types", "convert_case 0.6.0", - "ethers", "flate2", "futures", "katana-db", "katana-executor", "katana-primitives", "katana-provider", + "katana-rpc-types", "lazy_static", "parking_lot 0.12.1", "rand", + "saya-provider", + "serde", + "serde_json", + "serde_with", + "starknet 0.9.0", + "starknet-types-core", + "starknet_api", + "thiserror", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "saya-provider" +version = "0.6.0-alpha.5" +dependencies = [ + "anyhow", + "async-trait", + "auto_impl", + "convert_case 0.6.0", + "ethers", + "flate2", + "futures", + "jsonrpsee 0.16.3", + "katana-db", + "katana-executor", + "katana-primitives", + "katana-provider", + "katana-rpc-api", + "katana-rpc-types", + "lazy_static", "serde", "serde_json", "serde_with", diff --git a/Cargo.toml b/Cargo.toml index 6312ae36c2..408d81fad4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "crates/katana/tasks", "crates/metrics", "crates/saya/core", + "crates/saya/provider", "crates/sozo/signers", "crates/torii/client", "crates/torii/server", @@ -91,6 +92,7 @@ torii-server = { path = "crates/torii/server" } # saya saya-core = { path = "crates/saya/core" } +saya-provider = { path = "crates/saya/provider" } # sozo sozo-signers = { path = "crates/sozo/signers" } diff --git a/bin/katana/src/args.rs b/bin/katana/src/args.rs index 09fe81b18e..c583434afe 100644 --- a/bin/katana/src/args.rs +++ b/bin/katana/src/args.rs @@ -218,7 +218,7 @@ impl KatanaArgs { } pub fn server_config(&self) -> ServerConfig { - let mut apis = vec![ApiKind::Starknet, ApiKind::Katana, ApiKind::Torii]; + let mut apis = vec![ApiKind::Starknet, ApiKind::Katana, ApiKind::Torii, ApiKind::Saya]; // only enable `katana` API in dev mode if self.dev { apis.push(ApiKind::Dev); diff --git a/bin/saya/src/args/mod.rs b/bin/saya/src/args/mod.rs index 149e80c093..48e73a9d97 100644 --- a/bin/saya/src/args/mod.rs +++ b/bin/saya/src/args/mod.rs @@ -49,7 +49,7 @@ pub struct SayaArgs { impl SayaArgs { pub fn init_logging(&self) -> Result<(), Box> { - const DEFAULT_LOG_FILTER: &str = "info,saya_core=trace"; + const DEFAULT_LOG_FILTER: &str = "info,saya_core=trace,blockchain=trace,provider=trace"; let builder = fmt::Subscriber::builder().with_env_filter( EnvFilter::try_from_default_env().or(EnvFilter::try_new(DEFAULT_LOG_FILTER))?, diff --git a/bin/saya/src/main.rs b/bin/saya/src/main.rs index 07a2af69d7..9ed0796eb9 100644 --- a/bin/saya/src/main.rs +++ b/bin/saya/src/main.rs @@ -16,7 +16,7 @@ async fn main() -> Result<(), Box> { let config = args.try_into()?; print_intro(&config); - let saya = Saya::new(config).await?; + let mut saya = Saya::new(config).await?; saya.start().await?; // Wait until Ctrl + C is pressed, then shutdown diff --git a/crates/dojo-test-utils/src/sequencer.rs b/crates/dojo-test-utils/src/sequencer.rs index 8131635541..7017c6cc3c 100644 --- a/crates/dojo-test-utils/src/sequencer.rs +++ b/crates/dojo-test-utils/src/sequencer.rs @@ -68,7 +68,13 @@ impl TestSequencer { port: 0, host: "127.0.0.1".into(), max_connections: 100, - apis: vec![ApiKind::Starknet, ApiKind::Katana, ApiKind::Torii, ApiKind::Dev], + apis: vec![ + ApiKind::Starknet, + ApiKind::Katana, + ApiKind::Dev, + ApiKind::Saya, + ApiKind::Torii, + ], }, ) .await diff --git a/crates/katana/core/src/backend/mod.rs b/crates/katana/core/src/backend/mod.rs index 96a37d0202..979c0903d2 100644 --- a/crates/katana/core/src/backend/mod.rs +++ b/crates/katana/core/src/backend/mod.rs @@ -6,9 +6,7 @@ use katana_primitives::block::{ }; use katana_primitives::chain::ChainId; use katana_primitives::env::BlockEnv; -use katana_primitives::receipt::Receipt; use katana_primitives::state::StateUpdatesWithDeclaredClasses; -use katana_primitives::transaction::TxWithHash; use katana_primitives::version::CURRENT_STARKNET_VERSION; use katana_primitives::FieldElement; use katana_provider::providers::fork::ForkedProvider; @@ -28,7 +26,7 @@ pub mod storage; use self::config::StarknetConfig; use self::storage::Blockchain; use crate::env::BlockContextGenerator; -use crate::service::block_producer::{BlockProductionError, MinedBlockOutcome}; +use crate::service::block_producer::{BlockProductionError, MinedBlockOutcome, TxWithOutcome}; use crate::utils::get_current_timestamp; pub struct Backend { @@ -120,10 +118,18 @@ impl Backend { pub fn do_mine_block( &self, block_env: &BlockEnv, - tx_receipt_pairs: Vec<(TxWithHash, Receipt)>, + txs_outcomes: Vec, state_updates: StateUpdatesWithDeclaredClasses, ) -> Result { - let (txs, receipts): (Vec, Vec) = tx_receipt_pairs.into_iter().unzip(); + let mut txs = vec![]; + let mut receipts = vec![]; + let mut execs = vec![]; + + for t in txs_outcomes { + txs.push(t.tx); + receipts.push(t.receipt); + execs.push(t.exec_info); + } let prev_hash = BlockHashProvider::latest_hash(self.blockchain.provider())?; let block_number = block_env.number; @@ -150,6 +156,7 @@ impl Backend { block, state_updates, receipts, + execs, )?; info!(target: "backend", "⛏️ Block {block_number} mined with {tx_count} transactions"); diff --git a/crates/katana/core/src/backend/storage.rs b/crates/katana/core/src/backend/storage.rs index 1324791ed4..06cbb71358 100644 --- a/crates/katana/core/src/backend/storage.rs +++ b/crates/katana/core/src/backend/storage.rs @@ -12,7 +12,8 @@ use katana_provider::traits::env::BlockEnvProvider; use katana_provider::traits::state::{StateFactoryProvider, StateRootProvider, StateWriter}; use katana_provider::traits::state_update::StateUpdateProvider; use katana_provider::traits::transaction::{ - ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionsProviderExt, + ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionTraceProvider, + TransactionsProviderExt, }; use katana_provider::BlockchainProvider; @@ -21,6 +22,7 @@ pub trait Database: + BlockWriter + TransactionProvider + TransactionStatusProvider + + TransactionTraceProvider + TransactionsProviderExt + ReceiptProvider + StateUpdateProvider @@ -40,6 +42,7 @@ impl Database for T where + BlockWriter + TransactionProvider + TransactionStatusProvider + + TransactionTraceProvider + TransactionsProviderExt + ReceiptProvider + StateUpdateProvider @@ -119,7 +122,13 @@ impl Blockchain { block: SealedBlockWithStatus, states: StateUpdatesWithDeclaredClasses, ) -> Result { - BlockWriter::insert_block_with_states_and_receipts(&provider, block, states, vec![])?; + BlockWriter::insert_block_with_states_and_receipts( + &provider, + block, + states, + vec![], + vec![], + )?; Ok(Self::new(provider)) } } @@ -245,6 +254,7 @@ mod tests { dummy_block.clone(), StateUpdatesWithDeclaredClasses::default(), vec![Receipt::Invoke(InvokeTxReceipt::default())], + vec![], ) .unwrap(); diff --git a/crates/katana/core/src/service/block_producer.rs b/crates/katana/core/src/service/block_producer.rs index f04644f774..07b3e670e0 100644 --- a/crates/katana/core/src/service/block_producer.rs +++ b/crates/katana/core/src/service/block_producer.rs @@ -11,6 +11,7 @@ use futures::FutureExt; use katana_executor::{BlockExecutor, ExecutionOutput, ExecutorFactory}; use katana_primitives::block::{BlockHashOrNumber, ExecutableBlock, PartialHeader}; use katana_primitives::receipt::Receipt; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{ExecutableTxWithHash, TxWithHash}; use katana_primitives::version::CURRENT_STARKNET_VERSION; use katana_provider::error::ProviderError; @@ -43,17 +44,23 @@ pub struct MinedBlockOutcome { pub block_number: u64, } +#[derive(Debug, Clone)] +pub struct TxWithOutcome { + pub tx: TxWithHash, + pub receipt: Receipt, + pub exec_info: TxExecInfo, +} + type ServiceFuture = Pin> + Send + Sync>>; type BlockProductionResult = Result; type BlockProductionFuture = ServiceFuture; -type TxExecutionResult = Result; +type TxExecutionResult = Result, BlockProductionError>; type TxExecutionFuture = ServiceFuture; type BlockProductionWithTxnsFuture = - ServiceFuture>; -pub type TxWithHashAndReceiptPairs = Vec<(TxWithHash, Receipt)>; + ServiceFuture), BlockProductionError>>; /// The type which responsible for block production. #[must_use = "BlockProducer does nothing unless polled"] @@ -164,7 +171,7 @@ pub struct IntervalBlockProducer { blocking_task_spawner: BlockingTaskPool, ongoing_execution: Option, /// Listeners notified when a new executed tx is added. - tx_execution_listeners: RwLock>>, + tx_execution_listeners: RwLock>>>, } impl IntervalBlockProducer { @@ -259,7 +266,9 @@ impl IntervalBlockProducer { let transactions = transactions .into_iter() - .filter_map(|(tx, rct)| rct.map(|rct| (tx, rct))) + .filter_map(|(tx, rct, exec)| { + rct.map(|rct| TxWithOutcome { tx, receipt: rct, exec_info: exec }) + }) .collect::>(); let outcome = backend.do_mine_block(&block_env, transactions, states)?; @@ -272,19 +281,20 @@ impl IntervalBlockProducer { fn execute_transactions( executor: PendingExecutor, transactions: Vec, - ) -> Result { - let tx_receipt_pair = transactions + ) -> Result, BlockProductionError> { + let tx_outcome = transactions .into_iter() .map(|tx| { let tx_ = TxWithHash::from(&tx); let output = executor.write().execute(tx)?; - let receipt = output.receipt(tx_.as_ref()); - Ok((tx_, receipt)) + let exec_info = output.execution_info(); + + Ok(TxWithOutcome { tx: tx_, receipt, exec_info }) }) - .collect::>()?; + .collect::, BlockProductionError>>()?; - Ok(tx_receipt_pair) + Ok(tx_outcome) } fn create_new_executor_for_next_block(&self) -> Result { @@ -301,7 +311,7 @@ impl IntervalBlockProducer { Ok(PendingExecutor::new(executor)) } - pub fn add_listener(&self) -> Receiver { + pub fn add_listener(&self) -> Receiver> { const TX_LISTENER_BUFFER_SIZE: usize = 2048; let (tx, rx) = channel(TX_LISTENER_BUFFER_SIZE); self.tx_execution_listeners.write().push(tx); @@ -309,7 +319,7 @@ impl IntervalBlockProducer { } /// notifies all listeners about the transaction - fn notify_listener(&self, txs: TxWithHashAndReceiptPairs) { + fn notify_listener(&self, txs: Vec) { let mut listener = self.tx_execution_listeners.write(); // this is basically a retain but with mut reference for n in (0..listener.len()).rev() { @@ -436,7 +446,7 @@ pub struct InstantBlockProducer { blocking_task_pool: BlockingTaskPool, /// Listeners notified when a new executed tx is added. - tx_execution_listeners: RwLock>>, + tx_execution_listeners: RwLock>>>, } impl InstantBlockProducer { @@ -462,7 +472,7 @@ impl InstantBlockProducer { fn do_mine( backend: Arc>, transactions: Vec, - ) -> Result<(MinedBlockOutcome, TxWithHashAndReceiptPairs), BlockProductionError> { + ) -> Result<(MinedBlockOutcome, Vec), BlockProductionError> { trace!(target: "miner", "creating new block"); let provider = backend.blockchain.provider(); @@ -491,19 +501,21 @@ impl InstantBlockProducer { executor.execute_block(block)?; let ExecutionOutput { states, transactions } = executor.take_execution_output().unwrap(); - let tx_receipt_pairs = transactions + let txs_outcomes = transactions .into_iter() - .filter_map(|(tx, rct)| rct.map(|rct| (tx, rct))) + .filter_map(|(tx, rct, exec)| { + rct.map(|rct| TxWithOutcome { tx, receipt: rct, exec_info: exec }) + }) .collect::>(); - let outcome = backend.do_mine_block(&block_env, tx_receipt_pairs.clone(), states)?; + let outcome = backend.do_mine_block(&block_env, txs_outcomes.clone(), states)?; trace!(target: "miner", "created new block: {}", outcome.block_number); - Ok((outcome, tx_receipt_pairs)) + Ok((outcome, txs_outcomes)) } - pub fn add_listener(&self) -> Receiver { + pub fn add_listener(&self) -> Receiver> { const TX_LISTENER_BUFFER_SIZE: usize = 2048; let (tx, rx) = channel(TX_LISTENER_BUFFER_SIZE); self.tx_execution_listeners.write().push(tx); @@ -511,7 +523,7 @@ impl InstantBlockProducer { } /// notifies all listeners about the transaction - fn notify_listener(&self, txs: TxWithHashAndReceiptPairs) { + fn notify_listener(&self, txs: Vec) { let mut listener = self.tx_execution_listeners.write(); // this is basically a retain but with mut reference for n in (0..listener.len()).rev() { diff --git a/crates/katana/executor/src/abstraction.rs b/crates/katana/executor/src/abstraction.rs index eecabf01a8..42c3060a28 100644 --- a/crates/katana/executor/src/abstraction.rs +++ b/crates/katana/executor/src/abstraction.rs @@ -4,6 +4,7 @@ use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageVal use katana_primitives::env::{BlockEnv, CfgEnv}; use katana_primitives::receipt::Receipt; use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{ExecutableTxWithHash, Tx, TxWithHash}; use katana_primitives::FieldElement; use katana_provider::traits::contract::ContractClassProvider; @@ -107,7 +108,8 @@ pub struct ExecutionOutput { /// The state updates produced by the executions. pub states: StateUpdatesWithDeclaredClasses, /// The transactions that have been executed. - pub transactions: Vec<(TxWithHash, Option)>, + /// TODO: do we want a struct instead? + pub transactions: Vec<(TxWithHash, Option, TxExecInfo)>, } #[derive(Debug)] @@ -152,7 +154,7 @@ pub trait BlockExecutor<'a>: TransactionExecutor + Send + Sync { fn state(&self) -> Box; /// Returns the transactions that have been executed. - fn transactions(&self) -> &[(TxWithHash, Option)]; + fn transactions(&self) -> &[(TxWithHash, Option, TxExecInfo)]; /// Returns the current block environment of the executor. fn block_env(&self) -> BlockEnv; @@ -192,6 +194,9 @@ pub trait TransactionExecutionOutput { /// The error message if the transaction execution reverted, otherwise the value is `None`. fn revert_error(&self) -> Option<&str>; + + /// Retrieves the execution info of the transaction. + fn execution_info(&self) -> TxExecInfo; } /// A wrapper around a boxed [StateProvider] for implementing the executor's own state reader diff --git a/crates/katana/executor/src/implementation/blockifier/mod.rs b/crates/katana/executor/src/implementation/blockifier/mod.rs index addba7da7e..b85cc03715 100644 --- a/crates/katana/executor/src/implementation/blockifier/mod.rs +++ b/crates/katana/executor/src/implementation/blockifier/mod.rs @@ -7,6 +7,7 @@ use blockifier::state::cached_state::{self, MutRefState}; use katana_primitives::block::{ExecutableBlock, GasPrices, PartialHeader}; use katana_primitives::env::{BlockEnv, CfgEnv}; use katana_primitives::receipt::Receipt; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxWithHash}; use katana_primitives::FieldElement; use katana_provider::traits::state::StateProvider; @@ -61,7 +62,7 @@ impl abstraction::ExecutorFactory for BlockifierFactory { pub struct StarknetVMProcessor<'a> { block_context: BlockContext, state: CachedState>, - transactions: Vec<(TxWithHash, Option)>, + transactions: Vec<(TxWithHash, Option, TxExecInfo)>, simulation_flags: SimulationFlag, } @@ -113,7 +114,8 @@ impl<'a> abstraction::TransactionExecutor for StarknetVMProcessor<'a> { let res = utils::transact(tx, &mut state.0.write().inner, block_context, flags)?; let receipt = res.receipt(tx_.as_ref()); - self.transactions.push((tx_, Some(receipt))); + let exec_info = res.execution_info(); + self.transactions.push((tx_, Some(receipt), exec_info)); if let Some((class_hash, compiled_class, sierra_class)) = class_declaration_artifacts { state.0.write().declared_classes.insert(class_hash, (compiled_class, sierra_class)); @@ -172,7 +174,7 @@ impl<'a> abstraction::BlockExecutor<'a> for StarknetVMProcessor<'a> { Box::new(self.state.clone()) } - fn transactions(&self) -> &[(TxWithHash, Option)] { + fn transactions(&self) -> &[(TxWithHash, Option, TxExecInfo)] { &self.transactions } diff --git a/crates/katana/executor/src/implementation/blockifier/output.rs b/crates/katana/executor/src/implementation/blockifier/output.rs index 9f2fe16a6a..7c158fc714 100644 --- a/crates/katana/executor/src/implementation/blockifier/output.rs +++ b/crates/katana/executor/src/implementation/blockifier/output.rs @@ -7,6 +7,7 @@ use katana_primitives::receipt::{ DeclareTxReceipt, DeployAccountTxReceipt, Event, InvokeTxReceipt, L1HandlerTxReceipt, MessageToL1, Receipt, TxExecutionResources, }; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::Tx; use katana_primitives::FieldElement; @@ -75,6 +76,10 @@ impl TransactionExecutionOutput for TransactionExecutionInfo { fn revert_error(&self) -> Option<&str> { self.inner.revert_error.as_deref() } + + fn execution_info(&self) -> TxExecInfo { + utils::to_exec_info(self.inner.clone()) + } } fn events_from_exec_info(info: &TransactionExecutionInfo) -> Vec { diff --git a/crates/katana/executor/src/implementation/blockifier/utils.rs b/crates/katana/executor/src/implementation/blockifier/utils.rs index 19c702e55d..04f90561a3 100644 --- a/crates/katana/executor/src/implementation/blockifier/utils.rs +++ b/crates/katana/executor/src/implementation/blockifier/utils.rs @@ -6,7 +6,7 @@ use blockifier::execution::call_info::CallInfo; use blockifier::execution::common_hints::ExecutionMode; use blockifier::execution::contract_class::{ContractClass, ContractClassV0, ContractClassV1}; use blockifier::execution::entry_point::{ - CallEntryPoint, EntryPointExecutionContext, ExecutionResources, + CallEntryPoint, CallType, EntryPointExecutionContext, ExecutionResources, }; use blockifier::execution::errors::EntryPointExecutionError; use blockifier::fee::fee_utils::{self, calculate_tx_l1_gas_usages}; @@ -28,6 +28,7 @@ use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; use katana_primitives::transaction::{ DeclareTx, DeployAccountTx, ExecutableTx, ExecutableTxWithHash, InvokeTx, }; +use katana_primitives::FieldElement; use katana_provider::traits::contract::ContractClassProvider; use starknet::core::utils::parse_cairo_short_string; use starknet_api::block::{BlockNumber, BlockTimestamp}; @@ -35,6 +36,7 @@ use starknet_api::core::{ self, ChainId, ClassHash, CompiledClassHash, ContractAddress, Nonce, PatriciaKey, }; use starknet_api::data_availability::DataAvailabilityMode; +use starknet_api::deprecated_contract_class::EntryPointType; use starknet_api::hash::StarkHash; use starknet_api::patricia_key; use starknet_api::transaction::{ @@ -501,6 +503,102 @@ pub fn to_class( } } +/// TODO: remove this function once starknet api 0.8.0 is supported. +fn starknet_api_ethaddr_to_felt(value: starknet_api::core::EthAddress) -> FieldElement { + let mut bytes = [0u8; 32]; + // Padding H160 with zeros to 32 bytes (big endian) + bytes[12..32].copy_from_slice(value.0.as_bytes()); + let stark_felt = starknet_api::hash::StarkFelt::new(bytes).expect("valid slice for stark felt"); + stark_felt.into() +} + +pub fn to_exec_info( + exec_info: blockifier::transaction::objects::TransactionExecutionInfo, +) -> katana_primitives::trace::TxExecInfo { + katana_primitives::trace::TxExecInfo { + validate_call_info: exec_info.validate_call_info.map(to_call_info), + execute_call_info: exec_info.execute_call_info.map(to_call_info), + fee_transfer_call_info: exec_info.fee_transfer_call_info.map(to_call_info), + actual_fee: exec_info.actual_fee.0, + actual_resources: exec_info + .actual_resources + .0 + .into_iter() + .map(|(k, v)| (k, v as u64)) + .collect(), + revert_error: exec_info.revert_error.clone(), + } +} + +fn to_call_info(call_info: CallInfo) -> katana_primitives::trace::CallInfo { + let message_to_l1_from_address = if let Some(a) = call_info.call.code_address { + to_address(a) + } else { + to_address(call_info.call.caller_address) + }; + + katana_primitives::trace::CallInfo { + caller_address: to_address(call_info.call.caller_address), + call_type: match call_info.call.call_type { + CallType::Call => katana_primitives::trace::CallType::Call, + CallType::Delegate => katana_primitives::trace::CallType::Delegate, + }, + code_address: call_info.call.code_address.map(to_address), + class_hash: call_info.call.class_hash.map(|a| a.0.into()), + entry_point_selector: call_info.call.entry_point_selector.0.into(), + entry_point_type: match call_info.call.entry_point_type { + EntryPointType::External => katana_primitives::trace::EntryPointType::External, + EntryPointType::L1Handler => katana_primitives::trace::EntryPointType::L1Handler, + EntryPointType::Constructor => katana_primitives::trace::EntryPointType::Constructor, + }, + calldata: call_info.call.calldata.0.iter().map(|f| (*f).into()).collect(), + retdata: call_info.execution.retdata.0.iter().map(|f| (*f).into()).collect(), + execution_resources: katana_primitives::trace::ExecutionResources { + n_steps: call_info.vm_resources.n_steps as u64, + n_memory_holes: call_info.vm_resources.n_memory_holes as u64, + builtin_instance_counter: call_info + .vm_resources + .builtin_instance_counter + .into_iter() + .map(|(k, v)| (k, v as u64)) + .collect(), + }, + events: call_info + .execution + .events + .iter() + .map(|e| katana_primitives::event::OrderedEvent { + order: e.order as u64, + keys: e.event.keys.iter().map(|f| f.0.into()).collect(), + data: e.event.data.0.iter().map(|f| (*f).into()).collect(), + }) + .collect(), + l2_to_l1_messages: call_info + .execution + .l2_to_l1_messages + .iter() + .map(|m| { + let to_address = starknet_api_ethaddr_to_felt(m.message.to_address); + katana_primitives::message::OrderedL2ToL1Message { + order: m.order as u64, + from_address: message_to_l1_from_address, + to_address: to_address.into(), + payload: m.message.payload.0.iter().map(|f| (*f).into()).collect(), + } + }) + .collect(), + storage_read_values: call_info.storage_read_values.into_iter().map(|f| f.into()).collect(), + accessed_storage_keys: call_info + .accessed_storage_keys + .into_iter() + .map(|sk| (*sk.0.key()).into()) + .collect(), + inner_calls: call_info.inner_calls.iter().map(|c| to_call_info(c.clone())).collect(), + gas_consumed: call_info.execution.gas_consumed as u128, + failed: call_info.execution.failed, + } +} + #[cfg(test)] mod tests { use katana_primitives::chain::{ChainId, NamedChainId}; diff --git a/crates/katana/executor/src/implementation/noop.rs b/crates/katana/executor/src/implementation/noop.rs index 652873f0da..882b646f6d 100644 --- a/crates/katana/executor/src/implementation/noop.rs +++ b/crates/katana/executor/src/implementation/noop.rs @@ -3,6 +3,7 @@ use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, Flat use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; use katana_primitives::env::{BlockEnv, CfgEnv}; use katana_primitives::receipt::{InvokeTxReceipt, Receipt}; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{ExecutableTxWithHash, Tx, TxWithHash}; use katana_primitives::FieldElement; use katana_provider::traits::contract::ContractClassProvider; @@ -99,7 +100,7 @@ impl<'a> BlockExecutor<'a> for NoopExecutor { Box::new(NoopStateProvider) } - fn transactions(&self) -> &[(TxWithHash, Option)] { + fn transactions(&self) -> &[(TxWithHash, Option, TxExecInfo)] { &[] } @@ -127,6 +128,10 @@ impl TransactionExecutionOutput for NoopTransactionExecutionOutput { fn revert_error(&self) -> Option<&str> { None } + + fn execution_info(&self) -> TxExecInfo { + TxExecInfo::default() + } } struct NoopStateProvider; diff --git a/crates/katana/executor/src/implementation/sir/mod.rs b/crates/katana/executor/src/implementation/sir/mod.rs index 42c1c120b3..ed011ce9ec 100644 --- a/crates/katana/executor/src/implementation/sir/mod.rs +++ b/crates/katana/executor/src/implementation/sir/mod.rs @@ -11,6 +11,7 @@ use katana_primitives::contract::{ContractAddress, StorageKey, StorageValue}; use katana_primitives::env::{BlockEnv, CfgEnv}; use katana_primitives::receipt::Receipt; use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxWithHash}; use katana_primitives::FieldElement; use katana_provider::traits::state::StateProvider; @@ -71,7 +72,7 @@ impl ExecutorFactory for NativeExecutorFactory { pub struct StarknetVMProcessor<'a> { block_context: BlockContext, state: Arc, PermanentContractClassCache>>, - transactions: Vec<(TxWithHash, Option)>, + transactions: Vec<(TxWithHash, Option, TxExecInfo)>, simulation_flags: SimulationFlag, } @@ -159,7 +160,8 @@ impl<'a> TransactionExecutor for StarknetVMProcessor<'a> { let res = utils::transact(tx, &mut state.0.write().inner, block_context, gas, flags)?; let receipt = res.receipt(tx_.as_ref()); - self.transactions.push((tx_, Some(receipt))); + let exec_info = res.execution_info(); + self.transactions.push((tx_, Some(receipt), exec_info)); if let Some((class_hash, compiled_class, sierra_class)) = class_declaration_artifacts { state.0.write().declared_classes.insert(class_hash, (compiled_class, sierra_class)); @@ -281,7 +283,7 @@ impl<'a> BlockExecutor<'a> for StarknetVMProcessor<'a> { Box::new(self.state.clone()) } - fn transactions(&self) -> &[(TxWithHash, Option)] { + fn transactions(&self) -> &[(TxWithHash, Option, TxExecInfo)] { &self.transactions } diff --git a/crates/katana/executor/src/implementation/sir/output.rs b/crates/katana/executor/src/implementation/sir/output.rs index 05a418e83d..92d1b69bc2 100644 --- a/crates/katana/executor/src/implementation/sir/output.rs +++ b/crates/katana/executor/src/implementation/sir/output.rs @@ -4,6 +4,7 @@ use katana_primitives::receipt::{ DeclareTxReceipt, DeployAccountTxReceipt, Event, InvokeTxReceipt, L1HandlerTxReceipt, MessageToL1, Receipt, TxExecutionResources, }; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::Tx; use sir::execution::CallInfo; @@ -73,6 +74,10 @@ impl TransactionExecutionOutput for TransactionExecutionInfo { fn revert_error(&self) -> Option<&str> { self.inner.revert_error.as_deref() } + + fn execution_info(&self) -> TxExecInfo { + utils::from_sir_exec_info(&self.inner) + } } fn events_from_exec_info(info: &TransactionExecutionInfo) -> Vec { diff --git a/crates/katana/executor/src/implementation/sir/utils.rs b/crates/katana/executor/src/implementation/sir/utils.rs index 4dda6aff7a..a4dc0baf27 100644 --- a/crates/katana/executor/src/implementation/sir/utils.rs +++ b/crates/katana/executor/src/implementation/sir/utils.rs @@ -10,7 +10,7 @@ use katana_primitives::FieldElement; use sir::definitions::block_context::BlockContext; use sir::definitions::constants::TRANSACTION_VERSION; use sir::execution::execution_entry_point::{ExecutionEntryPoint, ExecutionResult}; -use sir::execution::{CallType, TransactionExecutionContext}; +use sir::execution::{CallInfo, CallType, TransactionExecutionContext}; use sir::services::api::contract_classes::compiled_class::CompiledClass as SirCompiledClass; use sir::services::api::contract_classes::deprecated_contract_class::ContractClass as SirDeprecatedContractClass; use sir::state::contract_class_cache::ContractClassCache; @@ -561,3 +561,99 @@ fn to_sir_current_account_tx_fields( nonce_data_availability_mode, }) } + +pub fn from_sir_exec_info( + exec_info: &sir::execution::TransactionExecutionInfo, +) -> katana_primitives::trace::TxExecInfo { + katana_primitives::trace::TxExecInfo { + validate_call_info: exec_info.validate_info.clone().map(from_sir_call_info), + execute_call_info: exec_info.call_info.clone().map(from_sir_call_info), + fee_transfer_call_info: exec_info.fee_transfer_info.clone().map(from_sir_call_info), + actual_fee: exec_info.actual_fee, + actual_resources: exec_info + .actual_resources + .clone() + .into_iter() + .map(|(k, v)| (k, v as u64)) + .collect(), + revert_error: exec_info.revert_error.clone(), + // exec_info.tx_type being dropped here. + } +} + +fn from_sir_call_info(call_info: CallInfo) -> katana_primitives::trace::CallInfo { + let message_to_l1_from_address = if let Some(ref a) = call_info.code_address { + to_address(a) + } else { + to_address(&call_info.caller_address) + }; + + katana_primitives::trace::CallInfo { + caller_address: to_address(&call_info.caller_address), + call_type: match call_info.call_type { + Some(CallType::Call) => katana_primitives::trace::CallType::Call, + Some(CallType::Delegate) => katana_primitives::trace::CallType::Delegate, + _ => panic!("CallType is expected"), + }, + code_address: call_info.code_address.as_ref().map(to_address), + class_hash: call_info.class_hash.as_ref().map(to_class_hash), + entry_point_selector: to_felt( + &call_info.entry_point_selector.expect("EntryPointSelector is expected"), + ), + entry_point_type: match call_info.entry_point_type { + Some(EntryPointType::External) => katana_primitives::trace::EntryPointType::External, + Some(EntryPointType::L1Handler) => katana_primitives::trace::EntryPointType::L1Handler, + Some(EntryPointType::Constructor) => { + katana_primitives::trace::EntryPointType::Constructor + } + _ => panic!("EntryPointType is expected"), + }, + calldata: call_info.calldata.iter().map(to_felt).collect(), + retdata: call_info.retdata.iter().map(to_felt).collect(), + execution_resources: if let Some(ei) = call_info.execution_resources { + katana_primitives::trace::ExecutionResources { + n_steps: ei.n_steps as u64, + n_memory_holes: ei.n_memory_holes as u64, + builtin_instance_counter: ei + .builtin_instance_counter + .into_iter() + .map(|(k, v)| (k, v as u64)) + .collect(), + } + } else { + katana_primitives::trace::ExecutionResources::default() + }, + events: call_info + .events + .iter() + .map(|e| katana_primitives::event::OrderedEvent { + order: e.order, + keys: e.keys.iter().map(to_felt).collect(), + data: e.data.iter().map(to_felt).collect(), + }) + .collect(), + l2_to_l1_messages: call_info + .l2_to_l1_messages + .iter() + .map(|m| katana_primitives::message::OrderedL2ToL1Message { + order: m.order as u64, + from_address: message_to_l1_from_address, + to_address: to_address(&m.to_address), + payload: m.payload.iter().map(to_felt).collect(), + }) + .collect(), + storage_read_values: call_info + .storage_read_values + .into_iter() + .map(|f| to_felt(&f)) + .collect(), + accessed_storage_keys: call_info.accessed_storage_keys.iter().map(to_class_hash).collect(), + inner_calls: call_info + .internal_calls + .iter() + .map(|c| from_sir_call_info(c.clone())) + .collect(), + gas_consumed: call_info.gas_consumed, + failed: call_info.failure_flag, + } +} diff --git a/crates/katana/executor/tests/executor.rs b/crates/katana/executor/tests/executor.rs index 5a386c4e90..7e6272546e 100644 --- a/crates/katana/executor/tests/executor.rs +++ b/crates/katana/executor/tests/executor.rs @@ -254,10 +254,10 @@ fn test_executor_with_valid_blocks_impl( // assert the state updates let ExecutionOutput { states, transactions } = executor.take_execution_output().unwrap(); // asserts that the executed transactions are stored - let actual_txs: Vec = transactions.iter().map(|(tx, _)| tx.clone()).collect(); + let actual_txs: Vec = transactions.iter().map(|(tx, _, _)| tx.clone()).collect(); assert_eq!(actual_txs, expected_txs); - assert!(transactions.iter().all(|(_, rct)| rct.is_some()), "all txs should have a receipt"); + assert!(transactions.iter().all(|(_, rct, _)| rct.is_some()), "all txs should have a receipt"); let actual_nonce_updates = states.state_updates.nonce_updates; let expected_nonce_updates = HashMap::from([(main_account, felt!("3")), (new_acc, felt!("1"))]); diff --git a/crates/katana/executor/tests/fixtures/mod.rs b/crates/katana/executor/tests/fixtures/mod.rs index 42c38daf7c..9a7f04ca8d 100644 --- a/crates/katana/executor/tests/fixtures/mod.rs +++ b/crates/katana/executor/tests/fixtures/mod.rs @@ -79,7 +79,7 @@ pub fn state_provider(genesis: &Genesis) -> Box { }; provider - .insert_block_with_states_and_receipts(block, states, vec![]) + .insert_block_with_states_and_receipts(block, states, vec![], vec![]) .expect("able to insert block"); ::latest(&provider).unwrap() diff --git a/crates/katana/primitives/src/event.rs b/crates/katana/primitives/src/event.rs index ebe08293cd..4fbbc470e9 100644 --- a/crates/katana/primitives/src/event.rs +++ b/crates/katana/primitives/src/event.rs @@ -1,6 +1,16 @@ use core::fmt; use std::num::ParseIntError; +use crate::FieldElement; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OrderedEvent { + pub order: u64, + pub keys: Vec, + pub data: Vec, +} + #[derive(PartialEq, Eq, Debug, Default)] pub struct ContinuationToken { pub block_n: u64, diff --git a/crates/katana/primitives/src/lib.rs b/crates/katana/primitives/src/lib.rs index 80e043914b..cc8bdd2988 100644 --- a/crates/katana/primitives/src/lib.rs +++ b/crates/katana/primitives/src/lib.rs @@ -5,7 +5,9 @@ pub mod contract; pub mod env; pub mod event; pub mod genesis; +pub mod message; pub mod receipt; +pub mod trace; pub mod transaction; pub mod version; diff --git a/crates/katana/primitives/src/message.rs b/crates/katana/primitives/src/message.rs new file mode 100644 index 0000000000..5545367756 --- /dev/null +++ b/crates/katana/primitives/src/message.rs @@ -0,0 +1,11 @@ +use crate::contract::ContractAddress; +use crate::FieldElement; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OrderedL2ToL1Message { + pub order: u64, + pub from_address: ContractAddress, + pub to_address: ContractAddress, + pub payload: Vec, +} diff --git a/crates/katana/primitives/src/trace.rs b/crates/katana/primitives/src/trace.rs new file mode 100644 index 0000000000..85d78c03f1 --- /dev/null +++ b/crates/katana/primitives/src/trace.rs @@ -0,0 +1,86 @@ +use std::collections::{HashMap, HashSet}; + +use crate::class::ClassHash; +use crate::contract::ContractAddress; +use crate::event::OrderedEvent; +use crate::message::OrderedL2ToL1Message; +use crate::FieldElement; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct TxExecInfo { + /// Transaction validation call info; [None] for `L1Handler`. + pub validate_call_info: Option, + /// Transaction execution call info; [None] for `Declare`. + pub execute_call_info: Option, + /// Fee transfer call info; [None] for `L1Handler`. + pub fee_transfer_call_info: Option, + /// The actual fee that was charged (in Wei). + pub actual_fee: u128, + /// Actual execution resources the transaction is charged for, + /// including L1 gas and additional OS resources estimation. + pub actual_resources: HashMap, + /// Error string for reverted transactions; [None] if transaction execution was successful. + pub revert_error: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ExecutionResources { + pub n_steps: u64, + pub n_memory_holes: u64, + pub builtin_instance_counter: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum CallType { + Call, + Delegate, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum EntryPointType { + External, + L1Handler, + Constructor, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CallInfo { + /// The contract address which the call is initiated from. + pub caller_address: ContractAddress, + /// The call type. + pub call_type: CallType, + /// The address where the code is being executed. + /// Optional, since there is no address to the code implementation in a delegate call. + pub code_address: Option, + /// The class hash, not given if it can be deduced from the storage address. + pub class_hash: Option, + /// The entry point selector. + pub entry_point_selector: FieldElement, + /// The entry point type. + pub entry_point_type: EntryPointType, + /// The data used as the input to the execute entry point. + pub calldata: Vec, + /// The data returned by the entry point execution. + pub retdata: Vec, + /// The resources used by the execution. + pub execution_resources: ExecutionResources, + /// The list of ordered events generated by the execution. + pub events: Vec, + /// The list of ordered l2 to l1 messages generated by the execution. + pub l2_to_l1_messages: Vec, + /// The list of storage addresses being read during the execution. + pub storage_read_values: Vec, + /// The list of storage addresses being accessed during the execution. + pub accessed_storage_keys: HashSet, + /// The list of inner calls triggered by the current call. + pub inner_calls: Vec, + /// The total gas consumed by the call. + pub gas_consumed: u128, + /// True if the execution has failed, false otherwise. + pub failed: bool, +} diff --git a/crates/katana/primitives/src/transaction.rs b/crates/katana/primitives/src/transaction.rs index e1d9792ae2..90a8d7c9be 100644 --- a/crates/katana/primitives/src/transaction.rs +++ b/crates/katana/primitives/src/transaction.rs @@ -14,7 +14,7 @@ use crate::{utils, FieldElement}; /// The hash of a transaction. pub type TxHash = FieldElement; -/// The sequential number for all the transactions.. +/// The sequential number for all the transactions. pub type TxNumber = u64; #[derive(Debug, Clone, PartialEq, Eq)] @@ -348,7 +348,7 @@ impl DeclareTx { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct L1HandlerTx { pub nonce: Nonce, diff --git a/crates/katana/rpc/rpc-api/src/lib.rs b/crates/katana/rpc/rpc-api/src/lib.rs index 6f381ac7be..198766158b 100644 --- a/crates/katana/rpc/rpc-api/src/lib.rs +++ b/crates/katana/rpc/rpc-api/src/lib.rs @@ -1,5 +1,6 @@ pub mod dev; pub mod katana; +pub mod saya; pub mod starknet; pub mod torii; @@ -10,4 +11,5 @@ pub enum ApiKind { Katana, Torii, Dev, + Saya, } diff --git a/crates/katana/rpc/rpc-api/src/saya.rs b/crates/katana/rpc/rpc-api/src/saya.rs new file mode 100644 index 0000000000..fa9017250f --- /dev/null +++ b/crates/katana/rpc/rpc-api/src/saya.rs @@ -0,0 +1,20 @@ +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use katana_rpc_types::transaction::{TransactionsExecutionsPage, TransactionsPageCursor}; + +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "saya"))] +#[cfg_attr(feature = "client", rpc(client, server, namespace = "saya"))] +pub trait SayaApi { + /// Fetches the transaction execution info for all the transactions in the + /// given block. + /// + /// # Arguments + /// + /// * `block_number` - The block number to get executions from. + /// * `chunk_size` - The maximum number of transaction execution that should be returned. + #[method(name = "getTransactionsExecutions")] + async fn get_transactions_executions( + &self, + cursor: TransactionsPageCursor, + ) -> RpcResult; +} diff --git a/crates/katana/rpc/rpc-types/src/error/mod.rs b/crates/katana/rpc/rpc-types/src/error/mod.rs index 4ba97df32c..e935e90516 100644 --- a/crates/katana/rpc/rpc-types/src/error/mod.rs +++ b/crates/katana/rpc/rpc-types/src/error/mod.rs @@ -1,3 +1,4 @@ pub mod katana; +pub mod saya; pub mod starknet; pub mod torii; diff --git a/crates/katana/rpc/rpc-types/src/error/saya.rs b/crates/katana/rpc/rpc-types/src/error/saya.rs new file mode 100644 index 0000000000..b832c46361 --- /dev/null +++ b/crates/katana/rpc/rpc-types/src/error/saya.rs @@ -0,0 +1,53 @@ +use jsonrpsee::core::Error; +use jsonrpsee::types::error::CallError; +use jsonrpsee::types::ErrorObject; +use katana_core::sequencer_error::SequencerError; +use katana_provider::error::ProviderError; + +#[derive(Debug, thiserror::Error, Clone)] +#[repr(i32)] +pub enum SayaApiError { + #[error("Transaction index out of bounds")] + TransactionOutOfBounds, + #[error("Block not found")] + BlockNotFound, + #[error("Transaction not found")] + TransactionNotFound, + #[error("An unexpected error occured: {reason}")] + UnexpectedError { reason: String }, +} + +impl SayaApiError { + fn code(&self) -> i32 { + match self { + SayaApiError::TransactionOutOfBounds => 1, + SayaApiError::BlockNotFound => 24, + SayaApiError::TransactionNotFound => 25, + SayaApiError::UnexpectedError { .. } => 63, + } + } +} + +impl From for SayaApiError { + fn from(value: ProviderError) -> Self { + SayaApiError::UnexpectedError { reason: value.to_string() } + } +} + +impl From for SayaApiError { + fn from(value: SequencerError) -> Self { + match value { + SequencerError::BlockNotFound(_) => SayaApiError::BlockNotFound, + err => SayaApiError::UnexpectedError { reason: err.to_string() }, + } + } +} + +impl From for Error { + fn from(err: SayaApiError) -> Self { + let code = err.code(); + let message = err.to_string(); + let err = ErrorObject::owned(code, message, None::<()>); + Error::Call(CallError::Custom(err)) + } +} diff --git a/crates/katana/rpc/rpc-types/src/error/torii.rs b/crates/katana/rpc/rpc-types/src/error/torii.rs index 8beeb260c4..c37f24e067 100644 --- a/crates/katana/rpc/rpc-types/src/error/torii.rs +++ b/crates/katana/rpc/rpc-types/src/error/torii.rs @@ -3,8 +3,7 @@ use jsonrpsee::core::Error; use jsonrpsee::types::error::CallError; use jsonrpsee::types::ErrorObject; use katana_core::sequencer_error::SequencerError; -use katana_primitives::receipt::Receipt; -use katana_primitives::transaction::TxWithHash; +use katana_core::service::block_producer::TxWithOutcome; use katana_provider::error::ProviderError; use crate::transaction::TransactionsPageCursor; @@ -21,10 +20,7 @@ pub enum ToriiApiError { #[error("Transaction receipt not found")] TransactionReceiptNotFound, #[error("Transactions not ready")] - TransactionsNotReady { - rx: Receiver>, - cursor: TransactionsPageCursor, - }, + TransactionsNotReady { rx: Receiver>, cursor: TransactionsPageCursor }, #[error("Long poll expired")] ChannelDisconnected, #[error("An unexpected error occured: {reason}")] diff --git a/crates/katana/rpc/rpc-types/src/transaction.rs b/crates/katana/rpc/rpc-types/src/transaction.rs index 0da613f0c1..87ecfde28a 100644 --- a/crates/katana/rpc/rpc-types/src/transaction.rs +++ b/crates/katana/rpc/rpc-types/src/transaction.rs @@ -9,6 +9,7 @@ use katana_primitives::conversion::rpc::{ compiled_class_hash_from_flattened_sierra_class, flattened_sierra_to_compiled_class, legacy_rpc_to_compiled_class, }; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{ DeclareTx, DeclareTxV1, DeclareTxV2, DeclareTxV3, DeclareTxWithClass, DeployAccountTx, DeployAccountTxV1, DeployAccountTxV3, InvokeTx, InvokeTxV1, InvokeTxV3, TxHash, TxWithHash, @@ -24,6 +25,8 @@ use starknet::core::utils::get_contract_address; use crate::receipt::MaybePendingTxReceipt; +pub const CHUNK_SIZE_DEFAULT: u64 = 100; + #[derive(Debug, Clone, Serialize, Deserialize, Deref)] #[serde(transparent)] pub struct BroadcastedInvokeTx(pub BroadcastedInvokeTransaction); @@ -498,10 +501,17 @@ impl From for DeployAccountTx { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Copy)] pub struct TransactionsPageCursor { pub block_number: u64, pub transaction_index: u64, + pub chunk_size: u64, +} + +impl Default for TransactionsPageCursor { + fn default() -> Self { + Self { block_number: 0, transaction_index: 0, chunk_size: CHUNK_SIZE_DEFAULT } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -509,3 +519,9 @@ pub struct TransactionsPage { pub transactions: Vec<(TxWithHash, MaybePendingTxReceipt)>, pub cursor: TransactionsPageCursor, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionsExecutionsPage { + pub transactions_executions: Vec, + pub cursor: TransactionsPageCursor, +} diff --git a/crates/katana/rpc/rpc/src/lib.rs b/crates/katana/rpc/rpc/src/lib.rs index 93d6352bbc..d5974b69d8 100644 --- a/crates/katana/rpc/rpc/src/lib.rs +++ b/crates/katana/rpc/rpc/src/lib.rs @@ -1,6 +1,7 @@ pub mod config; pub mod dev; pub mod katana; +pub mod saya; pub mod starknet; pub mod torii; @@ -21,6 +22,7 @@ use katana_core::sequencer::KatanaSequencer; 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::torii::ToriiApiServer; use katana_rpc_api::ApiKind; @@ -28,6 +30,7 @@ use tower_http::cors::{Any, CorsLayer}; use crate::dev::DevApi; use crate::katana::KatanaApi; +use crate::saya::SayaApi; use crate::starknet::StarknetApi; use crate::torii::ToriiApi; @@ -52,6 +55,9 @@ pub async fn spawn( ApiKind::Torii => { methods.merge(ToriiApi::new(sequencer.clone()).into_rpc())?; } + ApiKind::Saya => { + methods.merge(SayaApi::new(sequencer.clone()).into_rpc())?; + } } } diff --git a/crates/katana/rpc/rpc/src/saya.rs b/crates/katana/rpc/rpc/src/saya.rs new file mode 100644 index 0000000000..330fe3eac6 --- /dev/null +++ b/crates/katana/rpc/rpc/src/saya.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use jsonrpsee::core::{async_trait, RpcResult}; +use katana_core::sequencer::KatanaSequencer; +use katana_executor::ExecutorFactory; +use katana_primitives::block::BlockHashOrNumber; +use katana_provider::traits::transaction::TransactionTraceProvider; +use katana_rpc_api::saya::SayaApiServer; +use katana_rpc_types::error::saya::SayaApiError; +use katana_rpc_types::transaction::{TransactionsExecutionsPage, TransactionsPageCursor}; +use katana_tasks::TokioTaskSpawner; + +pub struct SayaApi { + sequencer: Arc>, +} + +impl Clone for SayaApi { + fn clone(&self) -> Self { + Self { sequencer: self.sequencer.clone() } + } +} + +impl SayaApi { + pub fn new(sequencer: Arc>) -> Self { + Self { sequencer } + } + + 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() + } +} + +#[async_trait] +impl SayaApiServer for SayaApi { + async fn get_transactions_executions( + &self, + cursor: TransactionsPageCursor, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + let provider = this.sequencer.backend.blockchain.provider(); + let mut next_cursor = cursor; + + let transactions_executions = provider + .transactions_executions_by_block(BlockHashOrNumber::Num(cursor.block_number)) + .map_err(SayaApiError::from)? + .ok_or(SayaApiError::BlockNotFound)?; + + let total_execs = transactions_executions.len() as u64; + + let transactions_executions = transactions_executions + .into_iter() + .skip(cursor.transaction_index as usize) + .take(cursor.chunk_size as usize) + .collect::>(); + + if cursor.transaction_index + cursor.chunk_size >= total_execs { + // All transactions of the block pointed by the cursor were fetched. + // Indicate to the client this situation by setting the block number + // to the next block and transaction index to 0. + next_cursor.block_number = cursor.block_number + 1; + next_cursor.transaction_index = 0; + } else { + next_cursor.transaction_index += + cursor.transaction_index + transactions_executions.len() as u64; + } + + Ok(TransactionsExecutionsPage { transactions_executions, cursor: next_cursor }) + }) + .await + } +} diff --git a/crates/katana/rpc/rpc/src/starknet.rs b/crates/katana/rpc/rpc/src/starknet.rs index 8d80fac292..f5f13394ba 100644 --- a/crates/katana/rpc/rpc/src/starknet.rs +++ b/crates/katana/rpc/rpc/src/starknet.rs @@ -182,7 +182,7 @@ impl StarknetApiServer for StarknetApi { .read() .transactions() .iter() - .map(|(tx, _)| tx.hash) + .map(|(tx, _, _)| tx.hash) .collect::>(); return Ok(MaybePendingBlockWithTxHashes::Pending( @@ -219,7 +219,7 @@ impl StarknetApiServer for StarknetApi { let executor = executor.read(); let pending_txs = executor.transactions(); - pending_txs.get(index as usize).map(|(tx, _)| tx.clone()) + pending_txs.get(index as usize).map(|(tx, _, _)| tx.clone()) } else { let provider = &this.inner.sequencer.backend.blockchain.provider(); @@ -261,7 +261,7 @@ impl StarknetApiServer for StarknetApi { .read() .transactions() .iter() - .map(|(tx, _)| tx.clone()) + .map(|(tx, _, _)| tx.clone()) .collect::>(); return Ok(MaybePendingBlockWithTxs::Pending(PendingBlockWithTxs::new( @@ -326,7 +326,7 @@ impl StarknetApiServer for StarknetApi { None => { let pending_receipt = this.inner.sequencer.pending_executor().and_then(|executor| { - executor.read().transactions().iter().find_map(|(tx, rct)| { + executor.read().transactions().iter().find_map(|(tx, rct, _)| { if tx.hash == transaction_hash { rct.clone() } else { None } }) }); @@ -697,13 +697,13 @@ impl StarknetApiServer for StarknetApi { let pending_txs = pending_executor.transactions(); // filter only the valid executed transactions (the ones with a receipt) - let mut executed_txs = pending_txs.iter().filter(|(_, rct)| rct.is_some()); + let mut executed_txs = pending_txs.iter().filter(|(_, rct, _)| rct.is_some()); // attemps to find in the valid transactions list first (executed_txs) // if not found, then search in the rejected transactions list (rejected_txs) if let Some(is_reverted) = executed_txs - .find(|(tx, _)| tx.hash == transaction_hash) - .map(|(_, rct)| rct.as_ref().is_some_and(|r| r.is_reverted())) + .find(|(tx, _, _)| tx.hash == transaction_hash) + .map(|(_, rct, _)| rct.as_ref().is_some_and(|r| r.is_reverted())) { let exec_status = if is_reverted { TransactionExecutionStatus::Reverted @@ -715,10 +715,10 @@ impl StarknetApiServer for StarknetApi { } else { // we filter out the executed transactions and only take the rejected ones (the ones // with no receipt) - let mut rejected_txs = pending_txs.iter().filter(|(_, rct)| rct.is_none()); + let mut rejected_txs = pending_txs.iter().filter(|(_, rct, _)| rct.is_none()); rejected_txs - .find(|(tx, _)| tx.hash == transaction_hash) + .find(|(tx, _, _)| tx.hash == transaction_hash) .map(|_| TransactionStatus::Rejected) .ok_or(Error::from(StarknetApiError::TxnHashNotFound)) } diff --git a/crates/katana/rpc/rpc/src/torii.rs b/crates/katana/rpc/rpc/src/torii.rs index 137bf22488..955feba038 100644 --- a/crates/katana/rpc/rpc/src/torii.rs +++ b/crates/katana/rpc/rpc/src/torii.rs @@ -50,7 +50,7 @@ impl ToriiApiServer for ToriiApi { match self .on_io_blocking_task(move |this| { let mut transactions = Vec::new(); - let mut next_cursor = cursor.clone(); + let mut next_cursor = cursor; let provider = this.sequencer.backend.blockchain.provider(); let latest_block_number = @@ -110,7 +110,7 @@ impl ToriiApiServer for ToriiApi { .iter() .skip(cursor.transaction_index as usize) .take(remaining) - .filter_map(|(tx, receipt)| { + .filter_map(|(tx, receipt, _)| { receipt.as_ref().map(|rct| { ( tx.clone(), @@ -148,7 +148,7 @@ impl ToriiApiServer for ToriiApi { .transactions() .iter() .take(remaining) - .filter_map(|(tx, receipt)| { + .filter_map(|(tx, receipt, _)| { receipt.as_ref().map(|rct| { ( tx.clone(), @@ -198,11 +198,12 @@ impl ToriiApiServer for ToriiApi { .await .ok_or(ToriiApiError::ChannelDisconnected)? .into_iter() - .map(|(tx, receipt)| { + .map(|tx_outcome| { ( - tx.clone(), + tx_outcome.tx.clone(), MaybePendingTxReceipt::Pending(PendingTxReceipt::new( - tx.hash, receipt, + tx_outcome.tx.hash, + tx_outcome.receipt, )), ) }) diff --git a/crates/katana/rpc/rpc/tests/common/mod.rs b/crates/katana/rpc/rpc/tests/common/mod.rs index 1d214569b5..321cb8b0ee 100644 --- a/crates/katana/rpc/rpc/tests/common/mod.rs +++ b/crates/katana/rpc/rpc/tests/common/mod.rs @@ -5,8 +5,10 @@ use anyhow::{anyhow, Result}; use cairo_lang_starknet::casm_contract_class::CasmContractClass; use cairo_lang_starknet::contract_class::ContractClass; use katana_primitives::conversion::rpc::CompiledClass; +use starknet::accounts::Call; use starknet::core::types::contract::SierraClass; use starknet::core::types::{FieldElement, FlattenedSierraClass}; +use starknet::core::utils::get_selector_from_name; pub fn prepare_contract_declaration_params( artifact_path: &PathBuf, @@ -33,3 +35,31 @@ fn get_compiled_class_hash(artifact_path: &PathBuf) -> Result { let compiled_class: CompiledClass = serde_json::from_str(&res)?; Ok(compiled_class.class_hash()?) } + +// TODO: not sure why this function is not seen as used +// as prepare_contract_declaration_params is. +#[allow(dead_code)] +pub fn build_deploy_cairo1_contract_call(class_hash: FieldElement, salt: FieldElement) -> Call { + let constructor_calldata = vec![FieldElement::from(1_u32), FieldElement::from(2_u32)]; + + let calldata = [ + vec![ + class_hash, // class hash + salt, // salt + FieldElement::ZERO, // unique + FieldElement::from(constructor_calldata.len()), // constructor calldata len + ], + constructor_calldata.clone(), + ] + .concat(); + + Call { + calldata, + // devnet UDC address + to: FieldElement::from_hex_be( + "0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf", + ) + .unwrap(), + selector: get_selector_from_name("deployContract").unwrap(), + } +} diff --git a/crates/katana/rpc/rpc/tests/saya.rs b/crates/katana/rpc/rpc/tests/saya.rs new file mode 100644 index 0000000000..dd1135c4b2 --- /dev/null +++ b/crates/katana/rpc/rpc/tests/saya.rs @@ -0,0 +1,186 @@ +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use dojo_test_utils::sequencer::{get_default_test_starknet_config, TestSequencer}; +use jsonrpsee::http_client::HttpClientBuilder; +use katana_core::sequencer::SequencerConfig; +use katana_rpc_api::dev::DevApiClient; +use katana_rpc_api::saya::SayaApiClient; +use katana_rpc_api::starknet::StarknetApiClient; +use katana_rpc_types::transaction::{ + TransactionsExecutionsPage, TransactionsPageCursor, CHUNK_SIZE_DEFAULT, +}; +use starknet::accounts::Account; +use starknet::core::types::{FieldElement, TransactionStatus}; +use tokio::time::sleep; + +pub const ENOUGH_GAS: &str = "0x100000000000000000"; + +mod common; + +#[tokio::test(flavor = "multi_thread")] +async fn no_pending_support() { + // Saya does not support the pending block and only work on sealed blocks. + let sequencer = TestSequencer::start( + SequencerConfig { block_time: None, no_mining: true, ..Default::default() }, + get_default_test_starknet_config(), + ) + .await; + + let client = HttpClientBuilder::default().build(sequencer.url()).unwrap(); + + // Should return block not found on trying to fetch the pending block. + let cursor = TransactionsPageCursor { block_number: 1, ..Default::default() }; + + match client.get_transactions_executions(cursor).await { + Ok(_) => panic!("Expected error BlockNotFound"), + Err(e) => { + let eo: jsonrpsee::types::ErrorObject<'_> = e.into(); + assert_eq!(eo.code(), 24); + assert_eq!(eo.message(), "Block not found"); + } + }; +} + +#[tokio::test(flavor = "multi_thread")] +async fn process_sealed_block_only() { + // Saya does not support the pending block and only work on sealed blocks. + let sequencer = TestSequencer::start( + SequencerConfig { block_time: None, no_mining: true, ..Default::default() }, + get_default_test_starknet_config(), + ) + .await; + + let client = HttpClientBuilder::default().build(sequencer.url()).unwrap(); + + let account = sequencer.account(); + + let path: PathBuf = PathBuf::from("tests/test_data/cairo1_contract.json"); + let (contract, compiled_class_hash) = + common::prepare_contract_declaration_params(&path).unwrap(); + let contract = Arc::new(contract); + + // Should return successfully when no transactions have been mined on a block. + let mut cursor = TransactionsPageCursor::default(); + + let response: TransactionsExecutionsPage = + client.get_transactions_executions(cursor).await.unwrap(); + + assert!(response.transactions_executions.is_empty()); + assert!(response.cursor.block_number == 1); + assert!(response.cursor.transaction_index == 0); + assert!(response.cursor.chunk_size == CHUNK_SIZE_DEFAULT); + + let _declare_res = account.declare(contract.clone(), compiled_class_hash).send().await.unwrap(); + + // Should still return 0 transactions execution for the block 0. + let response: TransactionsExecutionsPage = + client.get_transactions_executions(cursor).await.unwrap(); + + assert!(response.transactions_executions.is_empty()); + assert!(response.cursor.block_number == 1); + assert!(response.cursor.transaction_index == 0); + assert!(response.cursor.chunk_size == CHUNK_SIZE_DEFAULT); + + // Create block 1. + let _: () = client.generate_block().await.unwrap(); + + // Should now return 1 transaction from the mined block. + cursor.block_number = 1; + let response: TransactionsExecutionsPage = + client.get_transactions_executions(cursor).await.unwrap(); + + assert!(response.transactions_executions.len() == 1); + assert!(response.cursor.block_number == 2); + assert!(response.cursor.transaction_index == 0); + assert!(response.cursor.chunk_size == CHUNK_SIZE_DEFAULT); +} + +#[tokio::test(flavor = "multi_thread")] +async fn executions_chunks_logic_ok() { + let sequencer = TestSequencer::start( + SequencerConfig { block_time: None, no_mining: true, ..Default::default() }, + get_default_test_starknet_config(), + ) + .await; + + let client = HttpClientBuilder::default().build(sequencer.url()).unwrap(); + + let account = sequencer.account(); + + let path: PathBuf = PathBuf::from("tests/test_data/cairo1_contract.json"); + let (contract, compiled_class_hash) = + common::prepare_contract_declaration_params(&path).unwrap(); + let contract = Arc::new(contract); + + let declare_res = account.declare(contract.clone(), compiled_class_hash).send().await.unwrap(); + + let max_fee = FieldElement::from_hex_be(ENOUGH_GAS).unwrap(); + let mut nonce = FieldElement::ONE; + let mut last_tx_hash = FieldElement::ZERO; + + // Prepare 29 transactions to test chunks (30 at total with the previous declare). + for i in 0..29 { + let deploy_call = + common::build_deploy_cairo1_contract_call(declare_res.class_hash, (i + 2_u32).into()); + let deploy_txn = account.execute(vec![deploy_call]).nonce(nonce).max_fee(max_fee); + let tx_hash = deploy_txn.send().await.unwrap().transaction_hash; + nonce += FieldElement::ONE; + + if i == 28 { + last_tx_hash = tx_hash; + } + } + + assert!(last_tx_hash != FieldElement::ZERO); + + // Poll the statux of the last tx sent. + let max_retry = 10; + let mut attempt = 0; + loop { + match client.transaction_status(last_tx_hash).await { + Ok(s) => { + if s != TransactionStatus::Received { + break; + } + } + Err(_) => { + assert!(attempt < max_retry); + sleep(Duration::from_millis(300)).await; + attempt += 1; + } + } + } + + // Create block 1. + let _: () = client.generate_block().await.unwrap(); + + let cursor = TransactionsPageCursor { block_number: 1, chunk_size: 15, ..Default::default() }; + + let response: TransactionsExecutionsPage = + client.get_transactions_executions(cursor).await.unwrap(); + assert!(response.transactions_executions.len() == 15); + assert!(response.cursor.block_number == 1); + assert!(response.cursor.transaction_index == 15); + + // Should get the remaining 15 transactions and cursor to the next block. + let response: TransactionsExecutionsPage = + client.get_transactions_executions(response.cursor).await.unwrap(); + + assert!(response.transactions_executions.len() == 15); + assert!(response.cursor.block_number == 2); + assert!(response.cursor.transaction_index == 0); + + // Create block 2. + let _: () = client.generate_block().await.unwrap(); + + let response: TransactionsExecutionsPage = + client.get_transactions_executions(response.cursor).await.unwrap(); + + assert!(response.transactions_executions.is_empty()); + assert!(response.cursor.block_number == 3); + assert!(response.cursor.transaction_index == 0); + + sequencer.stop().expect("failed to stop sequencer"); +} diff --git a/crates/katana/rpc/rpc/tests/starknet.rs b/crates/katana/rpc/rpc/tests/starknet.rs index 9daff5b2fa..ec0bf3a57f 100644 --- a/crates/katana/rpc/rpc/tests/starknet.rs +++ b/crates/katana/rpc/rpc/tests/starknet.rs @@ -14,8 +14,6 @@ use starknet::core::types::{ use starknet::core::utils::{get_contract_address, get_selector_from_name}; use starknet::providers::Provider; -use crate::common::prepare_contract_declaration_params; - mod common; const WAIT_TX_DELAY_MILLIS: u64 = 1000; @@ -27,7 +25,8 @@ async fn test_send_declare_and_deploy_contract() { let account = sequencer.account(); let path: PathBuf = PathBuf::from("tests/test_data/cairo1_contract.json"); - let (contract, compiled_class_hash) = prepare_contract_declaration_params(&path).unwrap(); + let (contract, compiled_class_hash) = + common::prepare_contract_declaration_params(&path).unwrap(); let class_hash = contract.class_hash(); let res = account.declare(Arc::new(contract), compiled_class_hash).send().await.unwrap(); diff --git a/crates/katana/rpc/rpc/tests/torii.rs b/crates/katana/rpc/rpc/tests/torii.rs index 05781323b3..5ac772815f 100644 --- a/crates/katana/rpc/rpc/tests/torii.rs +++ b/crates/katana/rpc/rpc/tests/torii.rs @@ -36,7 +36,7 @@ async fn test_get_transactions() { let contract = Arc::new(contract); // Should return successfully when no transactions have been mined. - let cursor = TransactionsPageCursor { block_number: 0, transaction_index: 0 }; + let cursor = TransactionsPageCursor { block_number: 0, transaction_index: 0, chunk_size: 100 }; let response: TransactionsPage = client.get_transactions(cursor).await.unwrap(); @@ -91,7 +91,11 @@ async fn test_get_transactions() { // Should properly increment to new pending block let response: TransactionsPage = client - .get_transactions(TransactionsPageCursor { block_number: 2, transaction_index: 1 }) + .get_transactions(TransactionsPageCursor { + block_number: 2, + transaction_index: 1, + chunk_size: 100, + }) .await .unwrap(); @@ -118,7 +122,7 @@ async fn test_get_transactions() { sleep(Duration::from_millis(5000)).await; let start_cursor = response.cursor; - let response: TransactionsPage = client.get_transactions(start_cursor.clone()).await.unwrap(); + let response: TransactionsPage = client.get_transactions(start_cursor).await.unwrap(); assert!(response.transactions.len() == 100); assert!(response.cursor.block_number == 4); assert!(response.cursor.transaction_index == 100); @@ -132,7 +136,7 @@ async fn test_get_transactions() { // Create block 4. let _: () = client.generate_block().await.unwrap(); - let response: TransactionsPage = client.get_transactions(start_cursor.clone()).await.unwrap(); + let response: TransactionsPage = client.get_transactions(start_cursor).await.unwrap(); assert!(response.transactions.len() == 100); assert!(response.cursor.block_number == 4); assert!(response.cursor.transaction_index == 100); @@ -163,7 +167,7 @@ async fn test_get_transactions_with_instant_mining() { let contract = Arc::new(contract); // Should return successfully when no transactions have been mined. - let cursor = TransactionsPageCursor { block_number: 0, transaction_index: 0 }; + let cursor = TransactionsPageCursor { block_number: 0, transaction_index: 0, chunk_size: 100 }; let declare_res = account.declare(contract.clone(), compiled_class_hash).send().await.unwrap(); @@ -201,7 +205,11 @@ async fn test_get_transactions_with_instant_mining() { // Should properly increment to new pending block let response: TransactionsPage = client - .get_transactions(TransactionsPageCursor { block_number: 2, transaction_index: 1 }) + .get_transactions(TransactionsPageCursor { + block_number: 2, + transaction_index: 1, + chunk_size: 100, + }) .await .unwrap(); diff --git a/crates/katana/storage/db/src/codecs/postcard.rs b/crates/katana/storage/db/src/codecs/postcard.rs index 3b9ceb4338..074bbec480 100644 --- a/crates/katana/storage/db/src/codecs/postcard.rs +++ b/crates/katana/storage/db/src/codecs/postcard.rs @@ -1,6 +1,7 @@ use katana_primitives::block::{BlockNumber, Header}; use katana_primitives::contract::{ContractAddress, GenericContractInfo}; use katana_primitives::receipt::Receipt; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::Tx; use katana_primitives::FieldElement; use postcard; @@ -32,6 +33,7 @@ macro_rules! impl_compress_and_decompress_for_table_values { impl_compress_and_decompress_for_table_values!( u64, Tx, + TxExecInfo, Header, Receipt, FieldElement, diff --git a/crates/katana/storage/db/src/tables.rs b/crates/katana/storage/db/src/tables.rs index e9c94e1269..b42f431e8e 100644 --- a/crates/katana/storage/db/src/tables.rs +++ b/crates/katana/storage/db/src/tables.rs @@ -2,6 +2,7 @@ use katana_primitives::block::{BlockHash, BlockNumber, FinalityStatus, Header}; use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; use katana_primitives::contract::{ContractAddress, GenericContractInfo, StorageKey}; use katana_primitives::receipt::Receipt; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{Tx, TxHash, TxNumber}; use crate::codecs::{Compress, Decode, Decompress, Encode}; @@ -189,6 +190,8 @@ tables! { Transactions: (TxNumber) => Tx, /// Stores the block number of a transaction. TxBlocks: (TxNumber) => BlockNumber, + /// Stores the transaction's execution info. + TxExecutions: (TxNumber) => TxExecInfo, /// Store transaction receipts Receipts: (TxNumber) => Receipt, /// Store compiled classes diff --git a/crates/katana/storage/provider/src/lib.rs b/crates/katana/storage/provider/src/lib.rs index c14d1cbfbf..c14e408af3 100644 --- a/crates/katana/storage/provider/src/lib.rs +++ b/crates/katana/storage/provider/src/lib.rs @@ -10,13 +10,14 @@ use katana_primitives::contract::{ContractAddress, GenericContractInfo, StorageK use katana_primitives::env::BlockEnv; use katana_primitives::receipt::Receipt; use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{TxHash, TxNumber, TxWithHash}; use katana_primitives::FieldElement; use traits::block::{BlockIdReader, BlockStatusProvider, BlockWriter}; use traits::contract::{ContractClassProvider, ContractClassWriter}; use traits::env::BlockEnvProvider; use traits::state::{StateRootProvider, StateWriter}; -use traits::transaction::TransactionStatusProvider; +use traits::transaction::{TransactionStatusProvider, TransactionTraceProvider}; pub mod error; pub mod providers; @@ -127,8 +128,9 @@ where block: SealedBlockWithStatus, states: StateUpdatesWithDeclaredClasses, receipts: Vec, + executions: Vec, ) -> ProviderResult<()> { - self.provider.insert_block_with_states_and_receipts(block, states, receipts) + self.provider.insert_block_with_states_and_receipts(block, states, receipts, executions) } } @@ -183,6 +185,22 @@ where } } +impl TransactionTraceProvider for BlockchainProvider +where + Db: TransactionTraceProvider, +{ + fn transaction_execution(&self, hash: TxHash) -> ProviderResult> { + TransactionTraceProvider::transaction_execution(&self.provider, hash) + } + + fn transactions_executions_by_block( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + TransactionTraceProvider::transactions_executions_by_block(&self.provider, block_id) + } +} + impl TransactionsProviderExt for BlockchainProvider where Db: TransactionsProviderExt, diff --git a/crates/katana/storage/provider/src/providers/db/mod.rs b/crates/katana/storage/provider/src/providers/db/mod.rs index bce0999587..d59aa8b684 100644 --- a/crates/katana/storage/provider/src/providers/db/mod.rs +++ b/crates/katana/storage/provider/src/providers/db/mod.rs @@ -26,6 +26,7 @@ use katana_primitives::contract::{ use katana_primitives::env::BlockEnv; use katana_primitives::receipt::Receipt; use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{TxHash, TxNumber, TxWithHash}; use katana_primitives::FieldElement; @@ -38,7 +39,8 @@ use crate::traits::env::BlockEnvProvider; use crate::traits::state::{StateFactoryProvider, StateProvider, StateRootProvider}; use crate::traits::state_update::StateUpdateProvider; use crate::traits::transaction::{ - ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionsProviderExt, + ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionTraceProvider, + TransactionsProviderExt, }; use crate::ProviderResult; @@ -490,6 +492,19 @@ impl TransactionStatusProvider for DbProvider { } } +impl TransactionTraceProvider for DbProvider { + fn transaction_execution(&self, _hash: TxHash) -> ProviderResult> { + todo!() + } + + fn transactions_executions_by_block( + &self, + _block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + todo!() + } +} + impl ReceiptProvider for DbProvider { fn receipt_by_hash(&self, hash: TxHash) -> ProviderResult> { let db_tx = self.0.tx()?; @@ -547,6 +562,7 @@ impl BlockWriter for DbProvider { block: SealedBlockWithStatus, states: StateUpdatesWithDeclaredClasses, receipts: Vec, + _executions: Vec, ) -> ProviderResult<()> { self.0.update(move |db_tx| -> ProviderResult<()> { let block_hash = block.block.header.hash; @@ -805,6 +821,7 @@ mod tests { block.clone(), state_updates, vec![Receipt::Invoke(Default::default())], + vec![], ) .expect("failed to insert block"); @@ -882,6 +899,7 @@ mod tests { block.clone(), state_updates1, vec![Receipt::Invoke(Default::default())], + vec![], ) .expect("failed to insert block"); @@ -891,6 +909,7 @@ mod tests { block, state_updates2, vec![Receipt::Invoke(Default::default())], + vec![], ) .expect("failed to insert block"); diff --git a/crates/katana/storage/provider/src/providers/fork/mod.rs b/crates/katana/storage/provider/src/providers/fork/mod.rs index e98bc9e1e4..98a2bd991e 100644 --- a/crates/katana/storage/provider/src/providers/fork/mod.rs +++ b/crates/katana/storage/provider/src/providers/fork/mod.rs @@ -14,6 +14,7 @@ use katana_primitives::contract::ContractAddress; use katana_primitives::env::BlockEnv; use katana_primitives::receipt::Receipt; use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{Tx, TxHash, TxNumber, TxWithHash}; use parking_lot::RwLock; use starknet::providers::jsonrpc::HttpTransport; @@ -32,7 +33,8 @@ use crate::traits::env::BlockEnvProvider; use crate::traits::state::{StateFactoryProvider, StateProvider, StateRootProvider, StateWriter}; use crate::traits::state_update::StateUpdateProvider; use crate::traits::transaction::{ - ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionsProviderExt, + ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionTraceProvider, + TransactionsProviderExt, }; use crate::ProviderResult; @@ -321,6 +323,47 @@ impl TransactionStatusProvider for ForkedProvider { } } +impl TransactionTraceProvider for ForkedProvider { + fn transaction_execution(&self, hash: TxHash) -> ProviderResult> { + let exec = self.storage.read().transaction_numbers.get(&hash).and_then(|num| { + self.storage.read().transactions_executions.get(*num as usize).cloned() + }); + + Ok(exec) + } + + fn transactions_executions_by_block( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + let block_num = match block_id { + BlockHashOrNumber::Num(num) => Some(num), + BlockHashOrNumber::Hash(hash) => self.storage.read().block_numbers.get(&hash).cloned(), + }; + + let Some(StoredBlockBodyIndices { tx_offset, tx_count }) = + block_num.and_then(|num| self.storage.read().block_body_indices.get(&num).cloned()) + else { + return Ok(None); + }; + + let offset = tx_offset as usize; + let count = tx_count as usize; + + let execs = self + .storage + .read() + .transactions_executions + .iter() + .skip(offset) + .take(count) + .cloned() + .collect(); + + Ok(Some(execs)) + } +} + impl ReceiptProvider for ForkedProvider { fn receipt_by_hash(&self, hash: TxHash) -> ProviderResult> { let receipt = self @@ -413,6 +456,7 @@ impl BlockWriter for ForkedProvider { block: SealedBlockWithStatus, states: StateUpdatesWithDeclaredClasses, receipts: Vec, + _executions: Vec, ) -> ProviderResult<()> { let mut storage = self.storage.write(); diff --git a/crates/katana/storage/provider/src/providers/in_memory/cache.rs b/crates/katana/storage/provider/src/providers/in_memory/cache.rs index 82e493ab4c..6bd193b08c 100644 --- a/crates/katana/storage/provider/src/providers/in_memory/cache.rs +++ b/crates/katana/storage/provider/src/providers/in_memory/cache.rs @@ -7,6 +7,7 @@ use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, Flat use katana_primitives::contract::{ContractAddress, GenericContractInfo, StorageKey, StorageValue}; use katana_primitives::receipt::Receipt; use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{Tx, TxHash, TxNumber}; use parking_lot::RwLock; @@ -80,6 +81,7 @@ pub struct CacheDb { pub(crate) state_update: HashMap, pub(crate) receipts: Vec, pub(crate) transactions: Vec, + pub(crate) transactions_executions: Vec, pub(crate) transaction_hashes: HashMap, pub(crate) transaction_numbers: HashMap, pub(crate) transaction_block: HashMap, @@ -112,6 +114,7 @@ impl CacheDb { transaction_hashes: HashMap::new(), block_body_indices: HashMap::new(), transaction_numbers: HashMap::new(), + transactions_executions: Vec::new(), latest_block_hash: Default::default(), latest_block_number: Default::default(), } diff --git a/crates/katana/storage/provider/src/providers/in_memory/mod.rs b/crates/katana/storage/provider/src/providers/in_memory/mod.rs index ae9b0873bf..82b704cb29 100644 --- a/crates/katana/storage/provider/src/providers/in_memory/mod.rs +++ b/crates/katana/storage/provider/src/providers/in_memory/mod.rs @@ -14,6 +14,7 @@ use katana_primitives::contract::ContractAddress; use katana_primitives::env::BlockEnv; use katana_primitives::receipt::Receipt; use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{Tx, TxHash, TxNumber, TxWithHash}; use parking_lot::RwLock; @@ -28,7 +29,8 @@ use crate::traits::env::BlockEnvProvider; use crate::traits::state::{StateFactoryProvider, StateProvider, StateRootProvider, StateWriter}; use crate::traits::state_update::StateUpdateProvider; use crate::traits::transaction::{ - ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionsProviderExt, + ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionTraceProvider, + TransactionsProviderExt, }; use crate::ProviderResult; @@ -315,6 +317,47 @@ impl TransactionStatusProvider for InMemoryProvider { } } +impl TransactionTraceProvider for InMemoryProvider { + fn transaction_execution(&self, hash: TxHash) -> ProviderResult> { + let exec = self.storage.read().transaction_numbers.get(&hash).and_then(|num| { + self.storage.read().transactions_executions.get(*num as usize).cloned() + }); + + Ok(exec) + } + + fn transactions_executions_by_block( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + let block_num = match block_id { + BlockHashOrNumber::Num(num) => Some(num), + BlockHashOrNumber::Hash(hash) => self.storage.read().block_numbers.get(&hash).cloned(), + }; + + let Some(StoredBlockBodyIndices { tx_offset, tx_count }) = + block_num.and_then(|num| self.storage.read().block_body_indices.get(&num).cloned()) + else { + return Ok(None); + }; + + let offset = tx_offset as usize; + let count = tx_count as usize; + + let execs = self + .storage + .read() + .transactions_executions + .iter() + .skip(offset) + .take(count) + .cloned() + .collect(); + + Ok(Some(execs)) + } +} + impl ReceiptProvider for InMemoryProvider { fn receipt_by_hash(&self, hash: TxHash) -> ProviderResult> { let receipt = self @@ -407,6 +450,7 @@ impl BlockWriter for InMemoryProvider { block: SealedBlockWithStatus, states: StateUpdatesWithDeclaredClasses, receipts: Vec, + executions: Vec, ) -> ProviderResult<()> { let mut storage = self.storage.write(); @@ -440,6 +484,7 @@ impl BlockWriter for InMemoryProvider { storage.block_body_indices.insert(block_number, block_body_indices); storage.transactions.extend(txs); + storage.transactions_executions.extend(executions); storage.transaction_hashes.extend(txs_id); storage.transaction_numbers.extend(txs_num); storage.transaction_block.extend(txs_block); diff --git a/crates/katana/storage/provider/src/traits/block.rs b/crates/katana/storage/provider/src/traits/block.rs index ea8085564e..070d016210 100644 --- a/crates/katana/storage/provider/src/traits/block.rs +++ b/crates/katana/storage/provider/src/traits/block.rs @@ -7,6 +7,7 @@ use katana_primitives::block::{ }; use katana_primitives::receipt::Receipt; use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::trace::TxExecInfo; use super::transaction::{TransactionProvider, TransactionsProviderExt}; use crate::ProviderResult; @@ -147,5 +148,6 @@ pub trait BlockWriter: Send + Sync { block: SealedBlockWithStatus, states: StateUpdatesWithDeclaredClasses, receipts: Vec, + executions: Vec, ) -> ProviderResult<()>; } diff --git a/crates/katana/storage/provider/src/traits/transaction.rs b/crates/katana/storage/provider/src/traits/transaction.rs index 6237eaf69d..6054f0c3c0 100644 --- a/crates/katana/storage/provider/src/traits/transaction.rs +++ b/crates/katana/storage/provider/src/traits/transaction.rs @@ -2,6 +2,7 @@ use std::ops::Range; use katana_primitives::block::{BlockHash, BlockHashOrNumber, BlockNumber, FinalityStatus}; use katana_primitives::receipt::Receipt; +use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{TxHash, TxNumber, TxWithHash}; use crate::ProviderResult; @@ -52,6 +53,18 @@ pub trait TransactionStatusProvider: Send + Sync { fn transaction_status(&self, hash: TxHash) -> ProviderResult>; } +#[auto_impl::auto_impl(&, Box, Arc)] +pub trait TransactionTraceProvider: Send + Sync { + /// Returns a transaction execution given its hash. + fn transaction_execution(&self, hash: TxHash) -> ProviderResult>; + + /// Returns all the transactions executions for a given block. + fn transactions_executions_by_block( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>>; +} + #[auto_impl::auto_impl(&, Box, Arc)] pub trait ReceiptProvider: Send + Sync { /// Returns the transaction receipt given a transaction hash. diff --git a/crates/katana/storage/provider/tests/block.rs b/crates/katana/storage/provider/tests/block.rs index 2146eeef8f..83861e52a4 100644 --- a/crates/katana/storage/provider/tests/block.rs +++ b/crates/katana/storage/provider/tests/block.rs @@ -72,6 +72,7 @@ where block.clone(), Default::default(), receipts.clone(), + Default::default(), )?; assert_eq!(provider.latest_number().unwrap(), block.block.header.header.number); diff --git a/crates/katana/storage/provider/tests/fixtures.rs b/crates/katana/storage/provider/tests/fixtures.rs index d6334f61ce..610fb6389d 100644 --- a/crates/katana/storage/provider/tests/fixtures.rs +++ b/crates/katana/storage/provider/tests/fixtures.rs @@ -186,6 +186,7 @@ where }, state_update, Default::default(), + Default::default(), ) .unwrap(); } diff --git a/crates/saya/core/Cargo.toml b/crates/saya/core/Cargo.toml index e30211361c..754df6d8ee 100644 --- a/crates/saya/core/Cargo.toml +++ b/crates/saya/core/Cargo.toml @@ -11,11 +11,13 @@ katana-db.workspace = true katana-executor.workspace = true katana-primitives.workspace = true katana-provider.workspace = true +katana-rpc-types.workspace = true +saya-provider.workspace = true anyhow.workspace = true async-trait.workspace = true convert_case.workspace = true -ethers = "2.0.11" +cairo-vm.workspace = true flate2.workspace = true futures.workspace = true lazy_static = "1.4.0" @@ -30,6 +32,7 @@ thiserror.workspace = true tokio.workspace = true tracing.workspace = true url.workspace = true +starknet-types-core = { version = "0.0.9", default-features = false, features = ["serde", "curve", "num-traits"] } # TODO: use features for each possible DA. celestia-rpc = "0.1.1" diff --git a/crates/saya/core/src/blockchain/mod.rs b/crates/saya/core/src/blockchain/mod.rs new file mode 100644 index 0000000000..0d301de6d4 --- /dev/null +++ b/crates/saya/core/src/blockchain/mod.rs @@ -0,0 +1,152 @@ +//! Blockchain fetched from Katana. +use std::collections::HashMap; + +use cairo_vm::vm::runners::builtin_runner::{ + BITWISE_BUILTIN_NAME, EC_OP_BUILTIN_NAME, HASH_BUILTIN_NAME, KECCAK_BUILTIN_NAME, + OUTPUT_BUILTIN_NAME, POSEIDON_BUILTIN_NAME, RANGE_CHECK_BUILTIN_NAME, + SEGMENT_ARENA_BUILTIN_NAME, SIGNATURE_BUILTIN_NAME, +}; +use katana_primitives::block::{BlockHashOrNumber, BlockIdOrTag, BlockTag, SealedBlockWithStatus}; +use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_provider::providers::in_memory::InMemoryProvider; +use katana_provider::traits::block::{BlockProvider, BlockWriter}; +use katana_provider::traits::contract::ContractClassWriter; +use katana_provider::traits::env::BlockEnvProvider; +use katana_provider::traits::state::{ + StateFactoryProvider, StateProvider, StateRootProvider, StateWriter, +}; +use katana_provider::traits::state_update::StateUpdateProvider; +use katana_provider::traits::transaction::{ + ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionsProviderExt, +}; +use katana_provider::BlockchainProvider; + +use crate::error::{Error as SayaError, SayaResult}; + +pub trait Database: + BlockProvider + + BlockWriter + + TransactionProvider + + TransactionStatusProvider + + TransactionsProviderExt + + ReceiptProvider + + StateUpdateProvider + + StateRootProvider + + StateWriter + + ContractClassWriter + + StateFactoryProvider + + BlockEnvProvider + + 'static + + Send + + Sync +{ +} + +impl Database for T where + T: BlockProvider + + BlockWriter + + TransactionProvider + + TransactionStatusProvider + + TransactionsProviderExt + + ReceiptProvider + + StateUpdateProvider + + StateRootProvider + + StateWriter + + ContractClassWriter + + StateFactoryProvider + + BlockEnvProvider + + 'static + + Send + + Sync +{ +} + +/// Represents the whole blockchain fetched from Katana. +pub struct Blockchain { + inner: BlockchainProvider>, +} + +impl Default for Blockchain { + fn default() -> Self { + Self::new() + } +} + +impl Blockchain { + /// Initializes a new instance of [`Blockchain`]. + pub fn new() -> Self { + Self { inner: BlockchainProvider::new(Box::new(InMemoryProvider::new())) } + } + + /// Returns the internal provider. + pub fn provider(&self) -> &BlockchainProvider> { + &self.inner + } + + /// Retrieves historical state for the given block. + /// + /// # Arguments + /// + /// * `block_id` - The block id at which the state must be retrieved. + pub fn state(&self, block_id: &BlockIdOrTag) -> SayaResult> { + let provider = self.provider(); + + match block_id { + BlockIdOrTag::Tag(BlockTag::Latest) => { + let state = StateFactoryProvider::latest(provider)?; + Ok(state) + } + + BlockIdOrTag::Hash(hash) => { + StateFactoryProvider::historical(provider, BlockHashOrNumber::Hash(*hash))? + .ok_or(SayaError::BlockNotFound(*block_id)) + } + + BlockIdOrTag::Number(num) => { + StateFactoryProvider::historical(provider, BlockHashOrNumber::Num(*num))? + .ok_or(SayaError::BlockNotFound(*block_id)) + } + + BlockIdOrTag::Tag(BlockTag::Pending) => { + panic!("Pending block is not supported"); + } + } + } + + /// Updates the [`Blockchain`] internal state adding the given [`SealedBlockWithStatus`] + /// and the associated [`StateUpdatesWithDeclaredClasses`]. + /// + /// Currently receipts are ignored. + /// + /// # Arguments + /// + /// * `block` - The block to add. + /// * `states` - The state updates associated with the block. + pub fn update_state_with_block( + &mut self, + block: SealedBlockWithStatus, + states: StateUpdatesWithDeclaredClasses, + ) -> SayaResult<()> { + let provider = self.provider(); + // Receipts are not supported currently. We may need them if some + // information about the transaction is missing. + let receipts = vec![]; + + Ok(provider.insert_block_with_states_and_receipts(block, states, receipts, vec![])?) + } +} + +fn _get_default_vm_resource_fee_cost() -> HashMap { + HashMap::from([ + (String::from("n_steps"), 1_f64), + (HASH_BUILTIN_NAME.to_string(), 1_f64), + (RANGE_CHECK_BUILTIN_NAME.to_string(), 1_f64), + (SIGNATURE_BUILTIN_NAME.to_string(), 1_f64), + (BITWISE_BUILTIN_NAME.to_string(), 1_f64), + (POSEIDON_BUILTIN_NAME.to_string(), 1_f64), + (OUTPUT_BUILTIN_NAME.to_string(), 1_f64), + (EC_OP_BUILTIN_NAME.to_string(), 1_f64), + (KECCAK_BUILTIN_NAME.to_string(), 1_f64), + (SEGMENT_ARENA_BUILTIN_NAME.to_string(), 1_f64), + ]) +} diff --git a/crates/saya/core/src/data_availability/mod.rs b/crates/saya/core/src/data_availability/mod.rs index 356234a4c0..8bfe270cc6 100644 --- a/crates/saya/core/src/data_availability/mod.rs +++ b/crates/saya/core/src/data_availability/mod.rs @@ -12,7 +12,6 @@ use starknet::core::types::FieldElement; pub mod celestia; pub mod error; -pub mod state_diff; use error::DataAvailabilityResult; /// All possible chains configuration for data availability. diff --git a/crates/saya/core/src/error.rs b/crates/saya/core/src/error.rs index 1826aff786..aba773abd5 100644 --- a/crates/saya/core/src/error.rs +++ b/crates/saya/core/src/error.rs @@ -1,9 +1,19 @@ #[derive(thiserror::Error, Debug)] pub enum Error { + #[error(transparent)] + Anyhow(#[from] anyhow::Error), #[error(transparent)] DataAvailability(#[from] crate::data_availability::error::Error), #[error("Error from Katana client: {0}")] KatanaClient(String), + #[error(transparent)] + KatanaProvider(#[from] katana_provider::error::ProviderError), + #[error(transparent)] + SayaProvider(#[from] saya_provider::error::ProviderError), + #[error("Block {0:?} not found.")] + BlockNotFound(katana_primitives::block::BlockIdOrTag), + // #[error(transparent)] + // Snos(#[from] snos::error::SnOsError), } pub type SayaResult = Result; diff --git a/crates/saya/core/src/lib.rs b/crates/saya/core/src/lib.rs index ceb58eab2a..8d229b9294 100644 --- a/crates/saya/core/src/lib.rs +++ b/crates/saya/core/src/lib.rs @@ -1,19 +1,23 @@ //! Saya core library. + use std::sync::Arc; +use katana_primitives::block::{BlockNumber, FinalityStatus, SealedBlockWithStatus}; +use saya_provider::rpc::JsonRpcProvider; +use saya_provider::Provider as SayaProvider; use serde::{Deserialize, Serialize}; -use starknet::core::types::{BlockId, MaybePendingStateUpdate, StateUpdate}; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::{JsonRpcClient, Provider}; use tracing::{error, trace}; use url::Url; +use crate::blockchain::Blockchain; use crate::data_availability::{DataAvailabilityClient, DataAvailabilityConfig}; use crate::error::SayaResult; +pub mod blockchain; pub mod data_availability; pub mod error; pub mod prover; +pub mod starknet_os; pub mod verifier; /// Saya's main configuration. @@ -39,8 +43,10 @@ pub struct Saya { config: SayaConfig, /// The data availability client. da_client: Option>, - /// The katana (for now JSON RPC) client. - katana_client: Arc>, + /// The provider to fetch dojo from Katana. + provider: Arc, + /// The blockchain state. + blockchain: Blockchain, } impl Saya { @@ -50,8 +56,9 @@ impl Saya { /// /// * `config` - The main Saya configuration. pub async fn new(config: SayaConfig) -> SayaResult { - let katana_client = - Arc::new(JsonRpcClient::new(HttpTransport::new(config.katana_rpc.clone()))); + // Currently it's only RPC. But it can be the database + // file directly in the future or other transports. + let provider = Arc::new(JsonRpcProvider::new(config.katana_rpc.clone()).await?); let da_client = if let Some(da_conf) = &config.data_availability { Some(data_availability::client_from_config(da_conf.clone()).await?) @@ -59,7 +66,9 @@ impl Saya { None }; - Ok(Self { config, da_client, katana_client }) + let blockchain = Blockchain::new(); + + Ok(Self { config, da_client, provider, blockchain }) } /// Starts the Saya mainloop to fetch and process data. @@ -68,22 +77,22 @@ impl Saya { /// First naive version to have an overview of all the components /// and the process. /// Should be refacto in crates as necessary. - pub async fn start(&self) -> SayaResult<()> { + pub async fn start(&mut self) -> SayaResult<()> { let poll_interval_secs = 1; let mut block = self.config.start_block; loop { - let latest_block = match self.katana_client.block_number().await { + let latest_block = match self.provider.block_number().await { Ok(block_number) => block_number, Err(e) => { - error!("Can't retrieve latest block: {}", e); + error!(?e, "fetch block number"); tokio::time::sleep(tokio::time::Duration::from_secs(poll_interval_secs)).await; continue; } }; if block > latest_block { - trace!("Nothing to process yet, waiting for block {block}"); + trace!(block_number = block, "waiting block number"); tokio::time::sleep(tokio::time::Duration::from_secs(poll_interval_secs)).await; continue; } @@ -91,8 +100,6 @@ impl Saya { self.process_block(block).await?; block += 1; - - tokio::time::sleep(tokio::time::Duration::from_secs(poll_interval_secs)).await; } } @@ -114,38 +121,28 @@ impl Saya { /// # Arguments /// /// * `block_number` - The block number. - async fn process_block(&self, block_number: u64) -> SayaResult<()> { - trace!("Processing block {block_number}"); + async fn process_block(&mut self, block_number: BlockNumber) -> SayaResult<()> { + trace!(block_number, "processing block"); - self.fetch_publish_state_update(block_number).await?; + let block = self.provider.fetch_block(block_number).await?; + let (state_updates, da_state_update) = + self.provider.fetch_state_updates(block_number).await?; - Ok(()) - } + if let Some(da) = &self.da_client { + da.publish_state_diff_felts(&da_state_update).await?; + } - /// Fetches the state update for the given block and publish it to - /// the data availability layer (if any). - /// Returns the [`StateUpdate`]. - /// - /// # Arguments - /// - /// * `block_number` - The block number to get state update for. - async fn fetch_publish_state_update(&self, block_number: u64) -> SayaResult { - let state_update = - match self.katana_client.get_state_update(BlockId::Number(block_number)).await? { - MaybePendingStateUpdate::Update(su) => { - if let Some(da) = &self.da_client { - let sd_felts = - data_availability::state_diff::state_diff_to_felts(&su.state_diff); - - da.publish_state_diff_felts(&sd_felts).await?; - } - - su - } - MaybePendingStateUpdate::PendingUpdate(_) => unreachable!("Should not be used"), - }; + let block = SealedBlockWithStatus { block, status: FinalityStatus::AcceptedOnL2 }; + + self.blockchain.update_state_with_block(block.clone(), state_updates)?; - Ok(state_update) + if block_number == 0 { + return Ok(()); + } + + let _exec_infos = self.provider.fetch_transactions_executions(block_number).await?; + + Ok(()) } } diff --git a/crates/saya/core/src/starknet_os/felt.rs b/crates/saya/core/src/starknet_os/felt.rs new file mode 100644 index 0000000000..05199dddd2 --- /dev/null +++ b/crates/saya/core/src/starknet_os/felt.rs @@ -0,0 +1,25 @@ +//! Felt conversion. +//! +//! Starknet-rs should normally migrate to starknet types core. +//! To be removed once it's ok as the CairoVM is already using +//! the core types. +use starknet::core::types::FieldElement; +use starknet_types_core::felt::Felt; + +/// Converts a starknet-rs [`FieldElement`] to a starknet types core [`Felt`]. +/// +/// # Arguments +/// +/// * `ff` - Starknet-rs [`FieldElement`]. +pub fn from_ff(ff: &FieldElement) -> Felt { + Felt::from_bytes_be(&ff.to_bytes_be()) +} + +/// Converts a vec of [`FieldElement`] to a vec of starknet types core [`Felt`]. +/// +/// # Arguments +/// +/// * `ffs` - Starknet-rs [`&[FieldElement]`]. +pub fn from_ff_vec(ffs: &[FieldElement]) -> Vec { + ffs.iter().map(from_ff).collect() +} diff --git a/crates/saya/core/src/starknet_os/input.rs b/crates/saya/core/src/starknet_os/input.rs new file mode 100644 index 0000000000..5097f57c4e --- /dev/null +++ b/crates/saya/core/src/starknet_os/input.rs @@ -0,0 +1,23 @@ +//! Starknet OS inputs. +//! +//! Python code: +//! +use katana_primitives::block::SealedBlock; +use snos::io::input::StarknetOsInput; + +use super::{felt, transaction}; + +/// Setups a default [`StarknetOsInput`] with the block info. +/// +/// TODO: currently no commitments are computed, but are required +/// to be in the [`StarknetOsInput`]. +/// TODO: some of the input fields can be init from the state. +pub fn snos_input_from_block(block: &SealedBlock) -> StarknetOsInput { + let transactions = block.body.iter().map(transaction::snos_internal_from_tx).collect(); + + StarknetOsInput { + transactions, + block_hash: felt::from_ff(&block.header.hash), + ..Default::default() + } +} diff --git a/crates/saya/core/src/starknet_os/mod.rs b/crates/saya/core/src/starknet_os/mod.rs new file mode 100644 index 0000000000..de24233f11 --- /dev/null +++ b/crates/saya/core/src/starknet_os/mod.rs @@ -0,0 +1,8 @@ +//! Starknet OS types. +// SNOS is based on blockifier, which is not in sync with +// current primitives. +// And SNOS is for now not used. This work must be resume once +// SNOS is actualized. +// mod felt; +// pub mod input; +// pub mod transaction; diff --git a/crates/saya/core/src/starknet_os/transaction.rs b/crates/saya/core/src/starknet_os/transaction.rs new file mode 100644 index 0000000000..20600ce79b --- /dev/null +++ b/crates/saya/core/src/starknet_os/transaction.rs @@ -0,0 +1,120 @@ +//! Transaction conversion into Starknet OS internal transaction type. +//! +//! Transaction internal type python: +//! +//! Transaction types: +//! +use std::fmt; + +use katana_primitives::transaction::{DeclareTx, DeployAccountTx, InvokeTx, Tx, TxWithHash}; +use snos::io::InternalTransaction; +use starknet::core::types::FieldElement; + +use super::felt; + +pub fn snos_internal_from_tx(tx_with_hash: &TxWithHash) -> InternalTransaction { + let mut internal = + InternalTransaction { hash_value: felt::from_ff(&tx_with_hash.hash), ..Default::default() }; + + match &tx_with_hash.transaction { + Tx::Invoke(tx_e) => match tx_e { + InvokeTx::V1(tx) => { + internal.r#type = TransactionType::InvokeFunction.to_string(); + internal.entry_point_type = Some(EntryPointType::External.to_string()); + internal.version = Some(felt::from_ff(&FieldElement::ONE)); + internal.nonce = Some(felt::from_ff(&tx.nonce)); + internal.sender_address = Some(felt::from_ff(&tx.sender_address)); + internal.signature = Some(felt::from_ff_vec(&tx.signature)); + internal.calldata = Some(felt::from_ff_vec(&tx.calldata)); + // Entrypoint selector can be retrieved from Call? + } + // Will be done later as SNOS types should change. + _ => todo!(), + }, + Tx::Declare(tx_e) => match tx_e { + DeclareTx::V1(tx) => { + internal.r#type = TransactionType::Declare.to_string(); + internal.nonce = Some(felt::from_ff(&tx.nonce)); + internal.sender_address = Some(felt::from_ff(&tx.sender_address)); + internal.signature = Some(felt::from_ff_vec(&tx.signature)); + internal.class_hash = Some(felt::from_ff(&tx.class_hash)); + } + DeclareTx::V2(tx) => { + internal.r#type = TransactionType::Declare.to_string(); + internal.nonce = Some(felt::from_ff(&tx.nonce)); + internal.sender_address = Some(felt::from_ff(&tx.sender_address)); + internal.signature = Some(felt::from_ff_vec(&tx.signature)); + internal.class_hash = Some(felt::from_ff(&tx.class_hash)); + } + // Will be done later as SNOS types should change. + _ => todo!(), + }, + Tx::L1Handler(tx) => { + internal.r#type = TransactionType::L1Handler.to_string(); + internal.entry_point_type = Some(EntryPointType::L1Handler.to_string()); + internal.nonce = Some(felt::from_ff(&tx.nonce)); + internal.contract_address = Some(felt::from_ff(&tx.contract_address)); + internal.entry_point_selector = Some(felt::from_ff(&tx.entry_point_selector)); + internal.calldata = Some(felt::from_ff_vec(&tx.calldata)); + } + Tx::DeployAccount(tx_e) => match tx_e { + DeployAccountTx::V1(tx) => { + internal.r#type = TransactionType::DeployAccount.to_string(); + internal.nonce = Some(felt::from_ff(&tx.nonce)); + internal.contract_address = Some(felt::from_ff(&tx.contract_address)); + internal.contract_address_salt = Some(felt::from_ff(&tx.contract_address_salt)); + internal.class_hash = Some(felt::from_ff(&tx.class_hash)); + internal.constructor_calldata = Some(felt::from_ff_vec(&tx.constructor_calldata)); + internal.signature = Some(felt::from_ff_vec(&tx.signature)); + } + // Will be done later as SNOS types should change. + _ => todo!(), + }, + }; + + internal +} + +#[allow(dead_code)] +#[derive(Debug)] +enum TransactionType { + Declare, + Deploy, + DeployAccount, + InitializeBlockInfo, + InvokeFunction, + L1Handler, +} + +impl fmt::Display for TransactionType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match *self { + TransactionType::Declare => "DECLARE", + TransactionType::Deploy => "DEPLOY", + TransactionType::DeployAccount => "DEPLOY_ACCOUNT", + TransactionType::InitializeBlockInfo => "INITIALIZE_BLOCK_INFO", + TransactionType::InvokeFunction => "INVOKE_FUNCTION", + TransactionType::L1Handler => "L1_HANDLER", + }; + write!(f, "{}", s) + } +} + +#[allow(dead_code)] +#[derive(Debug)] +enum EntryPointType { + External, + L1Handler, + Constructor, +} + +impl fmt::Display for EntryPointType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match *self { + EntryPointType::External => "EXTERNAL", + EntryPointType::L1Handler => "L1_HANDLER", + EntryPointType::Constructor => "CONSTRUCTOR", + }; + write!(f, "{}", s) + } +} diff --git a/crates/saya/provider/Cargo.toml b/crates/saya/provider/Cargo.toml new file mode 100644 index 0000000000..13e9108e4a --- /dev/null +++ b/crates/saya/provider/Cargo.toml @@ -0,0 +1,34 @@ +[package] +description = "Saya providers to fetch block data from Katana." +edition.workspace = true +license-file.workspace = true +name = "saya-provider" +repository.workspace = true +version.workspace = true + +[dependencies] +katana-db.workspace = true +katana-executor.workspace = true +katana-primitives.workspace = true +katana-provider.workspace = true +katana-rpc-types.workspace = true +katana-rpc-api = { workspace = true, features = ["client"] } + +anyhow.workspace = true +auto_impl = "1.1.0" +async-trait.workspace = true +convert_case.workspace = true +ethers = "2.0.11" +flate2.workspace = true +futures.workspace = true +jsonrpsee = { workspace = true, features = ["client"] } +lazy_static = "1.4.0" +serde.workspace = true +serde_json.workspace = true +serde_with.workspace = true +starknet.workspace = true +starknet_api.workspace = true +thiserror.workspace = true +tokio.workspace = true +tracing.workspace = true +url.workspace = true diff --git a/crates/saya/provider/src/error.rs b/crates/saya/provider/src/error.rs new file mode 100644 index 0000000000..df3074c664 --- /dev/null +++ b/crates/saya/provider/src/error.rs @@ -0,0 +1,16 @@ +//! Errors related to providers. + +/// Possible errors returned by the provider. +#[derive(Debug, thiserror::Error)] +pub enum ProviderError { + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + #[error(transparent)] + KatanaProvider(#[from] katana_provider::error::ProviderError), + #[error("Block {0:?} not found.")] + BlockNotFound(katana_primitives::block::BlockIdOrTag), + #[error(transparent)] + StarknetProvider(#[from] starknet::providers::ProviderError), + #[error(transparent)] + ValueOutOfRange(#[from] starknet::core::types::ValueOutOfRangeError), +} diff --git a/crates/saya/provider/src/lib.rs b/crates/saya/provider/src/lib.rs new file mode 100644 index 0000000000..49aa867419 --- /dev/null +++ b/crates/saya/provider/src/lib.rs @@ -0,0 +1,13 @@ +//! Saya providers. +//! +//! A provider in Saya is responsible of fetching blocks data +//! and state updates from Katana. +pub mod error; +pub mod provider; +pub mod rpc; + +pub use provider::Provider; + +pub type ProviderResult = Result; + +const LOG_TARGET: &str = "provider"; diff --git a/crates/saya/provider/src/provider.rs b/crates/saya/provider/src/provider.rs new file mode 100644 index 0000000000..6401cdb798 --- /dev/null +++ b/crates/saya/provider/src/provider.rs @@ -0,0 +1,44 @@ +use katana_primitives::block::{BlockNumber, SealedBlock}; +use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::trace::TxExecInfo; +use starknet::core::types::FieldElement; + +use crate::ProviderResult; + +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Box, Arc)] +pub trait Provider { + /// Fetches the current block number of underlying chain. + async fn block_number(&self) -> ProviderResult; + + /// Fetches a block with it's transactions. + /// + /// # Arguments + /// + /// * `block_number` - The block to fetch. + async fn fetch_block(&self, block_number: BlockNumber) -> ProviderResult; + + /// Fetches the state updates related to a given block. + /// Returns the [`StateUpdatesWithDeclaredClasses`] and the serialiazed + /// state update for data availability layer. + /// + /// # Arguments + /// + /// * `block_number` - The block to fetch. + async fn fetch_state_updates( + &self, + block_number: BlockNumber, + ) -> ProviderResult<(StateUpdatesWithDeclaredClasses, Vec)>; + + /// Fetches the transactions executions info for a given block. + /// This method returns the all the executions info for each + /// transaction in a block. + /// + /// # Arguments + /// + /// * `block_number` - The block to fetch. + async fn fetch_transactions_executions( + &self, + block_number: BlockNumber, + ) -> ProviderResult>; +} diff --git a/crates/saya/provider/src/rpc/mod.rs b/crates/saya/provider/src/rpc/mod.rs new file mode 100644 index 0000000000..ffb0b93e64 --- /dev/null +++ b/crates/saya/provider/src/rpc/mod.rs @@ -0,0 +1,179 @@ +//! Provider to fetch Katana data from RPC. +//! +//! The transport here is fixed to JSON RPC. +use std::sync::Arc; + +use anyhow::anyhow; +use jsonrpsee::http_client::HttpClientBuilder; +use katana_primitives::block::{ + BlockIdOrTag, BlockNumber, GasPrices, Header, SealedBlock, SealedHeader, +}; +use katana_primitives::chain::ChainId; +use katana_primitives::conversion::rpc as rpc_converter; +use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::trace::TxExecInfo; +use katana_primitives::transaction::TxWithHash; +use katana_primitives::version::Version; +use katana_rpc_api::saya::SayaApiClient; +use katana_rpc_types::transaction::{TransactionsExecutionsPage, TransactionsPageCursor}; +use starknet::core::types::{ + ContractClass, FieldElement, MaybePendingBlockWithTxs, MaybePendingStateUpdate, +}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{JsonRpcClient, Provider as StarknetProvider}; +use tracing::trace; +use url::Url; + +use crate::provider::Provider; +use crate::rpc::{state as state_converter, transaction as tx_converter}; +use crate::{ProviderResult, LOG_TARGET}; + +mod state; +mod transaction; + +/// A JSON RPC provider. +pub struct JsonRpcProvider { + /// The RPC URL that must be kept for custom endpoints. + rpc_url: String, + /// The Starknet provider. + starknet_provider: Arc>, + /// Chain id detected from the `starknet_provider`. + chain_id: ChainId, +} + +impl JsonRpcProvider { + /// Initializes a new [`JsonRpcProvider`]. + /// Will attempt to fetch the chain id from the provider. + /// + /// # Arguments + /// + /// * `rpc_url` - The RPC url to fetch data from. Must be up and running to fetch the chain id. + pub async fn new(rpc_url: Url) -> ProviderResult { + let starknet_provider = Arc::new(JsonRpcClient::new(HttpTransport::new(rpc_url.clone()))); + + let chain_id: ChainId = starknet_provider.chain_id().await?.into(); + + Ok(Self { starknet_provider, chain_id, rpc_url: rpc_url.to_string() }) + } + + /// Returns the internal [`ChainId`]. + pub fn chain_id(&self) -> ChainId { + self.chain_id + } +} + +#[async_trait::async_trait] +impl Provider for JsonRpcProvider { + async fn block_number(&self) -> ProviderResult { + Ok(self.starknet_provider.block_number().await?) + } + + async fn fetch_block(&self, block_number: BlockNumber) -> ProviderResult { + let block = match self + .starknet_provider + .get_block_with_txs(BlockIdOrTag::Number(block_number)) + .await? + { + MaybePendingBlockWithTxs::Block(b) => b, + MaybePendingBlockWithTxs::PendingBlock(_) => { + panic!("PendingBlock should not be fetched") + } + }; + + let txs: Vec = block + .transactions + .iter() + .map(|tx_rpc| tx_converter::tx_from_rpc(tx_rpc, self.chain_id)) + .collect::, _>>()?; + + Ok(SealedBlock { + header: SealedHeader { + hash: block.block_hash, + header: Header { + parent_hash: block.parent_hash, + number: block.block_number, + gas_prices: GasPrices::new( + block.l1_gas_price.price_in_wei.try_into().unwrap(), + block.l1_gas_price.price_in_fri.try_into().unwrap(), + ), + timestamp: block.timestamp, + state_root: block.new_root, + sequencer_address: block.sequencer_address.into(), + version: Version::parse(&block.starknet_version)?, + }, + }, + body: txs, + }) + } + + async fn fetch_state_updates( + &self, + block_number: BlockNumber, + ) -> ProviderResult<(StateUpdatesWithDeclaredClasses, Vec)> { + let rpc_state_update = match self + .starknet_provider + .get_state_update(BlockIdOrTag::Number(block_number)) + .await? + { + MaybePendingStateUpdate::Update(su) => su, + MaybePendingStateUpdate::PendingUpdate(_) => { + return Err(anyhow!("PendingUpdate should not be fetched").into()); + } + }; + + let serialized_state_update = + state_converter::state_diff_to_felts(&rpc_state_update.state_diff); + let state_updates = state_converter::state_updates_from_rpc(&rpc_state_update)?; + + let mut state_updates_with_classes = + StateUpdatesWithDeclaredClasses { state_updates, ..Default::default() }; + + for class_hash in state_updates_with_classes.state_updates.declared_classes.keys() { + match self + .starknet_provider + .get_class(BlockIdOrTag::Number(block_number), class_hash) + .await? + { + ContractClass::Legacy(legacy) => { + trace!(target: LOG_TARGET, version = "cairo 0", %class_hash, "set contract class"); + + let (hash, class) = rpc_converter::legacy_rpc_to_compiled_class(&legacy)?; + state_updates_with_classes.declared_compiled_classes.insert(hash, class); + } + ContractClass::Sierra(s) => { + trace!(target: LOG_TARGET, version = "cairo 1", %class_hash, "set contract class"); + + state_updates_with_classes + .declared_sierra_classes + .insert(*class_hash, s.clone()); + } + } + } + + Ok((state_updates_with_classes, serialized_state_update)) + } + + async fn fetch_transactions_executions( + &self, + block_number: u64, + ) -> ProviderResult> { + trace!(target: LOG_TARGET, block_number, "fetch transactions executions"); + let cursor = TransactionsPageCursor { block_number, chunk_size: 50, ..Default::default() }; + + let client = HttpClientBuilder::default().build(&self.rpc_url).unwrap(); + let mut executions = vec![]; + + loop { + let rsp: TransactionsExecutionsPage = + client.get_transactions_executions(cursor).await.unwrap(); + + executions.extend(rsp.transactions_executions); + + if rsp.cursor.block_number > block_number { + break; + } + } + + Ok(executions) + } +} diff --git a/crates/saya/core/src/data_availability/state_diff.rs b/crates/saya/provider/src/rpc/state.rs similarity index 89% rename from crates/saya/core/src/data_availability/state_diff.rs rename to crates/saya/provider/src/rpc/state.rs index 7da386de05..890a88dd52 100644 --- a/crates/saya/core/src/data_availability/state_diff.rs +++ b/crates/saya/provider/src/rpc/state.rs @@ -1,7 +1,6 @@ -//! Formats the starknet state diff to be published -//! on a DA layer. +//! State update conversion and data availability formatting. //! -//! All the specification is available here: +//! For data availability format, all the specification is available here: //! . //! //! We use `U256` from ethers for easier computation (than working with felts). @@ -11,17 +10,61 @@ //! to know if an address has been deployed or declared. //! To avoid this overhead, we may want to first generate an hashmap of such //! arrays to then have O(1) search. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use ethers::types::U256; +use katana_primitives::contract::ContractAddress; +use katana_primitives::state::StateUpdates; use starknet::core::types::{ ContractStorageDiffItem, DeclaredClassItem, DeployedContractItem, FieldElement, NonceUpdate, - StateDiff, + StateDiff, StateUpdate, }; +use crate::ProviderResult; + // 2 ^ 128 const CLASS_INFO_FLAG_TRUE: &str = "0x100000000000000000000000000000000"; +/// Converts the [`StateUpdate`] RPC type into [`StateUpdate`] Katana primitive. +/// +/// # Arguments +/// +/// * `state_update` - The RPC state update to convert. +pub fn state_updates_from_rpc(state_update: &StateUpdate) -> ProviderResult { + let mut out = StateUpdates::default(); + + let state_diff = &state_update.state_diff; + + for contract_diff in &state_diff.storage_diffs { + let ContractStorageDiffItem { address, storage_entries: entries } = contract_diff; + + let address: ContractAddress = (*address).into(); + + let contract_entry = out.storage_updates.entry(address).or_insert_with(HashMap::new); + + for e in entries { + contract_entry.insert(e.key, e.value); + } + } + + for nonce_update in &state_diff.nonces { + let NonceUpdate { contract_address, nonce: new_nonce } = *nonce_update; + out.nonce_updates.insert(contract_address.into(), new_nonce); + } + + for deployed in &state_diff.deployed_contracts { + let DeployedContractItem { address, class_hash } = *deployed; + out.contract_updates.insert(address.into(), class_hash); + } + + for decl in &state_diff.declared_classes { + let DeclaredClassItem { class_hash, compiled_class_hash } = decl; + out.declared_classes.insert(*class_hash, *compiled_class_hash); + } + + Ok(out) +} + /// Converts the [`StateDiff`] from RPC types into a [`Vec`]. /// /// Currently, Katana does not support `replaced_classes` and `deprecated_declared_classes`: diff --git a/crates/saya/provider/src/rpc/state_diff.rs b/crates/saya/provider/src/rpc/state_diff.rs new file mode 100644 index 0000000000..959eb414a1 --- /dev/null +++ b/crates/saya/provider/src/rpc/state_diff.rs @@ -0,0 +1,12 @@ + +use std::collections::HashSet; + +use ethers::types::U256; +use starknet::core::types::{ + ContractStorageDiffItem, DeclaredClassItem, DeployedContractItem, FieldElement, NonceUpdate, + StateDiff, +}; + +// 2 ^ 128 +const CLASS_INFO_FLAG_TRUE: &str = "0x100000000000000000000000000000000"; + diff --git a/crates/saya/provider/src/rpc/transaction.rs b/crates/saya/provider/src/rpc/transaction.rs new file mode 100644 index 0000000000..bbc39b316b --- /dev/null +++ b/crates/saya/provider/src/rpc/transaction.rs @@ -0,0 +1,164 @@ +//! Transactions related conversions. +use katana_primitives::chain::ChainId; +use katana_primitives::transaction::{ + DeclareTx, DeclareTxV1, DeclareTxV2, DeclareTxV3, DeployAccountTx, DeployAccountTxV1, + DeployAccountTxV3, InvokeTx, InvokeTxV1, InvokeTxV3, L1HandlerTx, Tx, TxWithHash, +}; +use starknet::core::types::{ + DeclareTransaction, DeployAccountTransaction, FieldElement, InvokeTransaction, Transaction, +}; + +use crate::ProviderResult; + +pub fn tx_from_rpc(tx_rpc: &Transaction, chain_id: ChainId) -> ProviderResult { + match tx_rpc { + Transaction::Invoke(tx_e) => match tx_e { + InvokeTransaction::V0(tx) => Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: { + Tx::Invoke(InvokeTx::V1(InvokeTxV1 { + max_fee: tx.max_fee.try_into()?, + chain_id, + calldata: tx.calldata.clone(), + signature: tx.signature.clone(), + ..Default::default() + })) + }, + }), + InvokeTransaction::V1(tx) => Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: Tx::Invoke(InvokeTx::V1(InvokeTxV1 { + max_fee: tx.max_fee.try_into()?, + chain_id, + calldata: tx.calldata.clone(), + signature: tx.signature.clone(), + nonce: tx.nonce, + sender_address: tx.sender_address.into(), + })), + }), + InvokeTransaction::V3(tx) => Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: Tx::Invoke(InvokeTx::V3(InvokeTxV3 { + chain_id, + sender_address: tx.sender_address.into(), + nonce: tx.nonce, + calldata: tx.calldata.clone(), + signature: tx.signature.clone(), + resource_bounds: tx.resource_bounds.clone(), + tip: tx.tip, + paymaster_data: tx.paymaster_data.clone(), + account_deployment_data: tx.account_deployment_data.clone(), + nonce_data_availability_mode: tx.nonce_data_availability_mode, + fee_data_availability_mode: tx.fee_data_availability_mode, + })), + }), + }, + Transaction::L1Handler(tx) => { + // Seems we have data loss from only this content from the transaction. + // The receipt may be required to complete the data. + // (or use directly the database...) + Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: Tx::L1Handler(L1HandlerTx { + nonce: tx.nonce.into(), + chain_id, + version: FieldElement::ZERO, + calldata: tx.calldata.clone(), + contract_address: tx.contract_address.into(), + entry_point_selector: tx.entry_point_selector, + ..Default::default() + }), + }) + } + Transaction::Declare(tx_e) => match tx_e { + DeclareTransaction::V0(tx) => Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: Tx::Declare(DeclareTx::V1(DeclareTxV1 { + max_fee: tx.max_fee.try_into()?, + chain_id, + class_hash: tx.class_hash, + signature: tx.signature.clone(), + sender_address: tx.sender_address.into(), + ..Default::default() + })), + }), + DeclareTransaction::V1(tx) => Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: Tx::Declare(DeclareTx::V1(DeclareTxV1 { + nonce: tx.nonce, + max_fee: tx.max_fee.try_into()?, + chain_id, + class_hash: tx.class_hash, + signature: tx.signature.clone(), + sender_address: tx.sender_address.into(), + })), + }), + DeclareTransaction::V2(tx) => Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: Tx::Declare(DeclareTx::V2(DeclareTxV2 { + nonce: tx.nonce, + max_fee: tx.max_fee.try_into()?, + chain_id, + class_hash: tx.class_hash, + signature: tx.signature.clone(), + sender_address: tx.sender_address.into(), + compiled_class_hash: tx.compiled_class_hash, + })), + }), + DeclareTransaction::V3(tx) => Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: Tx::Declare(DeclareTx::V3(DeclareTxV3 { + chain_id, + sender_address: tx.sender_address.into(), + nonce: tx.nonce, + signature: tx.signature.clone(), + class_hash: tx.class_hash, + compiled_class_hash: tx.compiled_class_hash, + resource_bounds: tx.resource_bounds.clone(), + tip: tx.tip, + paymaster_data: tx.paymaster_data.clone(), + account_deployment_data: tx.account_deployment_data.clone(), + nonce_data_availability_mode: tx.nonce_data_availability_mode, + fee_data_availability_mode: tx.fee_data_availability_mode, + })), + }), + }, + Transaction::DeployAccount(tx_e) => match tx_e { + DeployAccountTransaction::V1(tx) => Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: Tx::DeployAccount(DeployAccountTx::V1(DeployAccountTxV1 { + nonce: tx.nonce, + max_fee: tx.max_fee.try_into()?, + chain_id, + class_hash: tx.class_hash, + signature: tx.signature.clone(), + contract_address_salt: tx.contract_address_salt, + constructor_calldata: tx.constructor_calldata.clone(), + // contract_address field is missing in tx, to be checked. + ..Default::default() + })), + }), + DeployAccountTransaction::V3(tx) => Ok(TxWithHash { + hash: tx.transaction_hash, + transaction: Tx::DeployAccount(DeployAccountTx::V3(DeployAccountTxV3 { + chain_id, + nonce: tx.nonce, + signature: tx.signature.clone(), + class_hash: tx.class_hash, + // contract_address field is missing in tx, to be checked. + contract_address: Default::default(), + contract_address_salt: tx.contract_address_salt, + constructor_calldata: tx.constructor_calldata.clone(), + resource_bounds: tx.resource_bounds.clone(), + tip: tx.tip, + paymaster_data: tx.paymaster_data.clone(), + nonce_data_availability_mode: tx.nonce_data_availability_mode, + fee_data_availability_mode: tx.fee_data_availability_mode, + })), + }), + }, + Transaction::Deploy(_) => { + panic!("Deploy transaction not supported"); + } + } +}