From 6f845a7e374ec8383775d6f3c40b394ed5758cc1 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 29 Oct 2024 15:09:30 -0400 Subject: [PATCH] feat(katana): fetch forked block data (#2592) Enable fetching block data from the forked chain. current forking feature only covers up to state data only. meaning doing any operations that requires only doing contract executions - estimate fee, tx execution - are allowed. This is a pretty simple solution as it just forwards the request to the forked network provider but doesn't do any caching of the requested data. --- crates/katana/core/src/backend/storage.rs | 11 +- crates/katana/node/src/lib.rs | 88 ++- crates/katana/rpc/rpc-api/src/starknet.rs | 4 +- .../katana/rpc/rpc-types-builder/src/block.rs | 6 +- crates/katana/rpc/rpc-types/src/block.rs | 42 +- .../rpc/rpc-types/src/error/starknet.rs | 67 +- crates/katana/rpc/rpc-types/src/receipt.rs | 8 +- .../katana/rpc/rpc-types/src/state_update.rs | 23 +- .../katana/rpc/rpc-types/src/transaction.rs | 6 + crates/katana/rpc/rpc/Cargo.toml | 3 +- crates/katana/rpc/rpc/src/starknet/forking.rs | 248 +++++++ crates/katana/rpc/rpc/src/starknet/mod.rs | 607 +++++++++++++++--- crates/katana/rpc/rpc/src/starknet/read.rs | 272 +------- crates/katana/rpc/rpc/src/starknet/trace.rs | 2 +- crates/katana/rpc/rpc/tests/common/mod.rs | 20 + crates/katana/rpc/rpc/tests/forking.rs | 387 +++++++++++ crates/katana/rpc/rpc/tests/starknet.rs | 21 +- 17 files changed, 1396 insertions(+), 419 deletions(-) create mode 100644 crates/katana/rpc/rpc/src/starknet/forking.rs create mode 100644 crates/katana/rpc/rpc/tests/forking.rs diff --git a/crates/katana/core/src/backend/storage.rs b/crates/katana/core/src/backend/storage.rs index 88ffd2a293..327c831375 100644 --- a/crates/katana/core/src/backend/storage.rs +++ b/crates/katana/core/src/backend/storage.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Context, Result}; use katana_db::mdbx::DbEnv; use katana_primitives::block::{ - BlockHashOrNumber, BlockIdOrTag, FinalityStatus, SealedBlockWithStatus, + BlockHashOrNumber, BlockIdOrTag, BlockNumber, FinalityStatus, SealedBlockWithStatus, }; use katana_primitives::chain_spec::ChainSpec; use katana_primitives::da::L1DataAvailabilityMode; @@ -119,7 +119,7 @@ impl Blockchain { fork_url: Url, fork_block: Option, chain: &mut ChainSpec, - ) -> Result { + ) -> Result<(Self, BlockNumber)> { let provider = JsonRpcClient::new(HttpTransport::new(fork_url)); let chain_id = provider.chain_id().await.context("failed to fetch forked network id")?; @@ -149,6 +149,8 @@ impl Blockchain { bail!("forking a pending block is not allowed") }; + let block_num = forked_block.block_number; + chain.id = chain_id.into(); chain.version = ProtocolVersion::parse(&forked_block.starknet_version)?; @@ -172,6 +174,8 @@ impl Blockchain { _ => bail!("qed; block status shouldn't be pending"), }; + // TODO: convert this to block number instead of BlockHashOrNumber so that it is easier to + // check if the requested block is within the supported range or not. let database = ForkedProvider::new(Arc::new(provider), block_id)?; // update the genesis block with the forked block's data @@ -193,7 +197,8 @@ impl Blockchain { let block = block.seal_with_hash_and_status(forked_block.block_hash, status); let state_updates = chain.state_updates(); - Self::new_with_genesis_block_and_state(database, block, state_updates) + let blockchain = Self::new_with_genesis_block_and_state(database, block, state_updates)?; + Ok((blockchain, block_num)) } pub fn provider(&self) -> &BlockchainProvider> { diff --git a/crates/katana/node/src/lib.rs b/crates/katana/node/src/lib.rs index 9e26ecb01d..309fedf51d 100644 --- a/crates/katana/node/src/lib.rs +++ b/crates/katana/node/src/lib.rs @@ -35,6 +35,7 @@ use katana_primitives::env::{CfgEnv, FeeTokenAddressses}; use katana_rpc::dev::DevApi; use katana_rpc::metrics::RpcServerMetrics; use katana_rpc::saya::SayaApi; +use katana_rpc::starknet::forking::ForkedClient; use katana_rpc::starknet::StarknetApi; use katana_rpc::torii::ToriiApi; use katana_rpc_api::dev::DevApiServer; @@ -87,13 +88,14 @@ pub struct Node { pub metrics_config: Option, pub sequencing_config: SequencingConfig, pub messaging_config: Option, + forked_client: Option, } impl Node { /// Start the node. /// /// This method will start all the node process, running them until the node is stopped. - pub async fn launch(self) -> Result { + pub async fn launch(mut self) -> Result { let chain = self.backend.chain_spec.id; info!(%chain, "Starting node."); @@ -139,7 +141,7 @@ impl Node { .name("Pipeline") .spawn(pipeline.into_future()); - let node_components = (pool, backend, block_producer, validator); + let node_components = (pool, backend, block_producer, validator, self.forked_client.take()); let rpc = spawn(node_components, self.rpc_config.clone()).await?; Ok(LaunchedNode { node: self, rpc }) @@ -178,15 +180,20 @@ pub async fn build(mut config: Config) -> Result { // --- build backend - let (blockchain, db) = if let Some(cfg) = config.forking { - let bc = Blockchain::new_from_forked(cfg.url.clone(), cfg.block, &mut config.chain).await?; - (bc, None) + let (blockchain, db, forked_client) = if let Some(cfg) = &config.forking { + let (bc, block_num) = + Blockchain::new_from_forked(cfg.url.clone(), cfg.block, &mut config.chain).await?; + + // TODO: it'd bee nice if the client can be shared on both the rpc and forked backend side + let forked_client = ForkedClient::new_http(cfg.url.clone(), block_num); + + (bc, None, Some(forked_client)) } else if let Some(db_path) = &config.db.dir { let db = katana_db::init_db(db_path)?; - (Blockchain::new_with_db(db.clone(), &config.chain)?, Some(db)) + (Blockchain::new_with_db(db.clone(), &config.chain)?, Some(db), None) } else { let db = katana_db::init_ephemeral_db()?; - (Blockchain::new_with_db(db.clone(), &config.chain)?, Some(db)) + (Blockchain::new_with_db(db.clone(), &config.chain)?, Some(db), None) }; let block_context_generator = BlockContextGenerator::default().into(); @@ -218,6 +225,7 @@ pub async fn build(mut config: Config) -> Result { db, pool, backend, + forked_client, block_producer, rpc_config: config.rpc, metrics_config: config.metrics, @@ -231,40 +239,50 @@ pub async fn build(mut config: Config) -> Result { // Moved from `katana_rpc` crate pub async fn spawn( - node_components: (TxPool, Arc>, BlockProducer, TxValidator), + node_components: ( + TxPool, + Arc>, + BlockProducer, + TxValidator, + Option, + ), config: RpcConfig, ) -> Result { - let (pool, backend, block_producer, validator) = node_components; + let (pool, backend, block_producer, validator, forked_client) = node_components; let mut methods = RpcModule::new(()); methods.register_method("health", |_, _| Ok(serde_json::json!({ "health": true })))?; - for api in &config.apis { - match api { - ApiKind::Starknet => { - // TODO: merge these into a single logic. - let server = StarknetApi::new( - backend.clone(), - pool.clone(), - block_producer.clone(), - validator.clone(), - ); - methods.merge(StarknetApiServer::into_rpc(server.clone()))?; - methods.merge(StarknetWriteApiServer::into_rpc(server.clone()))?; - methods.merge(StarknetTraceApiServer::into_rpc(server))?; - } - ApiKind::Dev => { - methods.merge(DevApi::new(backend.clone(), block_producer.clone()).into_rpc())?; - } - ApiKind::Torii => { - methods.merge( - ToriiApi::new(backend.clone(), pool.clone(), block_producer.clone()).into_rpc(), - )?; - } - ApiKind::Saya => { - methods.merge(SayaApi::new(backend.clone(), block_producer.clone()).into_rpc())?; - } - } + if config.apis.contains(&ApiKind::Starknet) { + let server = if let Some(client) = forked_client { + StarknetApi::new_forked( + backend.clone(), + pool.clone(), + block_producer.clone(), + validator, + client, + ) + } else { + StarknetApi::new(backend.clone(), pool.clone(), block_producer.clone(), validator) + }; + + methods.merge(StarknetApiServer::into_rpc(server.clone()))?; + methods.merge(StarknetWriteApiServer::into_rpc(server.clone()))?; + methods.merge(StarknetTraceApiServer::into_rpc(server))?; + } + + if config.apis.contains(&ApiKind::Dev) { + methods.merge(DevApi::new(backend.clone(), block_producer.clone()).into_rpc())?; + } + + if config.apis.contains(&ApiKind::Torii) { + methods.merge( + ToriiApi::new(backend.clone(), pool.clone(), block_producer.clone()).into_rpc(), + )?; + } + + if config.apis.contains(&ApiKind::Saya) { + methods.merge(SayaApi::new(backend.clone(), block_producer.clone()).into_rpc())?; } let cors = CorsLayer::new() diff --git a/crates/katana/rpc/rpc-api/src/starknet.rs b/crates/katana/rpc/rpc-api/src/starknet.rs index 844db5eb4f..85aaebc8fc 100644 --- a/crates/katana/rpc/rpc-api/src/starknet.rs +++ b/crates/katana/rpc/rpc-api/src/starknet.rs @@ -12,7 +12,7 @@ use katana_rpc_types::block::{ use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; use katana_rpc_types::message::MsgFromL1; use katana_rpc_types::receipt::TxReceiptWithBlockInfo; -use katana_rpc_types::state_update::StateUpdate; +use katana_rpc_types::state_update::MaybePendingStateUpdate; use katana_rpc_types::transaction::{ BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, BroadcastedTx, DeclareTxResult, DeployAccountTxResult, InvokeTxResult, Tx, @@ -61,7 +61,7 @@ pub trait StarknetApi { /// Get the information about the result of executing the requested block. #[method(name = "getStateUpdate")] - async fn get_state_update(&self, block_id: BlockIdOrTag) -> RpcResult; + async fn get_state_update(&self, block_id: BlockIdOrTag) -> RpcResult; /// Get the value of the storage at the given address and key #[method(name = "getStorageAt")] diff --git a/crates/katana/rpc/rpc-types-builder/src/block.rs b/crates/katana/rpc/rpc-types-builder/src/block.rs index 53638b1efa..d7d8b7aeb9 100644 --- a/crates/katana/rpc/rpc-types-builder/src/block.rs +++ b/crates/katana/rpc/rpc-types-builder/src/block.rs @@ -48,10 +48,12 @@ where } pub fn build_with_receipts(self) -> ProviderResult> { - let Some(block) = BlockProvider::block(&self.provider, self.block_id)? else { + let Some(hash) = BlockHashProvider::block_hash_by_id(&self.provider, self.block_id)? else { return Ok(None); }; + let block = BlockProvider::block(&self.provider, self.block_id)? + .expect("should exist if block exists"); let finality_status = BlockStatusProvider::block_status(&self.provider, self.block_id)? .expect("should exist if block exists"); let receipts = ReceiptProvider::receipts_by_block(&self.provider, self.block_id)? @@ -59,6 +61,6 @@ where let receipts_with_txs = block.body.into_iter().zip(receipts); - Ok(Some(BlockWithReceipts::new(block.header, finality_status, receipts_with_txs))) + Ok(Some(BlockWithReceipts::new(hash, block.header, finality_status, receipts_with_txs))) } } diff --git a/crates/katana/rpc/rpc-types/src/block.rs b/crates/katana/rpc/rpc-types/src/block.rs index b8cb53fafd..1a935f4747 100644 --- a/crates/katana/rpc/rpc-types/src/block.rs +++ b/crates/katana/rpc/rpc-types/src/block.rs @@ -95,6 +95,19 @@ pub enum MaybePendingBlockWithTxs { Block(BlockWithTxs), } +impl From for MaybePendingBlockWithTxs { + fn from(value: starknet::core::types::MaybePendingBlockWithTxs) -> Self { + match value { + starknet::core::types::MaybePendingBlockWithTxs::PendingBlock(block) => { + MaybePendingBlockWithTxs::Pending(PendingBlockWithTxs(block)) + } + starknet::core::types::MaybePendingBlockWithTxs::Block(block) => { + MaybePendingBlockWithTxs::Block(BlockWithTxs(block)) + } + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct BlockWithTxHashes(starknet::core::types::BlockWithTxHashes); @@ -181,6 +194,19 @@ pub enum MaybePendingBlockWithTxHashes { Block(BlockWithTxHashes), } +impl From for MaybePendingBlockWithTxHashes { + fn from(value: starknet::core::types::MaybePendingBlockWithTxHashes) -> Self { + match value { + starknet::core::types::MaybePendingBlockWithTxHashes::PendingBlock(block) => { + MaybePendingBlockWithTxHashes::Pending(PendingBlockWithTxHashes(block)) + } + starknet::core::types::MaybePendingBlockWithTxHashes::Block(block) => { + MaybePendingBlockWithTxHashes::Block(BlockWithTxHashes(block)) + } + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct BlockHashAndNumber(starknet::core::types::BlockHashAndNumber); @@ -203,6 +229,7 @@ pub struct BlockWithReceipts(starknet::core::types::BlockWithReceipts); impl BlockWithReceipts { pub fn new( + hash: BlockHash, header: Header, finality_status: FinalityStatus, receipts: impl Iterator, @@ -230,7 +257,7 @@ impl BlockWithReceipts { FinalityStatus::AcceptedOnL1 => BlockStatus::AcceptedOnL1, FinalityStatus::AcceptedOnL2 => BlockStatus::AcceptedOnL2, }, - block_hash: header.parent_hash, + block_hash: hash, parent_hash: header.parent_hash, block_number: header.number, new_root: header.state_root, @@ -297,3 +324,16 @@ pub enum MaybePendingBlockWithReceipts { Pending(PendingBlockWithReceipts), Block(BlockWithReceipts), } + +impl From for MaybePendingBlockWithReceipts { + fn from(value: starknet::core::types::MaybePendingBlockWithReceipts) -> Self { + match value { + starknet::core::types::MaybePendingBlockWithReceipts::PendingBlock(block) => { + MaybePendingBlockWithReceipts::Pending(PendingBlockWithReceipts(block)) + } + starknet::core::types::MaybePendingBlockWithReceipts::Block(block) => { + MaybePendingBlockWithReceipts::Block(BlockWithReceipts(block)) + } + } + } +} diff --git a/crates/katana/rpc/rpc-types/src/error/starknet.rs b/crates/katana/rpc/rpc-types/src/error/starknet.rs index 614184d681..0848efa999 100644 --- a/crates/katana/rpc/rpc-types/src/error/starknet.rs +++ b/crates/katana/rpc/rpc-types/src/error/starknet.rs @@ -7,6 +7,8 @@ use katana_primitives::event::ContinuationTokenError; use katana_provider::error::ProviderError; use serde::Serialize; use serde_json::Value; +use starknet::core::types::StarknetError as StarknetRsError; +use starknet::providers::ProviderError as StarknetRsProviderError; /// Possible list of errors that can be returned by the Starknet API according to the spec: . #[derive(Debug, thiserror::Error, Clone, Serialize)] @@ -40,7 +42,7 @@ pub enum StarknetApiError { #[error("Transaction execution error")] TransactionExecutionError { /// The index of the first transaction failing in a sequence of given transactions. - transaction_index: usize, + transaction_index: u64, /// The revert error with the execution trace up to the point of failure. execution_error: String, }, @@ -195,6 +197,69 @@ impl From> for StarknetApiError { } } +// ---- Forking client error conversion + +impl From for StarknetApiError { + fn from(value: StarknetRsError) -> Self { + match value { + StarknetRsError::FailedToReceiveTransaction => Self::FailedToReceiveTxn, + StarknetRsError::NoBlocks => Self::NoBlocks, + StarknetRsError::NonAccount => Self::NonAccount, + StarknetRsError::BlockNotFound => Self::BlockNotFound, + StarknetRsError::PageSizeTooBig => Self::PageSizeTooBig, + StarknetRsError::DuplicateTx => Self::DuplicateTransaction, + StarknetRsError::ContractNotFound => Self::ContractNotFound, + StarknetRsError::CompilationFailed => Self::CompilationFailed, + StarknetRsError::ClassHashNotFound => Self::ClassHashNotFound, + StarknetRsError::InsufficientMaxFee => Self::InsufficientMaxFee, + StarknetRsError::TooManyKeysInFilter => Self::TooManyKeysInFilter, + StarknetRsError::InvalidTransactionIndex => Self::InvalidTxnIndex, + StarknetRsError::TransactionHashNotFound => Self::TxnHashNotFound, + StarknetRsError::ClassAlreadyDeclared => Self::ClassAlreadyDeclared, + StarknetRsError::UnexpectedError(reason) => Self::UnexpectedError { reason }, + StarknetRsError::InvalidContinuationToken => Self::InvalidContinuationToken, + StarknetRsError::UnsupportedTxVersion => Self::UnsupportedTransactionVersion, + StarknetRsError::CompiledClassHashMismatch => Self::CompiledClassHashMismatch, + StarknetRsError::InsufficientAccountBalance => Self::InsufficientAccountBalance, + StarknetRsError::ValidationFailure(reason) => Self::ValidationFailure { reason }, + StarknetRsError::ContractClassSizeIsTooLarge => Self::ContractClassSizeIsTooLarge, + StarknetRsError::ContractError(data) => { + Self::ContractError { revert_error: data.revert_error } + } + StarknetRsError::TransactionExecutionError(data) => Self::TransactionExecutionError { + execution_error: data.execution_error, + transaction_index: data.transaction_index, + }, + StarknetRsError::InvalidTransactionNonce => { + Self::InvalidTransactionNonce { reason: "".to_string() } + } + StarknetRsError::UnsupportedContractClassVersion => { + Self::UnsupportedContractClassVersion + } + StarknetRsError::NoTraceAvailable(_) => { + Self::UnexpectedError { reason: "No trace available".to_string() } + } + } + } +} + +impl From for StarknetApiError { + fn from(value: StarknetRsProviderError) -> Self { + match value { + StarknetRsProviderError::StarknetError(error) => error.into(), + StarknetRsProviderError::Other(error) => { + Self::UnexpectedError { reason: error.to_string() } + } + StarknetRsProviderError::ArrayLengthMismatch { .. } => Self::UnexpectedError { + reason: "Forking client: Array length mismatch".to_string(), + }, + StarknetRsProviderError::RateLimited { .. } => { + Self::UnexpectedError { reason: "Forking client: Rate limited".to_string() } + } + } + } +} + #[cfg(test)] mod tests { use rstest::rstest; diff --git a/crates/katana/rpc/rpc-types/src/receipt.rs b/crates/katana/rpc/rpc-types/src/receipt.rs index 17dfe5df53..ec09178f69 100644 --- a/crates/katana/rpc/rpc-types/src/receipt.rs +++ b/crates/katana/rpc/rpc-types/src/receipt.rs @@ -119,7 +119,13 @@ impl TxReceipt { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] -pub struct TxReceiptWithBlockInfo(starknet::core::types::TransactionReceiptWithBlockInfo); +pub struct TxReceiptWithBlockInfo(pub starknet::core::types::TransactionReceiptWithBlockInfo); + +impl From for TxReceiptWithBlockInfo { + fn from(value: starknet::core::types::TransactionReceiptWithBlockInfo) -> Self { + Self(value) + } +} impl TxReceiptWithBlockInfo { pub fn new( diff --git a/crates/katana/rpc/rpc-types/src/state_update.rs b/crates/katana/rpc/rpc-types/src/state_update.rs index 6fb1a2bbb6..d4248b1a1f 100644 --- a/crates/katana/rpc/rpc-types/src/state_update.rs +++ b/crates/katana/rpc/rpc-types/src/state_update.rs @@ -10,6 +10,19 @@ pub enum MaybePendingStateUpdate { Update(StateUpdate), } +impl From for MaybePendingStateUpdate { + fn from(value: starknet::core::types::MaybePendingStateUpdate) -> Self { + match value { + starknet::core::types::MaybePendingStateUpdate::PendingUpdate(pending) => { + MaybePendingStateUpdate::Pending(pending.into()) + } + starknet::core::types::MaybePendingStateUpdate::Update(update) => { + MaybePendingStateUpdate::Update(update.into()) + } + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct StateUpdate(starknet::core::types::StateUpdate); @@ -24,7 +37,7 @@ pub struct StateDiff(pub starknet::core::types::StateDiff); impl From for StateUpdate { fn from(value: starknet::core::types::StateUpdate) -> Self { - StateUpdate(value) + Self(value) } } @@ -63,7 +76,7 @@ impl From for StateDiff { }) .collect(); - StateDiff(starknet::core::types::StateDiff { + Self(starknet::core::types::StateDiff { nonces, storage_diffs, declared_classes, @@ -73,3 +86,9 @@ impl From for StateDiff { }) } } + +impl From for PendingStateUpdate { + fn from(value: starknet::core::types::PendingStateUpdate) -> Self { + Self(value) + } +} diff --git a/crates/katana/rpc/rpc-types/src/transaction.rs b/crates/katana/rpc/rpc-types/src/transaction.rs index 4c466846fd..9af3598d80 100644 --- a/crates/katana/rpc/rpc-types/src/transaction.rs +++ b/crates/katana/rpc/rpc-types/src/transaction.rs @@ -397,6 +397,12 @@ impl From for Tx { } } +impl From for Tx { + fn from(value: starknet::core::types::Transaction) -> Self { + Self(value) + } +} + impl DeployAccountTxResult { pub fn new(transaction_hash: TxHash, contract_address: ContractAddress) -> Self { Self(DeployAccountTransactionResult { diff --git a/crates/katana/rpc/rpc/Cargo.toml b/crates/katana/rpc/rpc/Cargo.toml index cfd8b99c52..4f5cd75c30 100644 --- a/crates/katana/rpc/rpc/Cargo.toml +++ b/crates/katana/rpc/rpc/Cargo.toml @@ -23,7 +23,9 @@ katana-tasks.workspace = true metrics.workspace = true starknet.workspace = true thiserror.workspace = true +tokio.workspace = true tracing.workspace = true +url.workspace = true [dev-dependencies] alloy = { git = "https://github.com/alloy-rs/alloy", features = [ "contract", "network", "node-bindings", "provider-http", "providers", "signer-local" ] } @@ -45,4 +47,3 @@ serde.workspace = true serde_json.workspace = true tempfile.workspace = true tokio.workspace = true -url.workspace = true diff --git a/crates/katana/rpc/rpc/src/starknet/forking.rs b/crates/katana/rpc/rpc/src/starknet/forking.rs new file mode 100644 index 0000000000..e15dbfdc33 --- /dev/null +++ b/crates/katana/rpc/rpc/src/starknet/forking.rs @@ -0,0 +1,248 @@ +use katana_primitives::block::{BlockIdOrTag, BlockNumber}; +use katana_primitives::transaction::TxHash; +use katana_rpc_types::block::{ + MaybePendingBlockWithReceipts, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, +}; +use katana_rpc_types::error::starknet::StarknetApiError; +use katana_rpc_types::receipt::TxReceiptWithBlockInfo; +use katana_rpc_types::state_update::MaybePendingStateUpdate; +use katana_rpc_types::transaction::Tx; +use starknet::core::types::TransactionStatus; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{JsonRpcClient, Provider, ProviderError}; +use url::Url; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Error originating from the underlying [`Provider`] implementation. + #[error("Provider error: {0}")] + Provider(#[from] ProviderError), + + #[error("Block out of range")] + BlockOutOfRange, +} + +#[derive(Debug)] +pub struct ForkedClient> { + /// The block number where the node is forked from. + block: BlockNumber, + /// The Starknet Json RPC provider client for doing the request to the forked network. + provider: P, +} + +impl ForkedClient

{ + /// Creates a new forked client from the given [`Provider`] and block number. + pub fn new(provider: P, block: BlockNumber) -> Self { + Self { provider, block } + } + + /// Returns the block number of the forked client. + pub fn block(&self) -> &BlockNumber { + &self.block + } +} + +impl ForkedClient { + /// Creates a new forked client from the given HTTP URL and block number. + pub fn new_http(url: Url, block: BlockNumber) -> Self { + Self { provider: JsonRpcClient::new(HttpTransport::new(url)), block } + } +} + +impl ForkedClient

{ + pub async fn get_transaction_by_hash(&self, hash: TxHash) -> Result { + let tx = self.provider.get_transaction_by_hash(hash).await?; + Ok(tx.into()) + } + + pub async fn get_transaction_receipt( + &self, + hash: TxHash, + ) -> Result { + let receipt = self.provider.get_transaction_receipt(hash).await?; + + if let starknet::core::types::ReceiptBlock::Block { block_number, .. } = receipt.block { + if block_number > self.block { + return Err(Error::BlockOutOfRange); + } + } + + Ok(receipt.into()) + } + + pub async fn get_transaction_status(&self, hash: TxHash) -> Result { + let (receipt, status) = tokio::join!( + self.get_transaction_receipt(hash), + self.provider.get_transaction_status(hash) + ); + + // We get the receipt first to check if the block number is within the forked range. + let _ = receipt?; + + Ok(status?) + } + + pub async fn get_transaction_by_block_id_and_index( + &self, + block_id: BlockIdOrTag, + idx: u64, + ) -> Result { + match block_id { + BlockIdOrTag::Number(num) => { + if num > self.block { + return Err(Error::BlockOutOfRange); + } + + let tx = self.provider.get_transaction_by_block_id_and_index(block_id, idx).await?; + Ok(tx.into()) + } + + BlockIdOrTag::Hash(hash) => { + let (block, tx) = tokio::join!( + self.provider.get_block_with_tx_hashes(BlockIdOrTag::Hash(hash)), + self.provider.get_transaction_by_block_id_and_index(block_id, idx) + ); + + let number = match block? { + starknet::core::types::MaybePendingBlockWithTxHashes::Block(block) => { + block.block_number + } + starknet::core::types::MaybePendingBlockWithTxHashes::PendingBlock(_) => { + panic!("shouldn't be possible to be pending") + } + }; + + if number > self.block { + return Err(Error::BlockOutOfRange); + } + + Ok(tx?.into()) + } + + BlockIdOrTag::Tag(_) => { + panic!("shouldn't be possible to be tag") + } + } + } + + pub async fn get_block_with_txs( + &self, + block_id: BlockIdOrTag, + ) -> Result { + let block = self.provider.get_block_with_txs(block_id).await?; + + match block { + starknet::core::types::MaybePendingBlockWithTxs::Block(ref b) => { + if b.block_number > self.block { + Err(Error::BlockOutOfRange) + } else { + Ok(block.into()) + } + } + + starknet::core::types::MaybePendingBlockWithTxs::PendingBlock(_) => { + panic!("shouldn't be possible to be pending") + } + } + } + + pub async fn get_block_with_receipts( + &self, + block_id: BlockIdOrTag, + ) -> Result { + let block = self.provider.get_block_with_receipts(block_id).await?; + + match block { + starknet::core::types::MaybePendingBlockWithReceipts::Block(ref b) => { + if b.block_number > self.block { + return Err(Error::BlockOutOfRange); + } + } + starknet::core::types::MaybePendingBlockWithReceipts::PendingBlock(_) => { + panic!("shouldn't be possible to be pending") + } + } + + Ok(block.into()) + } + + pub async fn get_block_with_tx_hashes( + &self, + block_id: BlockIdOrTag, + ) -> Result { + let block = self.provider.get_block_with_tx_hashes(block_id).await?; + + match block { + starknet::core::types::MaybePendingBlockWithTxHashes::Block(ref b) => { + if b.block_number > self.block { + return Err(Error::BlockOutOfRange); + } + } + starknet::core::types::MaybePendingBlockWithTxHashes::PendingBlock(_) => { + panic!("shouldn't be possible to be pending") + } + } + + Ok(block.into()) + } + + pub async fn get_block_transaction_count(&self, block_id: BlockIdOrTag) -> Result { + match block_id { + BlockIdOrTag::Number(num) if num > self.block => { + return Err(Error::BlockOutOfRange); + } + BlockIdOrTag::Hash(hash) => { + let block = + self.provider.get_block_with_tx_hashes(BlockIdOrTag::Hash(hash)).await?; + if let starknet::core::types::MaybePendingBlockWithTxHashes::Block(b) = block { + if b.block_number > self.block { + return Err(Error::BlockOutOfRange); + } + } + } + BlockIdOrTag::Tag(_) => { + panic!("shouldn't be possible to be tag") + } + _ => {} + } + + let status = self.provider.get_block_transaction_count(block_id).await?; + Ok(status) + } + + pub async fn get_state_update( + &self, + block_id: BlockIdOrTag, + ) -> Result { + match block_id { + BlockIdOrTag::Number(num) if num > self.block => { + return Err(Error::BlockOutOfRange); + } + BlockIdOrTag::Hash(hash) => { + let block = + self.provider.get_block_with_tx_hashes(BlockIdOrTag::Hash(hash)).await?; + if let starknet::core::types::MaybePendingBlockWithTxHashes::Block(b) = block { + if b.block_number > self.block { + return Err(Error::BlockOutOfRange); + } + } + } + BlockIdOrTag::Tag(_) => { + panic!("shouldn't be possible to be tag") + } + _ => {} + } + + let state_update = self.provider.get_state_update(block_id).await?; + Ok(state_update.into()) + } +} + +impl From for StarknetApiError { + fn from(value: Error) -> Self { + match value { + Error::Provider(provider_error) => provider_error.into(), + Error::BlockOutOfRange => StarknetApiError::BlockNotFound, + } + } +} diff --git a/crates/katana/rpc/rpc/src/starknet/mod.rs b/crates/katana/rpc/rpc/src/starknet/mod.rs index 7ae5b0edca..b29f2da609 100644 --- a/crates/katana/rpc/rpc/src/starknet/mod.rs +++ b/crates/katana/rpc/rpc/src/starknet/mod.rs @@ -1,11 +1,13 @@ //! Server implementation for the Starknet JSON-RPC API. +pub mod forking; mod read; mod trace; mod write; use std::sync::Arc; +use forking::ForkedClient; use katana_core::backend::Backend; use katana_core::service::block_producer::{BlockProducer, BlockProducerMode, PendingExecutor}; use katana_executor::{ExecutionResult, ExecutorFactory}; @@ -13,13 +15,15 @@ use katana_pool::validation::stateful::TxValidator; use katana_pool::TxPool; use katana_primitives::block::{ BlockHash, BlockHashOrNumber, BlockIdOrTag, BlockNumber, BlockTag, FinalityStatus, + PartialHeader, }; use katana_primitives::class::{ClassHash, CompiledClass}; use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; use katana_primitives::conversion::rpc::legacy_inner_to_rpc_class; +use katana_primitives::da::L1DataAvailabilityMode; use katana_primitives::env::BlockEnv; use katana_primitives::event::ContinuationToken; -use katana_primitives::transaction::{ExecutableTxWithHash, TxHash, TxWithHash}; +use katana_primitives::transaction::{ExecutableTxWithHash, TxHash}; use katana_primitives::Felt; use katana_provider::traits::block::{BlockHashProvider, BlockIdReader, BlockNumberProvider}; use katana_provider::traits::contract::ContractClassProvider; @@ -28,8 +32,16 @@ use katana_provider::traits::state::{StateFactoryProvider, StateProvider}; use katana_provider::traits::transaction::{ ReceiptProvider, TransactionProvider, TransactionStatusProvider, }; +use katana_rpc_types::block::{ + MaybePendingBlockWithReceipts, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, + PendingBlockWithReceipts, PendingBlockWithTxHashes, PendingBlockWithTxs, +}; use katana_rpc_types::error::starknet::StarknetApiError; +use katana_rpc_types::receipt::{ReceiptBlock, TxReceiptWithBlockInfo}; +use katana_rpc_types::state_update::MaybePendingStateUpdate; +use katana_rpc_types::transaction::Tx; use katana_rpc_types::FeeEstimate; +use katana_rpc_types_builder::ReceiptBuilder; use katana_tasks::{BlockingTaskPool, TokioTaskSpawner}; use starknet::core::types::{ ContractClass, EventsPage, PriceUnit, TransactionExecutionStatus, TransactionStatus, @@ -38,6 +50,8 @@ use starknet::core::types::{ use crate::utils; use crate::utils::events::{Cursor, EventBlockId}; +pub type StarknetApiResult = Result; + #[allow(missing_debug_implementations)] pub struct StarknetApi { inner: Arc>, @@ -55,6 +69,7 @@ struct Inner { backend: Arc>, block_producer: BlockProducer, blocking_task_pool: BlockingTaskPool, + forked_client: Option, } impl StarknetApi { @@ -64,11 +79,30 @@ impl StarknetApi { block_producer: BlockProducer, validator: TxValidator, ) -> Self { - let blocking_task_pool = - BlockingTaskPool::new().expect("failed to create blocking task pool"); + Self::new_inner(backend, pool, block_producer, validator, None) + } - let inner = Inner { pool, backend, block_producer, blocking_task_pool, validator }; + pub fn new_forked( + backend: Arc>, + pool: TxPool, + block_producer: BlockProducer, + validator: TxValidator, + forked_client: ForkedClient, + ) -> Self { + Self::new_inner(backend, pool, block_producer, validator, Some(forked_client)) + } + fn new_inner( + backend: Arc>, + pool: TxPool, + block_producer: BlockProducer, + validator: TxValidator, + forked_client: Option, + ) -> Self { + let blocking_task_pool = + BlockingTaskPool::new().expect("failed to create blocking task pool"); + let inner = + Inner { pool, backend, block_producer, blocking_task_pool, validator, forked_client }; Self { inner: Arc::new(inner) } } @@ -95,7 +129,7 @@ impl StarknetApi { transactions: Vec, block_id: BlockIdOrTag, flags: katana_executor::ExecutionFlags, - ) -> Result, StarknetApiError> { + ) -> StarknetApiResult> { // get the state and block env at the specified block for execution let state = self.state(&block_id)?; let env = self.block_env_at(&block_id)?; @@ -121,7 +155,7 @@ impl StarknetApi { Err(err) => { return Err(StarknetApiError::TransactionExecutionError { - transaction_index: i, + transaction_index: i as u64, execution_error: err.to_string(), }); } @@ -139,7 +173,7 @@ impl StarknetApi { } } - fn state(&self, block_id: &BlockIdOrTag) -> Result, StarknetApiError> { + fn state(&self, block_id: &BlockIdOrTag) -> StarknetApiResult> { let provider = self.inner.backend.blockchain.provider(); let state = match block_id { @@ -160,7 +194,7 @@ impl StarknetApi { state.ok_or(StarknetApiError::BlockNotFound) } - fn block_env_at(&self, block_id: &BlockIdOrTag) -> Result { + fn block_env_at(&self, block_id: &BlockIdOrTag) -> StarknetApiResult { let provider = self.inner.backend.blockchain.provider(); let env = match block_id { @@ -185,7 +219,7 @@ impl StarknetApi { env.ok_or(StarknetApiError::BlockNotFound) } - fn block_hash_and_number(&self) -> Result<(BlockHash, BlockNumber), StarknetApiError> { + fn block_hash_and_number(&self) -> StarknetApiResult<(BlockHash, BlockNumber)> { let provider = self.inner.backend.blockchain.provider(); let hash = provider.latest_hash()?; let number = provider.latest_number()?; @@ -196,7 +230,7 @@ impl StarknetApi { &self, block_id: BlockIdOrTag, class_hash: ClassHash, - ) -> Result { + ) -> StarknetApiResult { self.on_io_blocking_task(move |this| { let state = this.state(&block_id)?; @@ -224,7 +258,7 @@ impl StarknetApi { &self, block_id: BlockIdOrTag, contract_address: ContractAddress, - ) -> Result { + ) -> StarknetApiResult { self.on_io_blocking_task(move |this| { let state = this.state(&block_id)?; let class_hash = state.class_hash_of_contract(contract_address)?; @@ -237,7 +271,7 @@ impl StarknetApi { &self, block_id: BlockIdOrTag, contract_address: ContractAddress, - ) -> Result { + ) -> StarknetApiResult { let hash = self.class_hash_at_address(block_id, contract_address).await?; let class = self.class_at_hash(block_id, hash).await?; Ok(class) @@ -248,7 +282,7 @@ impl StarknetApi { contract_address: ContractAddress, storage_key: StorageKey, block_id: BlockIdOrTag, - ) -> Result { + ) -> StarknetApiResult { let state = self.state(&block_id)?; // check that contract exist by checking the class hash of the contract @@ -260,27 +294,40 @@ impl StarknetApi { Ok(value.unwrap_or_default()) } - fn block_tx_count(&self, block_id: BlockIdOrTag) -> Result { - let provider = self.inner.backend.blockchain.provider(); + async fn block_tx_count(&self, block_id: BlockIdOrTag) -> StarknetApiResult { + let count = self + .on_io_blocking_task(move |this| { + let provider = this.inner.backend.blockchain.provider(); - let block_id: BlockHashOrNumber = match block_id { - BlockIdOrTag::Tag(BlockTag::Pending) => match self.pending_executor() { - Some(exec) => return Ok(exec.read().transactions().len() as u64), - None => provider.latest_hash()?.into(), - }, - BlockIdOrTag::Tag(BlockTag::Latest) => provider.latest_number()?.into(), - BlockIdOrTag::Number(num) => num.into(), - BlockIdOrTag::Hash(hash) => hash.into(), - }; + let block_id: BlockHashOrNumber = match block_id { + BlockIdOrTag::Tag(BlockTag::Pending) => match this.pending_executor() { + Some(exec) => { + let count = exec.read().transactions().len() as u64; + return Ok(Some(count)); + } + None => provider.latest_hash()?.into(), + }, + BlockIdOrTag::Tag(BlockTag::Latest) => provider.latest_number()?.into(), + BlockIdOrTag::Number(num) => num.into(), + BlockIdOrTag::Hash(hash) => hash.into(), + }; - let count = provider - .transaction_count_by_block(block_id)? - .ok_or(StarknetApiError::BlockNotFound)?; + let count = provider.transaction_count_by_block(block_id)?; + Result::<_, StarknetApiError>::Ok(count) + }) + .await?; - Ok(count) + if let Some(count) = count { + Ok(count) + } else if let Some(client) = &self.inner.forked_client { + let status = client.get_block_transaction_count(block_id).await?; + Ok(status) + } else { + Err(StarknetApiError::BlockNotFound) + } } - async fn latest_block_number(&self) -> Result { + async fn latest_block_number(&self) -> StarknetApiResult { self.on_io_blocking_task(move |this| { Ok(this.inner.backend.blockchain.provider().latest_number()?) }) @@ -291,7 +338,7 @@ impl StarknetApi { &self, block_id: BlockIdOrTag, contract_address: ContractAddress, - ) -> Result { + ) -> StarknetApiResult { self.on_io_blocking_task(move |this| { // read from the pool state if pending block // @@ -310,27 +357,137 @@ impl StarknetApi { .await } - async fn transaction(&self, hash: TxHash) -> Result { - self.on_io_blocking_task(move |this| { - let tx = this.inner.backend.blockchain.provider().transaction_by_hash(hash)?; - - let tx = match tx { - tx @ Some(_) => tx, - None => { - // check if the transaction is in the pending block - this.pending_executor().as_ref().and_then(|exec| { - exec.read() - .transactions() - .iter() - .find(|(tx, _)| tx.hash == hash) - .map(|(tx, _)| tx.clone()) - }) + async fn transaction_by_block_id_and_index( + &self, + block_id: BlockIdOrTag, + index: u64, + ) -> StarknetApiResult { + let tx = self + .on_io_blocking_task(move |this| { + // TEMP: have to handle pending tag independently for now + let tx = if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { + let Some(executor) = this.pending_executor() else { + return Err(StarknetApiError::BlockNotFound); + }; + + let executor = executor.read(); + let pending_txs = executor.transactions(); + pending_txs.get(index as usize).map(|(tx, _)| tx.clone()) + } else { + let provider = &this.inner.backend.blockchain.provider(); + + let block_num = BlockIdReader::convert_block_id(provider, block_id)? + .map(BlockHashOrNumber::Num) + .ok_or(StarknetApiError::BlockNotFound)?; + + provider.transaction_by_block_and_idx(block_num, index)? + }; + + StarknetApiResult::Ok(tx) + }) + .await?; + + if let Some(tx) = tx { + Ok(tx.into()) + } else if let Some(client) = &self.inner.forked_client { + Ok(client.get_transaction_by_block_id_and_index(block_id, index).await?) + } else { + Err(StarknetApiError::InvalidTxnIndex) + } + } + + async fn transaction(&self, hash: TxHash) -> StarknetApiResult { + let tx = self + .on_io_blocking_task(move |this| { + let tx = this + .inner + .backend + .blockchain + .provider() + .transaction_by_hash(hash)? + .map(Tx::from); + + let result = match tx { + tx @ Some(_) => tx, + None => { + // check if the transaction is in the pending block + this.pending_executor().as_ref().and_then(|exec| { + exec.read() + .transactions() + .iter() + .find(|(tx, _)| tx.hash == hash) + .map(|(tx, _)| Tx::from(tx.clone())) + }) + } + }; + + Result::<_, StarknetApiError>::Ok(result) + }) + .await?; + + if let Some(tx) = tx { + Ok(tx) + } else if let Some(client) = &self.inner.forked_client { + Ok(client.get_transaction_by_hash(hash).await?) + } else { + Err(StarknetApiError::TxnHashNotFound) + } + } + + async fn receipt(&self, hash: Felt) -> StarknetApiResult { + let receipt = self + .on_io_blocking_task(move |this| { + let provider = this.inner.backend.blockchain.provider(); + let receipt = ReceiptBuilder::new(hash, provider).build()?; + + // If receipt is not found, check the pending block. + match receipt { + Some(receipt) => Ok(Some(receipt)), + None => { + let executor = this.pending_executor(); + // If there's a pending executor + let pending_receipt = executor.and_then(|executor| { + // Find the transaction in the pending block that matches the hash + executor.read().transactions().iter().find_map(|(tx, res)| { + if tx.hash == hash { + // If the transaction is found, only return the receipt if it's + // successful + match res { + ExecutionResult::Success { receipt, .. } => { + Some(receipt.clone()) + } + ExecutionResult::Failed { .. } => None, + } + } else { + None + } + }) + }); + + if let Some(receipt) = pending_receipt { + let receipt = TxReceiptWithBlockInfo::new( + ReceiptBlock::Pending, + hash, + FinalityStatus::AcceptedOnL2, + receipt, + ); + + StarknetApiResult::Ok(Some(receipt)) + } else { + StarknetApiResult::Ok(None) + } + } } - }; + }) + .await?; - tx.ok_or(StarknetApiError::TxnHashNotFound) - }) - .await + if let Some(receipt) = receipt { + Ok(receipt) + } else if let Some(client) = &self.inner.forked_client { + Ok(client.get_transaction_receipt(hash).await?) + } else { + Err(StarknetApiError::TxnHashNotFound) + } } // TODO: should document more and possible find a simpler solution(?) @@ -342,7 +499,7 @@ impl StarknetApi { keys: Option>>, continuation_token: Option, chunk_size: u64, - ) -> Result { + ) -> StarknetApiResult { let provider = self.inner.backend.blockchain.provider(); let from = if BlockIdOrTag::Tag(BlockTag::Pending) == from_block { @@ -460,61 +617,315 @@ impl StarknetApi { } } - async fn transaction_status( + async fn transaction_status(&self, hash: TxHash) -> StarknetApiResult { + let status = self + .on_io_blocking_task(move |this| { + let provider = this.inner.backend.blockchain.provider(); + let status = provider.transaction_status(hash)?; + + if let Some(status) = status { + // TODO: this might not work once we allow querying for 'failed' transactions + // from the provider + let Some(receipt) = provider.receipt_by_hash(hash)? else { + return Err(StarknetApiError::UnexpectedError { + reason: "Transaction hash exist, but the receipt is missing" + .to_string(), + }); + }; + + let exec_status = if receipt.is_reverted() { + TransactionExecutionStatus::Reverted + } else { + TransactionExecutionStatus::Succeeded + }; + + let status = match status { + FinalityStatus::AcceptedOnL1 => { + TransactionStatus::AcceptedOnL1(exec_status) + } + FinalityStatus::AcceptedOnL2 => { + TransactionStatus::AcceptedOnL2(exec_status) + } + }; + + return Ok(Some(status)); + } + + // seach in the pending block if the transaction is not found + if let Some(pending_executor) = this.pending_executor() { + let pending_executor = pending_executor.read(); + let pending_txs = pending_executor.transactions(); + let (_, res) = pending_txs + .iter() + .find(|(tx, _)| tx.hash == hash) + .ok_or(StarknetApiError::TxnHashNotFound)?; + + // TODO: should impl From for TransactionStatus + let status = match res { + ExecutionResult::Failed { .. } => TransactionStatus::Rejected, + ExecutionResult::Success { receipt, .. } => { + if receipt.is_reverted() { + TransactionStatus::AcceptedOnL2( + TransactionExecutionStatus::Reverted, + ) + } else { + TransactionStatus::AcceptedOnL2( + TransactionExecutionStatus::Succeeded, + ) + } + } + }; + + Ok(Some(status)) + } else { + // Err(StarknetApiError::TxnHashNotFound) + Ok(None) + } + }) + .await?; + + if let Some(status) = status { + Ok(status) + } else if let Some(client) = &self.inner.forked_client { + Ok(client.get_transaction_status(hash).await?) + } else { + Err(StarknetApiError::TxnHashNotFound) + } + } + + async fn block_with_txs( &self, - hash: TxHash, - ) -> Result { - self.on_io_blocking_task(move |this| { - let provider = this.inner.backend.blockchain.provider(); - let status = provider.transaction_status(hash)?; - - if let Some(status) = status { - // TODO: this might not work once we allow querying for 'failed' transactions from - // the provider - let Some(receipt) = provider.receipt_by_hash(hash)? else { - return Err(StarknetApiError::UnexpectedError { - reason: "Transaction hash exist, but the receipt is missing".to_string(), - }); - }; + block_id: BlockIdOrTag, + ) -> StarknetApiResult { + let block = self + .on_io_blocking_task(move |this| { + let provider = this.inner.backend.blockchain.provider(); + + if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { + if let Some(executor) = this.pending_executor() { + let block_env = executor.read().block_env(); + let latest_hash = provider.latest_hash().map_err(StarknetApiError::from)?; + + let l1_gas_prices = block_env.l1_gas_prices.clone(); + let l1_data_gas_prices = block_env.l1_data_gas_prices.clone(); + + let header = PartialHeader { + l1_da_mode: L1DataAvailabilityMode::Calldata, + l1_gas_prices, + l1_data_gas_prices, + number: block_env.number, + parent_hash: latest_hash, + timestamp: block_env.timestamp, + sequencer_address: block_env.sequencer_address, + protocol_version: this.inner.backend.chain_spec.version.clone(), + }; + + // TODO(kariy): create a method that can perform this filtering for us + // instead of doing it manually. + + // A block should only include successful transactions, we filter out the + // failed ones (didn't pass validation stage). + let transactions = executor + .read() + .transactions() + .iter() + .filter(|(_, receipt)| receipt.is_success()) + .map(|(tx, _)| tx.clone()) + .collect::>(); + + let block = PendingBlockWithTxs::new(header, transactions); + return Ok(Some(MaybePendingBlockWithTxs::Pending(block))); + } + } + + if let Some(num) = provider.convert_block_id(block_id)? { + let block = katana_rpc_types_builder::BlockBuilder::new(num.into(), provider) + .build()? + .map(MaybePendingBlockWithTxs::Block); - let exec_status = if receipt.is_reverted() { - TransactionExecutionStatus::Reverted + StarknetApiResult::Ok(block) } else { - TransactionExecutionStatus::Succeeded - }; + StarknetApiResult::Ok(None) + } + }) + .await?; - return Ok(match status { - FinalityStatus::AcceptedOnL1 => TransactionStatus::AcceptedOnL1(exec_status), - FinalityStatus::AcceptedOnL2 => TransactionStatus::AcceptedOnL2(exec_status), - }); - } + if let Some(block) = block { + Ok(block) + } else if let Some(client) = &self.inner.forked_client { + Ok(client.get_block_with_txs(block_id).await?) + } else { + Err(StarknetApiError::BlockNotFound) + } + } - // seach in the pending block if the transaction is not found - if let Some(pending_executor) = this.pending_executor() { - let pending_executor = pending_executor.read(); - let pending_txs = pending_executor.transactions(); - let (_, res) = pending_txs - .iter() - .find(|(tx, _)| tx.hash == hash) - .ok_or(StarknetApiError::TxnHashNotFound)?; - - // TODO: should impl From for TransactionStatus - let status = match res { - ExecutionResult::Failed { .. } => TransactionStatus::Rejected, - ExecutionResult::Success { receipt, .. } => { - if receipt.is_reverted() { - TransactionStatus::AcceptedOnL2(TransactionExecutionStatus::Reverted) - } else { - TransactionStatus::AcceptedOnL2(TransactionExecutionStatus::Succeeded) - } + async fn block_with_receipts( + &self, + block_id: BlockIdOrTag, + ) -> StarknetApiResult { + let block = self + .on_io_blocking_task(move |this| { + let provider = this.inner.backend.blockchain.provider(); + + if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { + if let Some(executor) = this.pending_executor() { + let block_env = executor.read().block_env(); + let latest_hash = provider.latest_hash()?; + + let l1_gas_prices = block_env.l1_gas_prices.clone(); + let l1_data_gas_prices = block_env.l1_data_gas_prices.clone(); + + let header = PartialHeader { + l1_gas_prices, + l1_data_gas_prices, + number: block_env.number, + parent_hash: latest_hash, + timestamp: block_env.timestamp, + l1_da_mode: L1DataAvailabilityMode::Calldata, + sequencer_address: block_env.sequencer_address, + protocol_version: this.inner.backend.chain_spec.version.clone(), + }; + + let receipts = executor + .read() + .transactions() + .iter() + .filter_map(|(tx, result)| match result { + ExecutionResult::Success { receipt, .. } => { + Some((tx.clone(), receipt.clone())) + } + ExecutionResult::Failed { .. } => None, + }) + .collect::>(); + + let block = PendingBlockWithReceipts::new(header, receipts.into_iter()); + return Ok(Some(MaybePendingBlockWithReceipts::Pending(block))); + } + } + + if let Some(num) = provider.convert_block_id(block_id)? { + let block = katana_rpc_types_builder::BlockBuilder::new(num.into(), provider) + .build_with_receipts()? + .map(MaybePendingBlockWithReceipts::Block); + + StarknetApiResult::Ok(block) + } else { + StarknetApiResult::Ok(None) + } + }) + .await?; + + if let Some(block) = block { + Ok(block) + } else if let Some(client) = &self.inner.forked_client { + Ok(client.get_block_with_receipts(block_id).await?) + } else { + Err(StarknetApiError::BlockNotFound) + } + } + + async fn block_with_tx_hashes( + &self, + block_id: BlockIdOrTag, + ) -> StarknetApiResult { + let block = self + .on_io_blocking_task(move |this| { + let provider = this.inner.backend.blockchain.provider(); + + if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { + if let Some(executor) = this.pending_executor() { + let block_env = executor.read().block_env(); + let latest_hash = provider.latest_hash().map_err(StarknetApiError::from)?; + + let l1_gas_prices = block_env.l1_gas_prices.clone(); + let l1_data_gas_prices = block_env.l1_data_gas_prices.clone(); + + let header = PartialHeader { + l1_da_mode: L1DataAvailabilityMode::Calldata, + l1_data_gas_prices, + l1_gas_prices, + number: block_env.number, + parent_hash: latest_hash, + timestamp: block_env.timestamp, + protocol_version: this.inner.backend.chain_spec.version.clone(), + sequencer_address: block_env.sequencer_address, + }; + + // TODO(kariy): create a method that can perform this filtering for us + // instead of doing it manually. + + // A block should only include successful transactions, we filter out the + // failed ones (didn't pass validation stage). + let transactions = executor + .read() + .transactions() + .iter() + .filter(|(_, receipt)| receipt.is_success()) + .map(|(tx, _)| tx.hash) + .collect::>(); + + let block = PendingBlockWithTxHashes::new(header, transactions); + return Ok(Some(MaybePendingBlockWithTxHashes::Pending(block))); + } + } + + if let Some(num) = provider.convert_block_id(block_id)? { + let block = katana_rpc_types_builder::BlockBuilder::new(num.into(), provider) + .build_with_tx_hash()? + .map(MaybePendingBlockWithTxHashes::Block); + + StarknetApiResult::Ok(block) + } else { + StarknetApiResult::Ok(None) + } + }) + .await?; + + if let Some(block) = block { + Ok(block) + } else if let Some(client) = &self.inner.forked_client { + Ok(client.get_block_with_tx_hashes(block_id).await?) + } else { + Err(StarknetApiError::BlockNotFound) + } + } + + async fn state_update( + &self, + block_id: BlockIdOrTag, + ) -> StarknetApiResult { + let state_update = self + .on_io_blocking_task(move |this| { + let provider = this.inner.backend.blockchain.provider(); + + let block_id = match block_id { + BlockIdOrTag::Number(num) => BlockHashOrNumber::Num(num), + BlockIdOrTag::Hash(hash) => BlockHashOrNumber::Hash(hash), + + BlockIdOrTag::Tag(BlockTag::Latest) => { + provider.latest_number().map(BlockHashOrNumber::Num)? + } + + BlockIdOrTag::Tag(BlockTag::Pending) => { + return Err(StarknetApiError::BlockNotFound); } }; - Ok(status) - } else { - Err(StarknetApiError::TxnHashNotFound) - } - }) - .await + let state_update = + katana_rpc_types_builder::StateUpdateBuilder::new(block_id, provider) + .build()? + .map(MaybePendingStateUpdate::Update); + + StarknetApiResult::Ok(state_update) + }) + .await?; + + if let Some(state_update) = state_update { + Ok(state_update) + } else if let Some(client) = &self.inner.forked_client { + Ok(client.get_state_update(block_id).await?) + } else { + Err(StarknetApiError::BlockNotFound) + } } } diff --git a/crates/katana/rpc/rpc/src/starknet/read.rs b/crates/katana/rpc/rpc/src/starknet/read.rs index ed7702bd9c..20d5c23173 100644 --- a/crates/katana/rpc/rpc/src/starknet/read.rs +++ b/crates/katana/rpc/rpc/src/starknet/read.rs @@ -1,27 +1,22 @@ use jsonrpsee::core::{async_trait, Error, RpcResult}; -use katana_executor::{EntryPointCall, ExecutionResult, ExecutorFactory}; -use katana_primitives::block::{BlockHashOrNumber, BlockIdOrTag, FinalityStatus, PartialHeader}; -use katana_primitives::da::L1DataAvailabilityMode; +use katana_executor::{EntryPointCall, ExecutorFactory}; +use katana_primitives::block::BlockIdOrTag; use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; use katana_primitives::Felt; -use katana_provider::traits::block::{BlockHashProvider, BlockIdReader, BlockNumberProvider}; -use katana_provider::traits::transaction::TransactionProvider; use katana_rpc_api::starknet::StarknetApiServer; use katana_rpc_types::block::{ BlockHashAndNumber, MaybePendingBlockWithReceipts, MaybePendingBlockWithTxHashes, - MaybePendingBlockWithTxs, PendingBlockWithReceipts, PendingBlockWithTxHashes, - PendingBlockWithTxs, + MaybePendingBlockWithTxs, }; use katana_rpc_types::error::starknet::StarknetApiError; use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; use katana_rpc_types::message::MsgFromL1; -use katana_rpc_types::receipt::{ReceiptBlock, TxReceiptWithBlockInfo}; -use katana_rpc_types::state_update::StateUpdate; +use katana_rpc_types::receipt::TxReceiptWithBlockInfo; +use katana_rpc_types::state_update::MaybePendingStateUpdate; use katana_rpc_types::transaction::{BroadcastedTx, Tx}; use katana_rpc_types::{ ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlagForEstimateFee, }; -use katana_rpc_types_builder::ReceiptBuilder; use starknet::core::types::{BlockTag, TransactionStatus}; use super::StarknetApi; @@ -45,11 +40,11 @@ impl StarknetApiServer for StarknetApi { } async fn get_transaction_by_hash(&self, transaction_hash: Felt) -> RpcResult { - Ok(self.transaction(transaction_hash).await?.into()) + Ok(self.transaction(transaction_hash).await?) } async fn get_block_transaction_count(&self, block_id: BlockIdOrTag) -> RpcResult { - self.on_io_blocking_task(move |this| Ok(this.block_tx_count(block_id)?)).await + Ok(self.block_tx_count(block_id).await?) } async fn get_class_at( @@ -72,59 +67,7 @@ impl StarknetApiServer for StarknetApi { &self, block_id: BlockIdOrTag, ) -> RpcResult { - self.on_io_blocking_task(move |this| { - let provider = this.inner.backend.blockchain.provider(); - - if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { - if let Some(executor) = this.pending_executor() { - let block_env = executor.read().block_env(); - let latest_hash = provider.latest_hash().map_err(StarknetApiError::from)?; - - let l1_gas_prices = block_env.l1_gas_prices.clone(); - let l1_data_gas_prices = block_env.l1_data_gas_prices.clone(); - - let header = PartialHeader { - l1_da_mode: L1DataAvailabilityMode::Calldata, - l1_data_gas_prices, - l1_gas_prices, - number: block_env.number, - parent_hash: latest_hash, - timestamp: block_env.timestamp, - protocol_version: this.inner.backend.chain_spec.version.clone(), - sequencer_address: block_env.sequencer_address, - }; - - // TODO(kariy): create a method that can perform this filtering for us instead - // of doing it manually. - - // A block should only include successful transactions, we filter out the failed - // ones (didn't pass validation stage). - let transactions = executor - .read() - .transactions() - .iter() - .filter(|(_, receipt)| receipt.is_success()) - .map(|(tx, _)| tx.hash) - .collect::>(); - - return Ok(MaybePendingBlockWithTxHashes::Pending( - PendingBlockWithTxHashes::new(header, transactions), - )); - } - } - - let block_num = BlockIdReader::convert_block_id(provider, block_id) - .map_err(StarknetApiError::from)? - .map(BlockHashOrNumber::Num) - .ok_or(StarknetApiError::BlockNotFound)?; - - katana_rpc_types_builder::BlockBuilder::new(block_num, provider) - .build_with_tx_hash() - .map_err(StarknetApiError::from)? - .map(MaybePendingBlockWithTxHashes::Block) - .ok_or(Error::from(StarknetApiError::BlockNotFound)) - }) - .await + Ok(self.block_with_tx_hashes(block_id).await?) } async fn get_transaction_by_block_id_and_index( @@ -132,219 +75,32 @@ impl StarknetApiServer for StarknetApi { block_id: BlockIdOrTag, index: u64, ) -> RpcResult { - self.on_io_blocking_task(move |this| { - // TEMP: have to handle pending tag independently for now - let tx = if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { - let Some(executor) = this.pending_executor() else { - return Err(StarknetApiError::BlockNotFound.into()); - }; - - let executor = executor.read(); - let pending_txs = executor.transactions(); - pending_txs.get(index as usize).map(|(tx, _)| tx.clone()) - } else { - let provider = &this.inner.backend.blockchain.provider(); - - let block_num = BlockIdReader::convert_block_id(provider, block_id) - .map_err(StarknetApiError::from)? - .map(BlockHashOrNumber::Num) - .ok_or(StarknetApiError::BlockNotFound)?; - - TransactionProvider::transaction_by_block_and_idx(provider, block_num, index) - .map_err(StarknetApiError::from)? - }; - - Ok(tx.ok_or(StarknetApiError::InvalidTxnIndex)?.into()) - }) - .await + Ok(self.transaction_by_block_id_and_index(block_id, index).await?) } async fn get_block_with_txs( &self, block_id: BlockIdOrTag, ) -> RpcResult { - self.on_io_blocking_task(move |this| { - let provider = this.inner.backend.blockchain.provider(); - - if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { - if let Some(executor) = this.pending_executor() { - let block_env = executor.read().block_env(); - let latest_hash = provider.latest_hash().map_err(StarknetApiError::from)?; - - let l1_gas_prices = block_env.l1_gas_prices.clone(); - let l1_data_gas_prices = block_env.l1_data_gas_prices.clone(); - - let header = PartialHeader { - l1_da_mode: L1DataAvailabilityMode::Calldata, - l1_gas_prices, - l1_data_gas_prices, - number: block_env.number, - parent_hash: latest_hash, - timestamp: block_env.timestamp, - sequencer_address: block_env.sequencer_address, - protocol_version: this.inner.backend.chain_spec.version.clone(), - }; - - // TODO(kariy): create a method that can perform this filtering for us instead - // of doing it manually. - - // A block should only include successful transactions, we filter out the failed - // ones (didn't pass validation stage). - let transactions = executor - .read() - .transactions() - .iter() - .filter(|(_, receipt)| receipt.is_success()) - .map(|(tx, _)| tx.clone()) - .collect::>(); - - return Ok(MaybePendingBlockWithTxs::Pending(PendingBlockWithTxs::new( - header, - transactions, - ))); - } - } - - let block_num = BlockIdReader::convert_block_id(provider, block_id) - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? - .map(BlockHashOrNumber::Num) - .ok_or(StarknetApiError::BlockNotFound)?; - - katana_rpc_types_builder::BlockBuilder::new(block_num, provider) - .build() - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? - .map(MaybePendingBlockWithTxs::Block) - .ok_or(Error::from(StarknetApiError::BlockNotFound)) - }) - .await + Ok(self.block_with_txs(block_id).await?) } async fn get_block_with_receipts( &self, block_id: BlockIdOrTag, ) -> RpcResult { - self.on_io_blocking_task(move |this| { - let provider = this.inner.backend.blockchain.provider(); - - if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { - if let Some(executor) = this.pending_executor() { - let block_env = executor.read().block_env(); - let latest_hash = provider.latest_hash().map_err(StarknetApiError::from)?; - - let l1_gas_prices = block_env.l1_gas_prices.clone(); - let l1_data_gas_prices = block_env.l1_data_gas_prices.clone(); - - let header = PartialHeader { - l1_da_mode: L1DataAvailabilityMode::Calldata, - l1_gas_prices, - l1_data_gas_prices, - number: block_env.number, - parent_hash: latest_hash, - protocol_version: this.inner.backend.chain_spec.version.clone(), - timestamp: block_env.timestamp, - sequencer_address: block_env.sequencer_address, - }; - - let receipts = executor - .read() - .transactions() - .iter() - .filter_map(|(tx, result)| match result { - ExecutionResult::Success { receipt, .. } => { - Some((tx.clone(), receipt.clone())) - } - ExecutionResult::Failed { .. } => None, - }) - .collect::>(); - - return Ok(MaybePendingBlockWithReceipts::Pending( - PendingBlockWithReceipts::new(header, receipts.into_iter()), - )); - } - } - - let block_num = BlockIdReader::convert_block_id(provider, block_id) - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? - .map(BlockHashOrNumber::Num) - .ok_or(StarknetApiError::BlockNotFound)?; - - let block = katana_rpc_types_builder::BlockBuilder::new(block_num, provider) - .build_with_receipts() - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? - .ok_or(Error::from(StarknetApiError::BlockNotFound))?; - - Ok(MaybePendingBlockWithReceipts::Block(block)) - }) - .await + Ok(self.block_with_receipts(block_id).await?) } - async fn get_state_update(&self, block_id: BlockIdOrTag) -> RpcResult { - self.on_io_blocking_task(move |this| { - let provider = this.inner.backend.blockchain.provider(); - - let block_id = match block_id { - BlockIdOrTag::Number(num) => BlockHashOrNumber::Num(num), - BlockIdOrTag::Hash(hash) => BlockHashOrNumber::Hash(hash), - - BlockIdOrTag::Tag(BlockTag::Latest) => BlockNumberProvider::latest_number(provider) - .map(BlockHashOrNumber::Num) - .map_err(|_| StarknetApiError::BlockNotFound)?, - - BlockIdOrTag::Tag(BlockTag::Pending) => { - return Err(StarknetApiError::BlockNotFound.into()); - } - }; - - katana_rpc_types_builder::StateUpdateBuilder::new(block_id, provider) - .build() - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? - .ok_or(Error::from(StarknetApiError::BlockNotFound)) - }) - .await + async fn get_state_update(&self, block_id: BlockIdOrTag) -> RpcResult { + Ok(self.state_update(block_id).await?) } async fn get_transaction_receipt( &self, transaction_hash: Felt, ) -> RpcResult { - self.on_io_blocking_task(move |this| { - let provider = this.inner.backend.blockchain.provider(); - let receipt = ReceiptBuilder::new(transaction_hash, provider) - .build() - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })?; - - match receipt { - Some(receipt) => Ok(receipt), - - None => { - let executor = this.pending_executor(); - let pending_receipt = executor - .and_then(|executor| { - executor.read().transactions().iter().find_map(|(tx, res)| { - if tx.hash == transaction_hash { - match res { - ExecutionResult::Failed { .. } => None, - ExecutionResult::Success { receipt, .. } => { - Some(receipt.clone()) - } - } - } else { - None - } - }) - }) - .ok_or(Error::from(StarknetApiError::TxnHashNotFound))?; - - Ok(TxReceiptWithBlockInfo::new( - ReceiptBlock::Pending, - transaction_hash, - FinalityStatus::AcceptedOnL2, - pending_receipt, - )) - } - } - }) - .await + Ok(self.receipt(transaction_hash).await?) } async fn get_class_hash_at( diff --git a/crates/katana/rpc/rpc/src/starknet/trace.rs b/crates/katana/rpc/rpc/src/starknet/trace.rs index fe1d59ca4b..ef23f69439 100644 --- a/crates/katana/rpc/rpc/src/starknet/trace.rs +++ b/crates/katana/rpc/rpc/src/starknet/trace.rs @@ -96,7 +96,7 @@ impl StarknetApi { ExecutionResult::Failed { error } => { let error = StarknetApiError::TransactionExecutionError { - transaction_index: i, + transaction_index: i as u64, execution_error: error.to_string(), }; return Err(error); diff --git a/crates/katana/rpc/rpc/tests/common/mod.rs b/crates/katana/rpc/rpc/tests/common/mod.rs index fb452a29b4..a1fdd29cfa 100644 --- a/crates/katana/rpc/rpc/tests/common/mod.rs +++ b/crates/katana/rpc/rpc/tests/common/mod.rs @@ -1,3 +1,5 @@ +#![allow(unused)] + use std::fs::File; use std::path::PathBuf; @@ -69,3 +71,21 @@ pub fn split_felt(felt: Felt) -> (Felt, Felt) { let high = felt.to_biguint() >> 128; (low, Felt::from(high)) } + +/// Assert that the given error is a Starknet error from a +/// [`AccountError`](starknet::accounts::AccountError). +#[macro_export] +macro_rules! assert_account_starknet_err { + ($err:expr, $api_err:pat) => { + assert_matches!($err, AccountError::Provider(ProviderError::StarknetError($api_err))) + }; +} + +/// Assert that the given error is a Starknet error from a +/// [`ProviderError`](starknet::providers::ProviderError). +#[macro_export] +macro_rules! assert_provider_starknet_err { + ($err:expr, $api_err:pat) => { + assert_matches!($err, ProviderError::StarknetError($api_err)) + }; +} diff --git a/crates/katana/rpc/rpc/tests/forking.rs b/crates/katana/rpc/rpc/tests/forking.rs new file mode 100644 index 0000000000..71372f981b --- /dev/null +++ b/crates/katana/rpc/rpc/tests/forking.rs @@ -0,0 +1,387 @@ +use anyhow::Result; +use assert_matches::assert_matches; +use cainome::rs::abigen_legacy; +use dojo_test_utils::sequencer::{get_default_test_config, TestSequencer}; +use katana_node::config::fork::ForkingConfig; +use katana_node::config::SequencingConfig; +use katana_primitives::block::{BlockHash, BlockHashOrNumber, BlockIdOrTag, BlockNumber}; +use katana_primitives::chain::NamedChainId; +use katana_primitives::genesis::constant::DEFAULT_ETH_FEE_TOKEN_ADDRESS; +use katana_primitives::transaction::TxHash; +use katana_primitives::{felt, Felt}; +use starknet::core::types::{MaybePendingBlockWithTxHashes, StarknetError}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{JsonRpcClient, Provider, ProviderError}; +use url::Url; + +mod common; + +const SEPOLIA_CHAIN_ID: Felt = NamedChainId::SN_SEPOLIA; +const SEPOLIA_URL: &str = "https://api.cartridge.gg/x/starknet/sepolia"; +const FORK_BLOCK_NUMBER: BlockNumber = 268_471; + +fn forking_cfg() -> ForkingConfig { + ForkingConfig { + url: Url::parse(SEPOLIA_URL).unwrap(), + block: Some(BlockHashOrNumber::Num(FORK_BLOCK_NUMBER)), + } +} + +type LocalTestVector = Vec<((BlockNumber, BlockHash), TxHash)>; + +/// A helper function for setting a test environment, forked from the SN_SEPOLIA chain. +/// This function will forked Sepolia at block [`FORK_BLOCK_NUMBER`] and create 10 blocks, each has +/// a single transaction. +/// +/// The returned [`TestVector`] is a list of all the locally created blocks and transactions. +async fn setup_test() -> (TestSequencer, impl Provider, LocalTestVector) { + let mut config = get_default_test_config(SequencingConfig::default()); + config.forking = Some(forking_cfg()); + + let sequencer = TestSequencer::start(config).await; + let provider = JsonRpcClient::new(HttpTransport::new(sequencer.url())); + + let mut txs_vector: LocalTestVector = Vec::new(); + + // create some emtpy blocks and dummy transactions + abigen_legacy!(FeeToken, "crates/katana/rpc/rpc/tests/test_data/erc20.json"); + let contract = FeeToken::new(DEFAULT_ETH_FEE_TOKEN_ADDRESS.into(), sequencer.account()); + + // we're in auto mining, each transaction will create a new block + for i in 1..=10 { + let amount = Uint256 { low: Felt::ONE, high: Felt::ZERO }; + let res = contract.transfer(&Felt::ONE, &amount).send().await.unwrap(); + let _ = dojo_utils::TransactionWaiter::new(res.transaction_hash, &provider).await.unwrap(); + + let block_num = FORK_BLOCK_NUMBER + i; + + let block_id = BlockIdOrTag::Number(block_num); + let block = provider.get_block_with_tx_hashes(block_id).await.unwrap(); + let block_hash = match block { + MaybePendingBlockWithTxHashes::Block(b) => b.block_hash, + _ => panic!("Expected a block"), + }; + + txs_vector.push(((FORK_BLOCK_NUMBER + i, block_hash), res.transaction_hash)); + } + + (sequencer, provider, txs_vector) +} + +#[tokio::test] +async fn can_fork() -> Result<()> { + let (_sequencer, provider, _) = setup_test().await; + + let block = provider.block_number().await?; + let chain = provider.chain_id().await?; + + assert_eq!(chain, SEPOLIA_CHAIN_ID); + assert_eq!(block, FORK_BLOCK_NUMBER + 10); + + Ok(()) +} + +#[tokio::test] +async fn forked_blocks_from_num() -> Result<()> { + use starknet::core::types::{ + MaybePendingBlockWithReceipts, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, + }; + + let (_sequencer, provider, local_only_block) = setup_test().await; + + // ----------------------------------------------------------------------- + // Get the forked block + // https://sepolia.voyager.online/block/0x208950cfcbba73ecbda1c14e4d58d66a8d60655ea1b9dcf07c16014ae8a93cd + + let num = FORK_BLOCK_NUMBER; // 268471 + let id = BlockIdOrTag::Number(num); + + let block = provider.get_block_with_txs(id).await?; + assert_matches!(block, MaybePendingBlockWithTxs::Block(b) if b.block_number == num); + + let block = provider.get_block_with_receipts(id).await?; + assert_matches!(block, MaybePendingBlockWithReceipts::Block(b) if b.block_number == num); + + let block = provider.get_block_with_tx_hashes(id).await?; + assert_matches!(block, MaybePendingBlockWithTxHashes::Block(b) if b.block_number == num); + + let result = provider.get_block_transaction_count(id).await; + assert!(result.is_ok()); + + // TODO: uncomment this once we include genesis forked state update + // let state = provider.get_state_update(id).await?; + // assert_matches!(state, starknet::core::types::MaybePendingStateUpdate::Update(_)); + + // ----------------------------------------------------------------------- + // Get a block before the forked block + + // https://sepolia.voyager.online/block/0x42dc67af5003d212ac6cd784e72db945ea4d619898f30f422358ff215cbe1e4 + let num = FORK_BLOCK_NUMBER - 5; // 268466 + let id = BlockIdOrTag::Number(num); + + let block = provider.get_block_with_txs(id).await?; + assert_matches!(block, MaybePendingBlockWithTxs::Block(b) if b.block_number == num); + + let block = provider.get_block_with_receipts(id).await?; + assert_matches!(block, MaybePendingBlockWithReceipts::Block(b) if b.block_number == num); + + let block = provider.get_block_with_tx_hashes(id).await?; + assert_matches!(block, MaybePendingBlockWithTxHashes::Block(b) if b.block_number == num); + + let result = provider.get_block_transaction_count(id).await; + assert!(result.is_ok()); + + // TODO: uncomment this once we include genesis forked state update + // let state = provider.get_state_update(id).await?; + // assert_matches!(state, starknet::core::types::MaybePendingStateUpdate::Update(_)); + + // ----------------------------------------------------------------------- + // Get a block that is locally generated + + for ((num, _), _) in local_only_block { + let id = BlockIdOrTag::Number(num); + + let block = provider.get_block_with_txs(id).await?; + assert_matches!(block, MaybePendingBlockWithTxs::Block(b) if b.block_number == num); + + let block = provider.get_block_with_receipts(id).await?; + assert_matches!(block, starknet::core::types::MaybePendingBlockWithReceipts::Block(b) if b.block_number == num); + + let block = provider.get_block_with_tx_hashes(id).await?; + assert_matches!(block, starknet::core::types::MaybePendingBlockWithTxHashes::Block(b) if b.block_number == num); + + let count = provider.get_block_transaction_count(id).await?; + assert_eq!(count, 1, "all the locally generated blocks should have 1 tx"); + + // TODO: uncomment this once we include genesis forked state update + // let state = provider.get_state_update(id).await?; + // assert_matches!(state, starknet::core::types::MaybePendingStateUpdate::Update(_)); + } + + // ----------------------------------------------------------------------- + // Get a block that only exist in the forked chain + + // https://sepolia.voyager.online/block/0x347a9fa25700e7a2d8f26b39c0ecf765be9a78c559b9cae722a659f25182d10 + // We only created 10 local blocks so this is fine. + let id = BlockIdOrTag::Number(270_328); + let result = provider.get_block_with_txs(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_with_receipts(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_with_tx_hashes(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_transaction_count(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_state_update(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + // ----------------------------------------------------------------------- + // Get block that doesn't exist on the both the forked and local chain + + let id = BlockIdOrTag::Number(i64::MAX as u64); + let result = provider.get_block_with_txs(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_with_receipts(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_with_tx_hashes(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_transaction_count(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_state_update(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + Ok(()) +} + +#[tokio::test] +async fn forked_blocks_from_hash() -> Result<()> { + use starknet::core::types::{ + MaybePendingBlockWithReceipts, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, + }; + + let (_sequencer, provider, local_only_block) = setup_test().await; + + // ----------------------------------------------------------------------- + // Get the forked block + + // https://sepolia.voyager.online/block/0x208950cfcbba73ecbda1c14e4d58d66a8d60655ea1b9dcf07c16014ae8a93cd + let hash = felt!("0x208950cfcbba73ecbda1c14e4d58d66a8d60655ea1b9dcf07c16014ae8a93cd"); // 268471 + let id = BlockIdOrTag::Hash(hash); + + let block = provider.get_block_with_txs(id).await?; + assert_matches!(block, MaybePendingBlockWithTxs::Block(b) if b.block_hash == hash); + + let block = provider.get_block_with_receipts(id).await?; + assert_matches!(block, MaybePendingBlockWithReceipts::Block(b) if b.block_hash == hash); + + let block = provider.get_block_with_tx_hashes(id).await?; + assert_matches!(block, MaybePendingBlockWithTxHashes::Block(b) if b.block_hash == hash); + + let result = provider.get_block_transaction_count(id).await; + assert!(result.is_ok()); + + // TODO: uncomment this once we include genesis forked state update + // let state = provider.get_state_update(id).await?; + // assert_matches!(state, starknet::core::types::MaybePendingStateUpdate::Update(_)); + + // ----------------------------------------------------------------------- + // Get a block before the forked block + // https://sepolia.voyager.online/block/0x42dc67af5003d212ac6cd784e72db945ea4d619898f30f422358ff215cbe1e4 + + let hash = felt!("0x42dc67af5003d212ac6cd784e72db945ea4d619898f30f422358ff215cbe1e4"); // 268466 + let id = BlockIdOrTag::Hash(hash); + + let block = provider.get_block_with_txs(id).await?; + assert_matches!(block, MaybePendingBlockWithTxs::Block(b) if b.block_hash == hash); + + let block = provider.get_block_with_receipts(id).await?; + assert_matches!(block, MaybePendingBlockWithReceipts::Block(b) if b.block_hash == hash); + + let block = provider.get_block_with_tx_hashes(id).await?; + assert_matches!(block, MaybePendingBlockWithTxHashes::Block(b) if b.block_hash == hash); + + let result = provider.get_block_transaction_count(id).await; + assert!(result.is_ok()); + + // TODO: uncomment this once we include genesis forked state update + // let state = provider.get_state_update(id).await?; + // assert_matches!(state, starknet::core::types::MaybePendingStateUpdate::Update(_)); + + // ----------------------------------------------------------------------- + // Get a block that is locally generated + + for ((_, hash), _) in local_only_block { + let id = BlockIdOrTag::Hash(hash); + + let block = provider.get_block_with_txs(id).await?; + assert_matches!(block, MaybePendingBlockWithTxs::Block(b) if b.block_hash == hash); + + let block = provider.get_block_with_receipts(id).await?; + assert_matches!(block, MaybePendingBlockWithReceipts::Block(b) if b.block_hash == hash); + + let block = provider.get_block_with_tx_hashes(id).await?; + assert_matches!(block, MaybePendingBlockWithTxHashes::Block(b) if b.block_hash == hash); + + let result = provider.get_block_transaction_count(id).await; + assert!(result.is_ok()); + + // TODO: uncomment this once we include genesis forked state update + // let state = provider.get_state_update(id).await?; + // assert_matches!(state, starknet::core::types::MaybePendingStateUpdate::Update(_)); + } + + // ----------------------------------------------------------------------- + // Get a block that only exist in the forked chain + + // https://sepolia.voyager.online/block/0x347a9fa25700e7a2d8f26b39c0ecf765be9a78c559b9cae722a659f25182d10 + // We only created 10 local blocks so this is fine. + let id = BlockIdOrTag::Number(270_328); + let result = provider.get_block_with_txs(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_with_receipts(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_with_tx_hashes(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_transaction_count(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_state_update(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + // ----------------------------------------------------------------------- + // Get block that doesn't exist on the both the forked and local chain + + let id = BlockIdOrTag::Number(i64::MAX as u64); + let result = provider.get_block_with_txs(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_with_receipts(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_with_tx_hashes(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_block_transaction_count(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + let result = provider.get_state_update(id).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::BlockNotFound); + + Ok(()) +} + +#[tokio::test] +async fn forked_transactions() -> Result<()> { + let (_sequencer, provider, local_only_data) = setup_test().await; + + // ----------------------------------------------------------------------- + // Get txs before the forked block. + + // https://sepolia.voyager.online/tx/0x81207d4244596678e186f6ab9c833fe40a4b35291e8a90b9a163f7f643df9f + // Transaction in block num FORK_BLOCK_NUMBER - 1 + let tx_hash = felt!("0x81207d4244596678e186f6ab9c833fe40a4b35291e8a90b9a163f7f643df9f"); + + let tx = provider.get_transaction_by_hash(tx_hash).await?; + assert_eq!(*tx.transaction_hash(), tx_hash); + + let tx = provider.get_transaction_receipt(tx_hash).await?; + assert_eq!(*tx.receipt.transaction_hash(), tx_hash); + + let result = provider.get_transaction_status(tx_hash).await; + assert!(result.is_ok()); + + // https://sepolia.voyager.online/tx/0x1b18d62544d4ef749befadabcec019d83218d3905abd321b4c1b1fc948d5710 + // Transaction in block num FORK_BLOCK_NUMBER - 2 + let tx_hash = felt!("0x1b18d62544d4ef749befadabcec019d83218d3905abd321b4c1b1fc948d5710"); + + let tx = provider.get_transaction_by_hash(tx_hash).await?; + assert_eq!(*tx.transaction_hash(), tx_hash); + + let tx = provider.get_transaction_receipt(tx_hash).await?; + assert_eq!(*tx.receipt.transaction_hash(), tx_hash); + + let result = provider.get_transaction_status(tx_hash).await; + assert!(result.is_ok()); + + // ----------------------------------------------------------------------- + // Get the locally created transactions. + + for (_, tx_hash) in local_only_data { + let tx = provider.get_transaction_by_hash(tx_hash).await?; + assert_eq!(*tx.transaction_hash(), tx_hash); + + let tx = provider.get_transaction_receipt(tx_hash).await?; + assert_eq!(*tx.receipt.transaction_hash(), tx_hash); + + let result = provider.get_transaction_status(tx_hash).await; + assert!(result.is_ok()); + } + + // ----------------------------------------------------------------------- + // Get a tx that exists in the forked chain but is included in a block past the forked block. + + // https://sepolia.voyager.online/block/0x335a605f2c91873f8f830a6e5285e704caec18503ca28c18485ea6f682eb65e + // transaction in block num 268,474 (FORK_BLOCK_NUMBER + 3) + let tx_hash = felt!("0x335a605f2c91873f8f830a6e5285e704caec18503ca28c18485ea6f682eb65e"); + let result = provider.get_transaction_by_hash(tx_hash).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::TransactionHashNotFound); + + let result = provider.get_transaction_receipt(tx_hash).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::TransactionHashNotFound); + + let result = provider.get_transaction_status(tx_hash).await.unwrap_err(); + assert_provider_starknet_err!(result, StarknetError::TransactionHashNotFound); + + Ok(()) +} diff --git a/crates/katana/rpc/rpc/tests/starknet.rs b/crates/katana/rpc/rpc/tests/starknet.rs index ac4c6b655c..f07f4ed663 100644 --- a/crates/katana/rpc/rpc/tests/starknet.rs +++ b/crates/katana/rpc/rpc/tests/starknet.rs @@ -34,13 +34,6 @@ use tokio::sync::Mutex; mod common; -/// Macro used to assert that the given error is a Starknet error. -macro_rules! assert_starknet_err { - ($err:expr, $api_err:pat) => { - assert_matches!($err, AccountError::Provider(ProviderError::StarknetError($api_err))) - }; -} - #[tokio::test] async fn declare_and_deploy_contract() -> Result<()> { let sequencer = @@ -180,7 +173,7 @@ async fn declaring_already_existing_class() -> Result<()> { // checks and will not run the account's validation. let result = account.declare_v2(contract.into(), compiled_hash).max_fee(Felt::ONE).send().await; - assert_starknet_err!(result.unwrap_err(), StarknetError::ClassAlreadyDeclared); + assert_account_starknet_err!(result.unwrap_err(), StarknetError::ClassAlreadyDeclared); Ok(()) } @@ -411,7 +404,7 @@ async fn ensure_validator_have_valid_state( // actual balance that we have now. let fee = Felt::from(DEFAULT_PREFUNDED_ACCOUNT_BALANCE); let err = contract.transfer(&recipient, &amount).max_fee(fee).send().await.unwrap_err(); - assert_starknet_err!(err, StarknetError::InsufficientAccountBalance); + assert_account_starknet_err!(err, StarknetError::InsufficientAccountBalance); Ok(()) } @@ -453,7 +446,7 @@ async fn send_txs_with_insufficient_fee( let nonce = sequencer.account().get_nonce().await?; assert_eq!(initial_nonce + 1, nonce, "Nonce should change in fee-disabled mode"); } else { - assert_starknet_err!(res.unwrap_err(), StarknetError::InsufficientMaxFee); + assert_account_starknet_err!(res.unwrap_err(), StarknetError::InsufficientMaxFee); let nonce = sequencer.account().get_nonce().await?; assert_eq!(initial_nonce, nonce, "Nonce shouldn't change in fee-enabled mode"); } @@ -474,7 +467,7 @@ async fn send_txs_with_insufficient_fee( let nonce = sequencer.account().get_nonce().await?; assert_eq!(initial_nonce + 2, nonce, "Nonce should change in fee-disabled mode"); } else { - assert_starknet_err!(res.unwrap_err(), StarknetError::InsufficientAccountBalance); + assert_account_starknet_err!(res.unwrap_err(), StarknetError::InsufficientAccountBalance); // nonce shouldn't change for an invalid tx. let nonce = sequencer.account().get_nonce().await?; @@ -530,7 +523,7 @@ async fn send_txs_with_invalid_signature( let nonce = sequencer.account().get_nonce().await?; assert_eq!(initial_nonce + 1, nonce); } else { - assert_starknet_err!(res.unwrap_err(), StarknetError::ValidationFailure(_)); + assert_account_starknet_err!(res.unwrap_err(), StarknetError::ValidationFailure(_)); // nonce shouldn't change for an invalid tx. let nonce = sequencer.account().get_nonce().await?; @@ -576,7 +569,7 @@ async fn send_txs_with_invalid_nonces( let old_nonce = initial_nonce - Felt::ONE; let res = contract.transfer(&recipient, &amount).nonce(old_nonce).max_fee(fee).send().await; - assert_starknet_err!(res.unwrap_err(), StarknetError::InvalidTransactionNonce); + assert_account_starknet_err!(res.unwrap_err(), StarknetError::InvalidTransactionNonce); let nonce = account.get_nonce().await?; assert_eq!(nonce, initial_nonce, "Nonce shouldn't change on invalid tx."); @@ -601,7 +594,7 @@ async fn send_txs_with_invalid_nonces( let new_nonce = felt!("0x100"); let res = contract.transfer(&recipient, &amount).nonce(new_nonce).max_fee(fee).send().await; - assert_starknet_err!(res.unwrap_err(), StarknetError::InvalidTransactionNonce); + assert_account_starknet_err!(res.unwrap_err(), StarknetError::InvalidTransactionNonce); let nonce = account.get_nonce().await?; assert_eq!(nonce, Felt::TWO, "Nonce shouldn't change bcs the tx is still invalid.");