diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index 442ac255ac50c..366c648af301c 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -178,6 +178,14 @@ pub enum EthRequest { #[serde(rename = "eth_getTransactionByHash", with = "sequence")] EthGetTransactionByHash(TxHash), + /// Returns the blob for a given blob versioned hash. + #[serde(rename = "anvil_getBlobByHash", with = "sequence")] + GetBlobByHash(B256), + + /// Returns the blobs for a given transaction hash. + #[serde(rename = "anvil_getBlobsByTransactionHash", with = "sequence")] + GetBlobByTransactionHash(TxHash), + #[serde(rename = "eth_getTransactionByBlockHashAndIndex")] EthGetTransactionByBlockHashAndIndex(TxHash, Index), diff --git a/crates/anvil/core/src/eth/transaction/mod.rs b/crates/anvil/core/src/eth/transaction/mod.rs index f4c2f34a7d930..a85d08e38c373 100644 --- a/crates/anvil/core/src/eth/transaction/mod.rs +++ b/crates/anvil/core/src/eth/transaction/mod.rs @@ -723,6 +723,16 @@ impl TypedTransaction { } } + pub fn sidecar(&self) -> Option<&TxEip4844WithSidecar> { + match self { + Self::EIP4844(signed_variant) => match signed_variant.tx() { + TxEip4844Variant::TxEip4844WithSidecar(with_sidecar) => Some(with_sidecar), + _ => None, + }, + _ => None, + } + } + pub fn max_fee_per_blob_gas(&self) -> Option { match self { Self::EIP4844(tx) => Some(tx.tx().tx().max_fee_per_blob_gas), diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index bd33d4f6060e9..344bec84ce8c2 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -33,7 +33,7 @@ use crate::{ mem::transaction_build, }; use alloy_consensus::{ - Account, + Account, Blob, transaction::{Recovered, eip4844::TxEip4844Variant}, }; use alloy_dyn_abi::TypedData; @@ -279,6 +279,12 @@ impl EthApi { EthRequest::EthGetRawTransactionByHash(hash) => { self.raw_transaction(hash).await.to_rpc_result() } + EthRequest::GetBlobByHash(hash) => { + self.anvil_get_blob_by_versioned_hash(hash).to_rpc_result() + } + EthRequest::GetBlobByTransactionHash(hash) => { + self.anvil_get_blob_by_tx_hash(hash).to_rpc_result() + } EthRequest::EthGetRawTransactionByBlockHashAndIndex(hash, index) => { self.raw_transaction_by_block_hash_and_index(hash, index).await.to_rpc_result() } @@ -1312,6 +1318,21 @@ impl EthApi { .map(U256::from) } + /// Handler for RPC call: `anvil_getBlobByHash` + pub fn anvil_get_blob_by_versioned_hash( + &self, + hash: B256, + ) -> Result> { + node_info!("anvil_getBlobByHash"); + Ok(self.backend.get_blob_by_versioned_hash(hash)?) + } + + /// Handler for RPC call: `anvil_getBlobsByTransactionHash` + pub fn anvil_get_blob_by_tx_hash(&self, hash: B256) -> Result>> { + node_info!("anvil_getBlobsByTransactionHash"); + Ok(self.backend.get_blob_by_tx_hash(hash)?) + } + /// Get transaction by its hash. /// /// This will check the storage for a matching transaction, if no transaction exists in storage diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 5b775838d625a..362f1d987dd48 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -34,12 +34,12 @@ use crate::{ }; use alloy_chains::NamedChain; use alloy_consensus::{ - Account, BlockHeader, EnvKzgSettings, Header, Receipt, ReceiptWithBloom, Signed, + Account, Blob, BlockHeader, EnvKzgSettings, Header, Receipt, ReceiptWithBloom, Signed, Transaction as TransactionTrait, TxEnvelope, proofs::{calculate_receipt_root, calculate_transaction_root}, transaction::Recovered, }; -use alloy_eips::{eip1559::BaseFeeParams, eip7840::BlobParams}; +use alloy_eips::{eip1559::BaseFeeParams, eip4844::kzg_to_versioned_hash, eip7840::BlobParams}; use alloy_evm::{Database, Evm, eth::EthEvmContext, precompiles::PrecompilesMap}; use alloy_network::{ AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, AnyTxType, @@ -2918,6 +2918,41 @@ impl Backend { )) } + pub fn get_blob_by_tx_hash(&self, hash: B256) -> Result>> { + // Try to get the mined transaction by hash + if let Some(tx) = self.mined_transaction_by_hash(hash) + && let Ok(typed_tx) = TypedTransaction::try_from(tx) + && let Some(sidecar) = typed_tx.sidecar() + { + return Ok(Some(sidecar.sidecar.blobs.clone())); + } + + Ok(None) + } + + pub fn get_blob_by_versioned_hash(&self, hash: B256) -> Result> { + let storage = self.blockchain.storage.read(); + for block in storage.blocks.values() { + for tx in &block.transactions { + let typed_tx = tx.as_ref(); + if let Some(sidecar) = typed_tx.sidecar() { + for versioned_hash in sidecar.sidecar.versioned_hashes() { + if versioned_hash == hash + && let Some(index) = + sidecar.sidecar.commitments.iter().position(|commitment| { + kzg_to_versioned_hash(commitment.as_slice()) == *hash + }) + && let Some(blob) = sidecar.sidecar.blobs.get(index) + { + return Ok(Some(*blob)); + } + } + } + } + } + Ok(None) + } + /// Prove an account's existence or nonexistence in the state trie. /// /// Returns a merkle proof of the account's trie node, `account_key` == keccak(address) diff --git a/crates/anvil/tests/it/eip4844.rs b/crates/anvil/tests/it/eip4844.rs index 3d512dbc333ff..292194aae61da 100644 --- a/crates/anvil/tests/it/eip4844.rs +++ b/crates/anvil/tests/it/eip4844.rs @@ -346,3 +346,78 @@ async fn can_bypass_sidecar_requirement() { assert_eq!(tx.inner.ty(), 3); } + +#[tokio::test(flavor = "multi_thread")] +async fn can_get_blobs_by_versioned_hash() { + let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into())); + let (api, handle) = spawn(node_config).await; + + let wallets = handle.dev_wallets().collect::>(); + let from = wallets[0].address(); + let to = wallets[1].address(); + let provider = http_provider(&handle.http_endpoint()); + + let eip1559_est = provider.estimate_eip1559_fees().await.unwrap(); + let gas_price = provider.get_gas_price().await.unwrap(); + + let sidecar: SidecarBuilder = SidecarBuilder::from_slice(b"Hello World"); + + let sidecar = sidecar.build().unwrap(); + let tx = TransactionRequest::default() + .with_from(from) + .with_to(to) + .with_nonce(0) + .with_max_fee_per_blob_gas(gas_price + 1) + .with_max_fee_per_gas(eip1559_est.max_fee_per_gas) + .with_max_priority_fee_per_gas(eip1559_est.max_priority_fee_per_gas) + .with_blob_sidecar(sidecar.clone()) + .value(U256::from(5)); + + let mut tx = WithOtherFields::new(tx); + + tx.populate_blob_hashes(); + + let _receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + + let hash = sidecar.versioned_hash_for_blob(0).unwrap(); + // api.anvil_set_auto_mine(true).await.unwrap(); + let blob = api.anvil_get_blob_by_versioned_hash(hash).unwrap().unwrap(); + assert_eq!(blob, sidecar.blobs[0]); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_get_blobs_by_tx_hash() { + let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into())); + let (api, handle) = spawn(node_config).await; + + let wallets = handle.dev_wallets().collect::>(); + let from = wallets[0].address(); + let to = wallets[1].address(); + let provider = http_provider(&handle.http_endpoint()); + + let eip1559_est = provider.estimate_eip1559_fees().await.unwrap(); + let gas_price = provider.get_gas_price().await.unwrap(); + + let sidecar: SidecarBuilder = SidecarBuilder::from_slice(b"Hello World"); + + let sidecar = sidecar.build().unwrap(); + let tx = TransactionRequest::default() + .with_from(from) + .with_to(to) + .with_nonce(0) + .with_max_fee_per_blob_gas(gas_price + 1) + .with_max_fee_per_gas(eip1559_est.max_fee_per_gas) + .with_max_priority_fee_per_gas(eip1559_est.max_priority_fee_per_gas) + .with_blob_sidecar(sidecar.clone()) + .value(U256::from(5)); + + let mut tx = WithOtherFields::new(tx); + + tx.populate_blob_hashes(); + + let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + let hash = receipt.transaction_hash; + api.anvil_set_auto_mine(true).await.unwrap(); + let blobs = api.anvil_get_blob_by_tx_hash(hash).unwrap().unwrap(); + assert_eq!(blobs, sidecar.blobs); +}