diff --git a/core/src/client/mod.rs b/core/src/client/mod.rs index 68a9e05d..c8bbe783 100644 --- a/core/src/client/mod.rs +++ b/core/src/client/mod.rs @@ -113,6 +113,13 @@ impl> Client { self.node.get_transaction_receipt(tx_hash).await } + pub async fn get_block_receipts( + &self, + block: BlockTag, + ) -> Result>> { + self.node.get_block_receipts(block).await + } + pub async fn get_transaction_by_hash(&self, tx_hash: B256) -> Option { self.node.get_transaction_by_hash(tx_hash).await } diff --git a/core/src/client/node.rs b/core/src/client/node.rs index 278aa46f..fbc0030c 100644 --- a/core/src/client/node.rs +++ b/core/src/client/node.rs @@ -122,6 +122,15 @@ impl> Node { self.execution.get_transaction_receipt(tx_hash).await } + pub async fn get_block_receipts( + &self, + block: BlockTag, + ) -> Result>> { + self.check_blocktag_age(&block).await?; + + self.execution.get_block_receipts(block).await + } + pub async fn get_transaction_by_hash(&self, tx_hash: B256) -> Option { self.execution.get_transaction(tx_hash).await } diff --git a/core/src/client/rpc.rs b/core/src/client/rpc.rs index 564b11d3..b8cb67c1 100644 --- a/core/src/client/rpc.rs +++ b/core/src/client/rpc.rs @@ -102,6 +102,9 @@ trait EthRpc Result; #[method(name = "getTransactionReceipt")] async fn get_transaction_receipt(&self, hash: B256) -> Result, ErrorObjectOwned>; + #[method(name = "getBlockReceipts")] + async fn get_block_receipts(&self, block: BlockTag) + -> Result>, ErrorObjectOwned>; #[method(name = "getTransactionByHash")] async fn get_transaction_by_hash(&self, hash: B256) -> Result, ErrorObjectOwned>; #[method(name = "getTransactionByBlockHashAndIndex")] @@ -257,6 +260,13 @@ impl> convert_err(self.node.get_transaction_receipt(hash).await) } + async fn get_block_receipts( + &self, + block: BlockTag, + ) -> Result>, ErrorObjectOwned> { + convert_err(self.node.get_block_receipts(block).await) + } + async fn get_transaction_by_hash( &self, hash: B256, diff --git a/core/src/execution/errors.rs b/core/src/execution/errors.rs index 913ce22f..7f9f8422 100644 --- a/core/src/execution/errors.rs +++ b/core/src/execution/errors.rs @@ -25,6 +25,8 @@ pub enum ExecutionError { IncorrectRpcNetwork(), #[error("block not found: {0}")] BlockNotFound(BlockTag), + #[error("receipts root mismatch for block: {0}")] + BlockReceiptsRootMismatch(BlockTag), } /// Errors that can occur during evm.rs calls diff --git a/core/src/execution/mod.rs b/core/src/execution/mod.rs index bc8979d6..db3ba140 100644 --- a/core/src/execution/mod.rs +++ b/core/src/execution/mod.rs @@ -184,32 +184,38 @@ impl> ExecutionClient { if receipt.is_none() { return Ok(None); } - let receipt = receipt.unwrap(); + let block_number = receipt.block_number().unwrap(); + let tag = BlockTag::Number(block_number); - let block = self.state.get_block(BlockTag::Number(block_number)).await; + let block = self.state.get_block(tag).await; let block = if let Some(block) = block { block } else { return Ok(None); }; - let tx_hashes = block.transactions.hashes(); - - let receipts_fut = tx_hashes.iter().map(|hash| async move { - let receipt = self.rpc.get_transaction_receipt(*hash).await; - receipt?.ok_or(eyre::eyre!("missing block receipt")) - }); + // Fetch all receipts in block, check root and inclusion + let receipts = self + .rpc + .get_block_receipts(tag) + .await? + .ok_or(eyre::eyre!("missing block receipt"))?; - let receipts = join_all(receipts_fut).await; - let receipts = receipts.into_iter().collect::>>()?; let receipts_encoded: Vec> = receipts.iter().map(N::encode_receipt).collect(); - - let expected_receipt_root = ordered_trie_root(receipts_encoded); + let expected_receipt_root = ordered_trie_root(receipts_encoded.clone()); let expected_receipt_root = B256::from_slice(&expected_receipt_root.to_fixed_bytes()); - if expected_receipt_root != block.receipts_root || !N::receipt_contains(&receipts, &receipt) + if expected_receipt_root != block.receipts_root + // Note: Some RPC providers return different response in `eth_getTransactionReceipt` vs `eth_getBlockReceipts` + // Primarily due to https://github.com/ethereum/execution-apis/issues/295 not finalized + // Which means that the basic equality check in N::receipt_contains can be flaky + // So as a fallback do equality check on encoded receipts as well + || !( + N::receipt_contains(&receipts, &receipt) + || receipts_encoded.contains(&N::encode_receipt(&receipt)) + ) { return Err(ExecutionError::ReceiptRootMismatch(tx_hash).into()); } @@ -217,6 +223,37 @@ impl> ExecutionClient { Ok(Some(receipt)) } + pub async fn get_block_receipts( + &self, + tag: BlockTag, + ) -> Result>> { + let block = self.state.get_block(tag).await; + let block = if let Some(block) = block { + block + } else { + return Ok(None); + }; + + let tag = BlockTag::Number(block.number.to()); + + let receipts = self + .rpc + .get_block_receipts(tag) + .await? + .ok_or(eyre::eyre!("block receipts not found"))?; + + let receipts_encoded: Vec> = receipts.iter().map(N::encode_receipt).collect(); + + let expected_receipt_root = ordered_trie_root(receipts_encoded); + let expected_receipt_root = B256::from_slice(&expected_receipt_root.to_fixed_bytes()); + + if expected_receipt_root != block.receipts_root { + return Err(ExecutionError::BlockReceiptsRootMismatch(tag).into()); + } + + Ok(Some(receipts)) + } + pub async fn get_transaction(&self, hash: B256) -> Option { self.state.get_transaction(hash).await } @@ -300,6 +337,7 @@ impl> ExecutionClient { .collect::, _>>()?; // Collect all (proven) tx receipts as a map of tx hash to receipt + // TODO: use get_block_receipts instead to reduce the number of RPC calls? let receipts_fut = txs_hash.iter().map(|&tx_hash| async move { let receipt = self.get_transaction_receipt(tx_hash).await; receipt?.map(|r| (tx_hash, r)).ok_or(eyre::eyre!( diff --git a/core/src/execution/rpc/http_rpc.rs b/core/src/execution/rpc/http_rpc.rs index 4eb52b96..525e76f4 100644 --- a/core/src/execution/rpc/http_rpc.rs +++ b/core/src/execution/rpc/http_rpc.rs @@ -1,3 +1,4 @@ +use alloy::eips::BlockNumberOrTag; use alloy::primitives::{Address, B256, U256}; use alloy::providers::{Provider, ProviderBuilder, RootProvider}; use alloy::rpc::client::ClientBuilder; @@ -117,6 +118,22 @@ impl ExecutionRpc for HttpRpc { Ok(receipt) } + async fn get_block_receipts(&self, block: BlockTag) -> Result>> { + let block = match block { + BlockTag::Latest => BlockNumberOrTag::Latest, + BlockTag::Finalized => BlockNumberOrTag::Finalized, + BlockTag::Number(num) => BlockNumberOrTag::Number(num), + }; + + let receipts = self + .provider + .get_block_receipts(block) + .await + .map_err(|e| RpcError::new("get_block_receipts", e))?; + + Ok(receipts) + } + async fn get_transaction(&self, tx_hash: B256) -> Result> { Ok(self .provider diff --git a/core/src/execution/rpc/mock_rpc.rs b/core/src/execution/rpc/mock_rpc.rs index 17c977bc..1d0406c0 100644 --- a/core/src/execution/rpc/mock_rpc.rs +++ b/core/src/execution/rpc/mock_rpc.rs @@ -54,6 +54,14 @@ impl ExecutionRpc for MockRpc { Ok(serde_json::from_str(&receipt)?) } + async fn get_block_receipts( + &self, + _block: BlockTag, + ) -> Result>> { + let receipts = read_to_string(self.path.join("receipts.json"))?; + Ok(serde_json::from_str(&receipts)?) + } + async fn get_transaction(&self, _tx_hash: B256) -> Result> { let tx = read_to_string(self.path.join("transaction.json"))?; Ok(serde_json::from_str(&tx)?) diff --git a/core/src/execution/rpc/mod.rs b/core/src/execution/rpc/mod.rs index 56706dc4..e4709e45 100644 --- a/core/src/execution/rpc/mod.rs +++ b/core/src/execution/rpc/mod.rs @@ -32,6 +32,7 @@ pub trait ExecutionRpc: Send + Clone + Sync + 'static { async fn get_code(&self, address: Address, block: u64) -> Result>; async fn send_raw_transaction(&self, bytes: &[u8]) -> Result; async fn get_transaction_receipt(&self, tx_hash: B256) -> Result>; + async fn get_block_receipts(&self, block: BlockTag) -> Result>>; async fn get_transaction(&self, tx_hash: B256) -> Result>; async fn get_logs(&self, filter: &Filter) -> Result>; async fn get_filter_changes(&self, filter_id: U256) -> Result>; diff --git a/helios-ts/lib.ts b/helios-ts/lib.ts index 5115fa20..89e2d60f 100644 --- a/helios-ts/lib.ts +++ b/helios-ts/lib.ts @@ -104,6 +104,8 @@ export class HeliosProvider { case "eth_getTransactionReceipt": { return this.#client.get_transaction_receipt(req.params[0]); } + case "eth_getBlockReceipts": + return this.#client.get_block_receipts(req.params[0]); case "eth_getLogs": { return this.#client.get_logs(req.params[0]); } diff --git a/helios-ts/src/ethereum.rs b/helios-ts/src/ethereum.rs index f03adb2a..1afdbb95 100644 --- a/helios-ts/src/ethereum.rs +++ b/helios-ts/src/ethereum.rs @@ -232,6 +232,13 @@ impl EthereumClient { Ok(serde_wasm_bindgen::to_value(&receipt)?) } + #[wasm_bindgen] + pub async fn get_block_receipts(&self, block: JsValue) -> Result { + let block: BlockTag = serde_wasm_bindgen::from_value(block)?; + let receipts = map_err(self.inner.get_block_receipts(block).await)?; + Ok(serde_wasm_bindgen::to_value(&receipts)?) + } + #[wasm_bindgen] pub async fn get_logs(&self, filter: JsValue) -> Result { let filter: Filter = serde_wasm_bindgen::from_value(filter)?; diff --git a/helios-ts/src/opstack.rs b/helios-ts/src/opstack.rs index 47fe946b..45b19074 100644 --- a/helios-ts/src/opstack.rs +++ b/helios-ts/src/opstack.rs @@ -175,6 +175,13 @@ impl OpStackClient { Ok(serde_wasm_bindgen::to_value(&receipt)?) } + #[wasm_bindgen] + pub async fn get_block_receipts(&self, block: JsValue) -> Result { + let block: BlockTag = serde_wasm_bindgen::from_value(block)?; + let receipts = map_err(self.inner.get_block_receipts(block).await)?; + Ok(serde_wasm_bindgen::to_value(&receipts)?) + } + #[wasm_bindgen] pub async fn get_logs(&self, filter: JsValue) -> Result { let filter: Filter = serde_wasm_bindgen::from_value(filter)?; diff --git a/rpc.md b/rpc.md index 5cbb1f77..4e0828f8 100644 --- a/rpc.md +++ b/rpc.md @@ -19,6 +19,7 @@ Helios provides a variety of RPC methods for interacting with the Ethereum netwo | `eth_getBlockByHash` | `get_block_by_hash` | Returns the information of a block by hash. | `get_block_by_hash(&self, hash: &str, full_tx: bool)` | | `eth_sendRawTransaction` | `send_raw_transaction` | Submits a raw transaction to the network. | `client.send_raw_transaction(&self, bytes: &str)` | | `eth_getTransactionReceipt` | `get_transaction_receipt` | Returns the receipt of a transaction by transaction hash. | `client.get_transaction_receipt(&self, hash: &str)` | +| `eth_getBlockReceipts` | `get_block_receipts` | Returns all transaction receipts of a block by number. | `client.get_block_receipts(&self, block: BlockTag)` | | `eth_getLogs` | `get_logs` | Returns an array of logs matching the filter. | `client.get_logs(&self, filter: Filter)` | | `eth_getStorageAt` | `get_storage_at` | Returns the value from a storage position at a given address. | `client.get_storage_at(&self, address: &str, slot: H256, block: BlockTag)` | | `eth_getBlockTransactionCountByHash` | `get_block_transaction_count_by_hash` | Returns the number of transactions in a block from a block matching the transaction hash. | `client.get_block_transaction_count_by_hash(&self, hash: &str)` | diff --git a/tests/rpc_equivalence.rs b/tests/rpc_equivalence.rs index 69846191..262605d9 100644 --- a/tests/rpc_equivalence.rs +++ b/tests/rpc_equivalence.rs @@ -120,6 +120,33 @@ async fn get_transaction_receipt() { assert_eq!(helios_receipt, receipt); } +#[tokio::test] +async fn get_block_receipts() { + let (_handle, helios_provider, provider) = setup().await; + + let block = helios_provider + .get_block_by_number(BlockNumberOrTag::Latest, false) + .await + .unwrap() + .unwrap(); + + let block_num = block.header.number.unwrap().into(); + + let helios_receipts = helios_provider + .get_block_receipts(block_num) + .await + .unwrap() + .unwrap(); + + let receipts = provider + .get_block_receipts(block_num) + .await + .unwrap() + .unwrap(); + + assert_eq!(helios_receipts, receipts); +} + #[tokio::test] async fn get_balance() { let (_handle, helios_provider, provider) = setup().await;