diff --git a/crates/katana/rpc/rpc-api/src/saya.rs b/crates/katana/rpc/rpc-api/src/saya.rs index fa9017250f..e9e86b7c89 100644 --- a/crates/katana/rpc/rpc-api/src/saya.rs +++ b/crates/katana/rpc/rpc-api/src/saya.rs @@ -1,20 +1,15 @@ use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; -use katana_rpc_types::transaction::{TransactionsExecutionsPage, TransactionsPageCursor}; +use katana_primitives::block::BlockIdOrTag; +use katana_rpc_types::trace::TxExecutionInfo; #[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( + /// Retrieves a list of transaction execution informations of a given block. + #[method(name = "getTransactionExecutionsByBlock")] + async fn transaction_executions_by_block( &self, - cursor: TransactionsPageCursor, - ) -> RpcResult; + block_id: BlockIdOrTag, + ) -> RpcResult>; } diff --git a/crates/katana/rpc/rpc-types/src/trace.rs b/crates/katana/rpc/rpc-types/src/trace.rs index 3bea0d6edb..971d1b46a0 100644 --- a/crates/katana/rpc/rpc-types/src/trace.rs +++ b/crates/katana/rpc/rpc-types/src/trace.rs @@ -1,4 +1,6 @@ -use katana_primitives::trace::CallInfo; +use katana_primitives::trace::{CallInfo, TxExecInfo}; +use katana_primitives::transaction::TxHash; +use serde::{Deserialize, Serialize}; use starknet::core::types::{ CallType, EntryPointType, ExecutionResources, OrderedEvent, OrderedMessage, }; @@ -72,3 +74,12 @@ impl From for FunctionInvocation { }) } } + +/// The type returned by the `saya_getTransactionExecutionsByBlock` RPC method. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxExecutionInfo { + /// The transaction hash. + pub hash: TxHash, + /// The transaction execution trace. + pub trace: TxExecInfo, +} diff --git a/crates/katana/rpc/rpc-types/src/transaction.rs b/crates/katana/rpc/rpc-types/src/transaction.rs index 87ecfde28a..231326474d 100644 --- a/crates/katana/rpc/rpc-types/src/transaction.rs +++ b/crates/katana/rpc/rpc-types/src/transaction.rs @@ -9,7 +9,6 @@ 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, @@ -519,9 +518,3 @@ 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/saya.rs b/crates/katana/rpc/rpc/src/saya.rs index 330fe3eac6..317b128056 100644 --- a/crates/katana/rpc/rpc/src/saya.rs +++ b/crates/katana/rpc/rpc/src/saya.rs @@ -3,11 +3,13 @@ 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_primitives::block::{BlockIdOrTag, BlockTag}; +use katana_provider::error::ProviderError; +use katana_provider::traits::block::{BlockIdReader, BlockProvider}; +use katana_provider::traits::transaction::{TransactionTraceProvider, TransactionsProviderExt}; use katana_rpc_api::saya::SayaApiServer; use katana_rpc_types::error::saya::SayaApiError; -use katana_rpc_types::transaction::{TransactionsExecutionsPage, TransactionsPageCursor}; +use katana_rpc_types::trace::TxExecutionInfo; use katana_tasks::TokioTaskSpawner; pub struct SayaApi { @@ -37,39 +39,72 @@ impl SayaApi { #[async_trait] impl SayaApiServer for SayaApi { - async fn get_transactions_executions( + async fn transaction_executions_by_block( &self, - cursor: TransactionsPageCursor, - ) -> RpcResult { + block_id: BlockIdOrTag, + ) -> 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 }) + match block_id { + BlockIdOrTag::Tag(BlockTag::Pending) => { + // if there is no pending block (eg on instant mining), return an empty list + let Some(pending) = this.sequencer.pending_executor() else { + return Ok(Vec::new()); + }; + + // get the read lock on the pending block + let lock = pending.read(); + + // extract the traces from the pending block + let mut traces = Vec::new(); + for (tx, res) in lock.transactions() { + if let Some(trace) = res.trace().cloned() { + traces.push(TxExecutionInfo { hash: tx.hash, trace }); + } + } + + Ok(traces) + } + + id => { + let number = provider + .convert_block_id(id) + .map_err(SayaApiError::from)? + .ok_or(SayaApiError::BlockNotFound)?; + + // get the transaction traces and their corresponding hashes + + let traces = provider + .transaction_executions_by_block(number.into()) + .map_err(SayaApiError::from)? + .expect("qed; must be Some if block exists"); + + // get the block body indices for the requested block to determine its tx range + // in the db for the tx hashes + + let block_indices = provider + .block_body_indices(number.into()) + .map_err(SayaApiError::from)? + .ok_or(ProviderError::MissingBlockBodyIndices(number)) + .expect("qed; must be Some if block exists"); + + // TODO: maybe we should add a `_by_block` method for the tx hashes as well? + let hashes = provider + .transaction_hashes_in_range(block_indices.clone().into()) + .map_err(SayaApiError::from)?; + + // build the rpc response + + let traces = hashes + .into_iter() + .zip(traces) + .map(|(hash, trace)| TxExecutionInfo { hash, trace }) + .collect::>(); + + Ok(traces) + } + } }) .await } diff --git a/crates/katana/rpc/rpc/tests/saya.rs b/crates/katana/rpc/rpc/tests/saya.rs deleted file mode 100644 index e706177157..0000000000 --- a/crates/katana/rpc/rpc/tests/saya.rs +++ /dev/null @@ -1,204 +0,0 @@ -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 { 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 { 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_eq!(response.cursor.block_number, 1); - assert_eq!(response.cursor.transaction_index, 0); - assert_eq!(response.cursor.chunk_size, CHUNK_SIZE_DEFAULT); - - let declare_res = account.declare(contract.clone(), compiled_class_hash).send().await.unwrap(); - - let max_retry = 10; - let mut attempt = 0; - loop { - match client.transaction_status(declare_res.transaction_hash).await { - Ok(s) => { - if s != TransactionStatus::Received { - break; - } - } - Err(_) => { - assert!(attempt < max_retry); - sleep(Duration::from_millis(300)).await; - attempt += 1; - } - } - } - - // 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_eq!(response.cursor.block_number, 1); - assert_eq!(response.cursor.transaction_index, 0); - assert_eq!(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_eq!(response.transactions_executions.len(), 1); - assert_eq!(response.cursor.block_number, 2); - assert_eq!(response.cursor.transaction_index, 0); - assert_eq!(response.cursor.chunk_size, CHUNK_SIZE_DEFAULT); -} - -#[tokio::test(flavor = "multi_thread")] -async fn executions_chunks_logic_ok() { - let sequencer = TestSequencer::start( - SequencerConfig { 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_eq!(response.transactions_executions.len(), 15); - assert_eq!(response.cursor.block_number, 1); - assert_eq!(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_eq!(response.transactions_executions.len(), 15); - assert_eq!(response.cursor.block_number, 2); - assert_eq!(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_eq!(response.cursor.block_number, 3); - assert_eq!(response.cursor.transaction_index, 0); - - sequencer.stop().expect("failed to stop sequencer"); -} diff --git a/crates/katana/storage/provider/src/lib.rs b/crates/katana/storage/provider/src/lib.rs index 5b247c70da..3f5581d94c 100644 --- a/crates/katana/storage/provider/src/lib.rs +++ b/crates/katana/storage/provider/src/lib.rs @@ -195,11 +195,18 @@ where TransactionTraceProvider::transaction_execution(&self.provider, hash) } - fn transactions_executions_by_block( + fn transaction_executions_by_block( &self, block_id: BlockHashOrNumber, ) -> ProviderResult>> { - TransactionTraceProvider::transactions_executions_by_block(&self.provider, block_id) + TransactionTraceProvider::transaction_executions_by_block(&self.provider, block_id) + } + + fn transaction_executions_in_range( + &self, + range: Range, + ) -> ProviderResult> { + TransactionTraceProvider::transaction_executions_in_range(&self.provider, range) } } diff --git a/crates/katana/storage/provider/src/providers/db/mod.rs b/crates/katana/storage/provider/src/providers/db/mod.rs index c3b302aedc..ec07fa213b 100644 --- a/crates/katana/storage/provider/src/providers/db/mod.rs +++ b/crates/katana/storage/provider/src/providers/db/mod.rs @@ -506,27 +506,36 @@ impl TransactionTraceProvider for DbProvider { } } - fn transactions_executions_by_block( + fn transaction_executions_by_block( &self, block_id: BlockHashOrNumber, ) -> ProviderResult>> { - if let Some(indices) = self.block_body_indices(block_id)? { - let db_tx = self.0.tx()?; - let mut executions = Vec::with_capacity(indices.tx_count as usize); - - let range = Range::from(indices); - for i in range { - if let Some(execution) = db_tx.get::(i)? { - executions.push(execution); - } - } - - db_tx.commit()?; - Ok(Some(executions)) + if let Some(index) = self.block_body_indices(block_id)? { + let traces = self.transaction_executions_in_range(index.into())?; + Ok(Some(traces)) } else { Ok(None) } } + + fn transaction_executions_in_range( + &self, + range: Range, + ) -> ProviderResult> { + let db_tx = self.0.tx()?; + + let total = range.end - range.start; + let mut traces = Vec::with_capacity(total as usize); + + for i in range { + if let Some(trace) = db_tx.get::(i)? { + traces.push(trace); + } + } + + db_tx.commit()?; + Ok(traces) + } } impl ReceiptProvider for DbProvider { diff --git a/crates/katana/storage/provider/src/providers/fork/mod.rs b/crates/katana/storage/provider/src/providers/fork/mod.rs index 3fc82df95b..0e55e1b99d 100644 --- a/crates/katana/storage/provider/src/providers/fork/mod.rs +++ b/crates/katana/storage/provider/src/providers/fork/mod.rs @@ -332,7 +332,7 @@ impl TransactionTraceProvider for ForkedProvider { Ok(exec) } - fn transactions_executions_by_block( + fn transaction_executions_by_block( &self, block_id: BlockHashOrNumber, ) -> ProviderResult>> { @@ -341,26 +341,34 @@ impl TransactionTraceProvider for ForkedProvider { BlockHashOrNumber::Hash(hash) => self.storage.read().block_numbers.get(&hash).cloned(), }; - let Some(StoredBlockBodyIndices { tx_offset, tx_count }) = + let Some(index) = 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 traces = self.transaction_executions_in_range(index.into())?; + Ok(Some(traces)) + } + + fn transaction_executions_in_range( + &self, + range: Range, + ) -> ProviderResult> { + let start = range.start as usize; + let total = range.end as usize - start; - let execs = self + let traces = self .storage .read() .transactions_executions .iter() - .skip(offset) - .take(count) + .skip(start) + .take(total) .cloned() .collect(); - Ok(Some(execs)) + Ok(traces) } } 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 82b704cb29..f59d0ec94a 100644 --- a/crates/katana/storage/provider/src/providers/in_memory/mod.rs +++ b/crates/katana/storage/provider/src/providers/in_memory/mod.rs @@ -326,7 +326,7 @@ impl TransactionTraceProvider for InMemoryProvider { Ok(exec) } - fn transactions_executions_by_block( + fn transaction_executions_by_block( &self, block_id: BlockHashOrNumber, ) -> ProviderResult>> { @@ -335,26 +335,34 @@ impl TransactionTraceProvider for InMemoryProvider { BlockHashOrNumber::Hash(hash) => self.storage.read().block_numbers.get(&hash).cloned(), }; - let Some(StoredBlockBodyIndices { tx_offset, tx_count }) = + let Some(index) = 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 traces = self.transaction_executions_in_range(index.into())?; + Ok(Some(traces)) + } + + fn transaction_executions_in_range( + &self, + range: Range, + ) -> ProviderResult> { + let start = range.start as usize; + let total = range.end as usize - start; - let execs = self + let traces = self .storage .read() .transactions_executions .iter() - .skip(offset) - .take(count) + .skip(start) + .take(total) .cloned() .collect(); - Ok(Some(execs)) + Ok(traces) } } diff --git a/crates/katana/storage/provider/src/traits/transaction.rs b/crates/katana/storage/provider/src/traits/transaction.rs index 6054f0c3c0..ddc9b9bdc6 100644 --- a/crates/katana/storage/provider/src/traits/transaction.rs +++ b/crates/katana/storage/provider/src/traits/transaction.rs @@ -59,10 +59,16 @@ pub trait TransactionTraceProvider: Send + Sync { fn transaction_execution(&self, hash: TxHash) -> ProviderResult>; /// Returns all the transactions executions for a given block. - fn transactions_executions_by_block( + fn transaction_executions_by_block( &self, block_id: BlockHashOrNumber, ) -> ProviderResult>>; + + /// Retrieves the execution traces for the given range of tx numbers. + fn transaction_executions_in_range( + &self, + range: Range, + ) -> ProviderResult>; } #[auto_impl::auto_impl(&, Box, Arc)] diff --git a/crates/katana/storage/provider/tests/block.rs b/crates/katana/storage/provider/tests/block.rs index c2ff3b3a8b..2378ee3968 100644 --- a/crates/katana/storage/provider/tests/block.rs +++ b/crates/katana/storage/provider/tests/block.rs @@ -139,7 +139,7 @@ where let actual_block_tx_count = provider.transaction_count_by_block(block_id)?; let actual_receipts = provider.receipts_by_block(block_id)?; - let actual_executions = provider.transactions_executions_by_block(block_id)?; + let actual_executions = provider.transaction_executions_by_block(block_id)?; let expected_block_with_tx_hashes = BlockWithTxHashes { header: expected_block.header.clone(), @@ -244,7 +244,7 @@ where let actual_block_tx_count = provider.transaction_count_by_block(block_id)?; let actual_receipts = provider.receipts_by_block(block_id)?; - let actual_executions = provider.transactions_executions_by_block(block_id)?; + let actual_executions = provider.transaction_executions_by_block(block_id)?; let expected_block_with_tx_hashes = BlockWithTxHashes { header: expected_block.header.clone(), body: vec![] };