Skip to content

Commit

Permalink
feat: implement eth_getBlockReceipts (#423)
Browse files Browse the repository at this point in the history
* feat: implement eth_getBlockReceipts

* nits

* use eth_getBlockReceipts in get_transaction_receipt

* chore: add todo re get_block_receipts in verify_logs
  • Loading branch information
eshaan7 authored Nov 20, 2024
1 parent 362e9c9 commit c34324c
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 13 deletions.
7 changes: 7 additions & 0 deletions core/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ impl<N: NetworkSpec, C: Consensus<N::TransactionResponse>> Client<N, C> {
self.node.get_transaction_receipt(tx_hash).await
}

pub async fn get_block_receipts(
&self,
block: BlockTag,
) -> Result<Option<Vec<N::ReceiptResponse>>> {
self.node.get_block_receipts(block).await
}

pub async fn get_transaction_by_hash(&self, tx_hash: B256) -> Option<N::TransactionResponse> {
self.node.get_transaction_by_hash(tx_hash).await
}
Expand Down
9 changes: 9 additions & 0 deletions core/src/client/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ impl<N: NetworkSpec, C: Consensus<N::TransactionResponse>> Node<N, C> {
self.execution.get_transaction_receipt(tx_hash).await
}

pub async fn get_block_receipts(
&self,
block: BlockTag,
) -> Result<Option<Vec<N::ReceiptResponse>>> {
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<N::TransactionResponse> {
self.execution.get_transaction(tx_hash).await
}
Expand Down
10 changes: 10 additions & 0 deletions core/src/client/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ trait EthRpc<TX: TransactionResponse + RpcObject, TXR: RpcObject, R: ReceiptResp
async fn send_raw_transaction(&self, bytes: Bytes) -> Result<B256, ErrorObjectOwned>;
#[method(name = "getTransactionReceipt")]
async fn get_transaction_receipt(&self, hash: B256) -> Result<Option<R>, ErrorObjectOwned>;
#[method(name = "getBlockReceipts")]
async fn get_block_receipts(&self, block: BlockTag)
-> Result<Option<Vec<R>>, ErrorObjectOwned>;
#[method(name = "getTransactionByHash")]
async fn get_transaction_by_hash(&self, hash: B256) -> Result<Option<TX>, ErrorObjectOwned>;
#[method(name = "getTransactionByBlockHashAndIndex")]
Expand Down Expand Up @@ -257,6 +260,13 @@ impl<N: NetworkSpec, C: Consensus<N::TransactionResponse>>
convert_err(self.node.get_transaction_receipt(hash).await)
}

async fn get_block_receipts(
&self,
block: BlockTag,
) -> Result<Option<Vec<N::ReceiptResponse>>, ErrorObjectOwned> {
convert_err(self.node.get_block_receipts(block).await)
}

async fn get_transaction_by_hash(
&self,
hash: B256,
Expand Down
2 changes: 2 additions & 0 deletions core/src/execution/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 51 additions & 13 deletions core/src/execution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,39 +184,76 @@ impl<N: NetworkSpec, R: ExecutionRpc<N>> ExecutionClient<N, R> {
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::<Result<Vec<_>>>()?;
let receipts_encoded: Vec<Vec<u8>> = 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());
}

Ok(Some(receipt))
}

pub async fn get_block_receipts(
&self,
tag: BlockTag,
) -> Result<Option<Vec<N::ReceiptResponse>>> {
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<Vec<u8>> = 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<N::TransactionResponse> {
self.state.get_transaction(hash).await
}
Expand Down Expand Up @@ -300,6 +337,7 @@ impl<N: NetworkSpec, R: ExecutionRpc<N>> ExecutionClient<N, R> {
.collect::<Result<HashSet<_>, _>>()?;

// 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!(
Expand Down
17 changes: 17 additions & 0 deletions core/src/execution/rpc/http_rpc.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -117,6 +118,22 @@ impl<N: NetworkSpec> ExecutionRpc<N> for HttpRpc<N> {
Ok(receipt)
}

async fn get_block_receipts(&self, block: BlockTag) -> Result<Option<Vec<N::ReceiptResponse>>> {
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<Option<N::TransactionResponse>> {
Ok(self
.provider
Expand Down
8 changes: 8 additions & 0 deletions core/src/execution/rpc/mock_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ impl<N: NetworkSpec> ExecutionRpc<N> for MockRpc {
Ok(serde_json::from_str(&receipt)?)
}

async fn get_block_receipts(
&self,
_block: BlockTag,
) -> Result<Option<Vec<N::ReceiptResponse>>> {
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<Option<N::TransactionResponse>> {
let tx = read_to_string(self.path.join("transaction.json"))?;
Ok(serde_json::from_str(&tx)?)
Expand Down
1 change: 1 addition & 0 deletions core/src/execution/rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub trait ExecutionRpc<N: NetworkSpec>: Send + Clone + Sync + 'static {
async fn get_code(&self, address: Address, block: u64) -> Result<Vec<u8>>;
async fn send_raw_transaction(&self, bytes: &[u8]) -> Result<B256>;
async fn get_transaction_receipt(&self, tx_hash: B256) -> Result<Option<N::ReceiptResponse>>;
async fn get_block_receipts(&self, block: BlockTag) -> Result<Option<Vec<N::ReceiptResponse>>>;
async fn get_transaction(&self, tx_hash: B256) -> Result<Option<N::TransactionResponse>>;
async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>>;
async fn get_filter_changes(&self, filter_id: U256) -> Result<Vec<Log>>;
Expand Down
2 changes: 2 additions & 0 deletions helios-ts/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
7 changes: 7 additions & 0 deletions helios-ts/src/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsValue, JsError> {
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<JsValue, JsError> {
let filter: Filter = serde_wasm_bindgen::from_value(filter)?;
Expand Down
7 changes: 7 additions & 0 deletions helios-ts/src/opstack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsValue, JsError> {
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<JsValue, JsError> {
let filter: Filter = serde_wasm_bindgen::from_value(filter)?;
Expand Down
1 change: 1 addition & 0 deletions rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)` |
Expand Down
27 changes: 27 additions & 0 deletions tests/rpc_equivalence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit c34324c

Please sign in to comment.