From fbd8c3467fb98acf4a3153d9c4ab4ae09d70eceb Mon Sep 17 00:00:00 2001 From: Enrique Date: Mon, 15 Jan 2024 13:49:54 -0400 Subject: [PATCH] chore(`anvil`): Add base alloy types (#6778) * chore: typedtx skeleton * feat: add all alloy tx types * chore: add op deposit tx support * chore: add block type * feat: proof type * chore: fix test * docs * chore: move optimism types to their own file * chore: more docs --- Cargo.lock | 52 +- Cargo.toml | 8 +- crates/anvil/core/Cargo.toml | 4 + crates/anvil/core/src/eth/alloy_block.rs | 271 +++++ crates/anvil/core/src/eth/alloy_proof.rs | 25 + crates/anvil/core/src/eth/mod.rs | 2 + .../anvil/core/src/eth/transaction/alloy.rs | 1073 +++++++++++++++++ crates/anvil/core/src/eth/transaction/mod.rs | 7 +- .../core/src/eth/transaction/optimism.rs | 179 +++ crates/anvil/core/src/eth/utils.rs | 8 + 10 files changed, 1608 insertions(+), 21 deletions(-) create mode 100644 crates/anvil/core/src/eth/alloy_block.rs create mode 100644 crates/anvil/core/src/eth/alloy_proof.rs create mode 100644 crates/anvil/core/src/eth/transaction/alloy.rs create mode 100644 crates/anvil/core/src/eth/transaction/optimism.rs diff --git a/Cargo.lock b/Cargo.lock index 8ce6db782467..ecafc175ce6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,6 +87,17 @@ dependencies = [ "strum", ] +[[package]] +name = "alloy-consensus" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" +dependencies = [ + "alloy-eips", + "alloy-network", + "alloy-primitives", + "alloy-rlp", +] + [[package]] name = "alloy-dyn-abi" version = "0.6.0" @@ -111,7 +122,7 @@ dependencies = [ [[package]] name = "alloy-eips" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -134,7 +145,7 @@ dependencies = [ [[package]] name = "alloy-json-rpc" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-primitives", "serde", @@ -145,7 +156,7 @@ dependencies = [ [[package]] name = "alloy-network" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-eips", "alloy-json-rpc", @@ -184,7 +195,7 @@ dependencies = [ [[package]] name = "alloy-providers" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-network", "alloy-primitives", @@ -203,7 +214,7 @@ dependencies = [ [[package]] name = "alloy-pubsub" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -241,7 +252,7 @@ dependencies = [ [[package]] name = "alloy-rpc-client" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -258,7 +269,7 @@ dependencies = [ [[package]] name = "alloy-rpc-trace-types" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-primitives", "alloy-rpc-types", @@ -269,7 +280,7 @@ dependencies = [ [[package]] name = "alloy-rpc-types" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -324,7 +335,7 @@ dependencies = [ [[package]] name = "alloy-transport" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-json-rpc", "base64 0.21.7", @@ -340,7 +351,7 @@ dependencies = [ [[package]] name = "alloy-transport-http" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -353,7 +364,7 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -371,7 +382,7 @@ dependencies = [ [[package]] name = "alloy-transport-ws" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy#44ddd61de3f4c7d6c6a021c65af5de25ad9871dd" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -526,7 +537,11 @@ dependencies = [ name = "anvil-core" version = "0.2.0" dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-network", "alloy-primitives", + "alloy-rlp", "alloy-rpc-trace-types", "alloy-rpc-types", "anvil-core", @@ -3717,9 +3732,9 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e1127ede0475b58f4fe9c0aaa0d9bb0bad2af90bbd93ccd307c8632b863d89" +checksum = "02b202d766a7fefc596e2cc6a89cda8ad8ad733aed82da635ac120691112a9b1" [[package]] name = "gix-utils" @@ -7928,9 +7943,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "8.2.8" +version = "8.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cf9c2670809c4840d4648fc7daa396551d7d88966f9ba93821b81a5c0c2d3f5" +checksum = "03c7b513a7d40506d5a07f186a298821cc9ec2cc22088a3593f7b612a66659f6" dependencies = [ "anyhow", "git2", @@ -8536,3 +8551,8 @@ dependencies = [ "cc", "pkg-config", ] + +[[patch.unused]] +name = "alloy-signer" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy#c5e9371f1c00e7fd700e28bf2e95fc5e671d93c4" diff --git a/Cargo.toml b/Cargo.toml index 21acb389c2b5..cac488bd48f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,12 +145,14 @@ ethers-solc = { version = "2.0", default-features = false } ## alloy alloy-consensus = "0.1.0" +alloy-network = "0.1.0" alloy-json-rpc = "0.1.0" alloy-providers = "0.1.0" alloy-pubsub = "0.1.0" alloy-rpc-client = "0.1.0" alloy-rpc-trace-types = "0.1.0" alloy-rpc-types = "0.1.0" +alloy-eips = "0.1.0" alloy-transport = "0.1.0" alloy-transport-http = "0.1.0" alloy-transport-ipc = "0.1.0" @@ -211,14 +213,16 @@ ethers-signers = { git = "https://github.com/gakonst/ethers-rs", rev = "f0e5b194 ethers-middleware = { git = "https://github.com/gakonst/ethers-rs", rev = "f0e5b194f09c533feb10d1a686ddb9e5946ec107" } ethers-solc = { git = "https://github.com/gakonst/ethers-rs", rev = "f0e5b194f09c533feb10d1a686ddb9e5946ec107" } -# alloy-consensus = { git = "https://github.com/alloy-rs/alloy" } +alloy-consensus = { git = "https://github.com/alloy-rs/alloy" } +alloy-network = { git = "https://github.com/alloy-rs/alloy" } alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy" } alloy-providers = { git = "https://github.com/alloy-rs/alloy" } alloy-pubsub = { git = "https://github.com/alloy-rs/alloy" } alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy" } alloy-rpc-trace-types = { git = "https://github.com/alloy-rs/alloy" } alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy" } -# alloy-signer = { git = "https://github.com/alloy-rs/alloy" } +alloy-eips = { git = "https://github.com/alloy-rs/alloy" } +alloy-signer = { git = "https://github.com/alloy-rs/alloy" } alloy-transport = { git = "https://github.com/alloy-rs/alloy" } alloy-transport-http = { git = "https://github.com/alloy-rs/alloy" } alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy" } diff --git a/crates/anvil/core/Cargo.toml b/crates/anvil/core/Cargo.toml index 1a69cd012a87..9fb87785cbca 100644 --- a/crates/anvil/core/Cargo.toml +++ b/crates/anvil/core/Cargo.toml @@ -17,6 +17,10 @@ revm = { workspace = true, default-features = false, features = ["std", "serde", alloy-primitives = { workspace = true, features = ["serde"] } alloy-rpc-types = { workspace = true } alloy-rpc-trace-types.workspace = true +alloy-rlp.workspace = true +alloy-eips.workspace = true +alloy-network = { workspace = true, features = ["k256"] } +alloy-consensus.workspace = true ethers-core = { workspace = true, features = ["optimism"] } # theses are not used by anvil-core, but are required by ethers, because pulled in via foundry-common ethers-contract = { workspace = true, features = ["optimism"] } diff --git a/crates/anvil/core/src/eth/alloy_block.rs b/crates/anvil/core/src/eth/alloy_block.rs new file mode 100644 index 000000000000..44536f886258 --- /dev/null +++ b/crates/anvil/core/src/eth/alloy_block.rs @@ -0,0 +1,271 @@ +use super::trie; +use alloy_consensus::Header; +use alloy_primitives::{Address, Bloom, Bytes, B256, U256}; +use alloy_rlp::{RlpDecodable, RlpEncodable}; +use foundry_common::types::ToAlloy; + +// Type alias to optionally support impersonated transactions +#[cfg(not(feature = "impersonated-tx"))] +type Transaction = crate::eth::transaction::alloy::TypedTransaction; +#[cfg(feature = "impersonated-tx")] +type Transaction = crate::eth::transaction::alloy::MaybeImpersonatedTransaction; + +/// An Ethereum Block +#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable)] +pub struct Block { + pub header: Header, + pub transactions: Vec, + pub ommers: Vec
, +} + +impl Block { + /// Creates a new block + /// + /// Note: if the `impersonate-tx` feature is enabled this will also accept + /// [MaybeImpersonatedTransaction] + pub fn new( + partial_header: PartialHeader, + transactions: impl IntoIterator, + ommers: Vec
, + ) -> Self + where + T: Into, + { + let transactions: Vec<_> = transactions.into_iter().map(Into::into).collect(); + let mut encoded_ommers: Vec = Vec::new(); + alloy_rlp::encode_list(&ommers, &mut encoded_ommers); + let ommers_hash = + B256::from_slice(alloy_primitives::utils::keccak256(encoded_ommers).as_slice()); + let transactions_root = + trie::ordered_trie_root(transactions.iter().map(|r| Bytes::from(alloy_rlp::encode(r)))) + .to_alloy(); + + Self { + header: Header { + parent_hash: partial_header.parent_hash, + beneficiary: partial_header.beneficiary, + ommers_hash, + state_root: partial_header.state_root, + transactions_root, + receipts_root: partial_header.receipts_root, + logs_bloom: partial_header.logs_bloom, + difficulty: partial_header.difficulty, + number: partial_header.number, + gas_limit: partial_header.gas_limit, + gas_used: partial_header.gas_used, + timestamp: partial_header.timestamp, + extra_data: partial_header.extra_data, + mix_hash: partial_header.mix_hash, + withdrawals_root: Some(partial_header.mix_hash), + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + nonce: partial_header.nonce, + base_fee_per_gas: partial_header.base_fee, + }, + transactions, + ommers, + } + } +} + +/// Partial header definition without ommers hash and transactions root +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PartialHeader { + pub parent_hash: B256, + pub beneficiary: Address, + pub state_root: B256, + pub receipts_root: B256, + pub logs_bloom: Bloom, + pub difficulty: U256, + pub number: u64, + pub gas_limit: u64, + pub gas_used: u64, + pub timestamp: u64, + pub extra_data: Bytes, + pub mix_hash: B256, + pub nonce: u64, + pub base_fee: Option, +} + +impl From
for PartialHeader { + fn from(value: Header) -> Self { + Self { + parent_hash: value.parent_hash, + beneficiary: value.beneficiary, + state_root: value.state_root, + receipts_root: value.receipts_root, + logs_bloom: value.logs_bloom, + difficulty: value.difficulty, + number: value.number, + gas_limit: value.gas_limit, + gas_used: value.gas_used, + timestamp: value.timestamp, + extra_data: value.extra_data, + mix_hash: value.mix_hash, + nonce: value.nonce, + base_fee: value.base_fee_per_gas, + } + } +} + +#[cfg(test)] +mod tests { + use alloy_network::Sealable; + use alloy_primitives::{ + b256, + hex::{self, FromHex}, + }; + use alloy_rlp::Decodable; + + use super::*; + use std::str::FromStr; + + #[test] + fn header_rlp_roundtrip() { + let mut header = Header { + parent_hash: Default::default(), + ommers_hash: Default::default(), + beneficiary: Default::default(), + state_root: Default::default(), + transactions_root: Default::default(), + receipts_root: Default::default(), + logs_bloom: Default::default(), + difficulty: Default::default(), + number: 124u64, + gas_limit: Default::default(), + gas_used: 1337u64, + timestamp: 0, + extra_data: Default::default(), + mix_hash: Default::default(), + nonce: 99u64, + withdrawals_root: Default::default(), + blob_gas_used: Default::default(), + excess_blob_gas: Default::default(), + parent_beacon_block_root: Default::default(), + base_fee_per_gas: None, + }; + + let encoded = alloy_rlp::encode(&header); + let decoded: Header =
::decode(&mut encoded.as_ref()).unwrap(); + assert_eq!(header, decoded); + + header.base_fee_per_gas = Some(12345u64); + + let encoded = alloy_rlp::encode(&header); + let decoded: Header =
::decode(&mut encoded.as_ref()).unwrap(); + assert_eq!(header, decoded); + } + + #[test] + fn test_encode_block_header() { + use alloy_rlp::Encodable; + + let expected = hex::decode("f901f9a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008208ae820d0582115c8215b3821a0a827788a00000000000000000000000000000000000000000000000000000000000000000880000000000000000").unwrap(); + let mut data = vec![]; + let header = Header { + parent_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + ommers_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + beneficiary: Address::from_str("0000000000000000000000000000000000000000").unwrap(), + state_root: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + transactions_root: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + receipts_root: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + logs_bloom: Bloom::from_hex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), + difficulty: U256::from(2222), + number: 0xd05u64, + gas_limit: 0x115cu64, + gas_used: 0x15b3u64, + timestamp: 0x1a0au64, + extra_data: hex::decode("7788").unwrap().into(), + mix_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + nonce: 0, + base_fee_per_gas: None, + }; + + header.encode(&mut data); + assert_eq!(hex::encode(&data), hex::encode(expected)); + assert_eq!(header.length(), data.len()); + } + + #[test] + // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 + fn test_decode_block_header() { + let data = hex::decode("f901f9a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008208ae820d0582115c8215b3821a0a827788a00000000000000000000000000000000000000000000000000000000000000000880000000000000000").unwrap(); + let expected = Header { + parent_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + ommers_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + beneficiary: Address::from_str("0000000000000000000000000000000000000000").unwrap(), + state_root: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + transactions_root: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + receipts_root: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + logs_bloom: <[u8; 256]>::from_hex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap().into(), + difficulty: U256::from(2222), + number: 0xd05u64, + gas_limit: 0x115cu64, + gas_used: 0x15b3u64, + timestamp: 0x1a0au64, + extra_data: hex::decode("7788").unwrap().into(), + mix_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + nonce: 0, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + base_fee_per_gas: None, + }; + let header =
::decode(&mut data.as_slice()).unwrap(); + assert_eq!(header, expected); + } + + #[test] + // Test vector from: https://github.com/ethereum/tests/blob/f47bbef4da376a49c8fc3166f09ab8a6d182f765/BlockchainTests/ValidBlocks/bcEIP1559/baseFee.json#L15-L36 + fn test_eip1559_block_header_hash() { + let expected_hash = + b256!("6a251c7c3c5dca7b42407a3752ff48f3bbca1fab7f9868371d9918daf1988d1f"); + let header = Header { + parent_hash: B256::from_str("e0a94a7a3c9617401586b1a27025d2d9671332d22d540e0af72b069170380f2a").unwrap(), + ommers_hash: B256::from_str("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347").unwrap(), + beneficiary: Address::from_str("ba5e000000000000000000000000000000000000").unwrap(), + state_root: B256::from_str("ec3c94b18b8a1cff7d60f8d258ec723312932928626b4c9355eb4ab3568ec7f7").unwrap(), + transactions_root: B256::from_str("50f738580ed699f0469702c7ccc63ed2e51bc034be9479b7bff4e68dee84accf").unwrap(), + receipts_root: B256::from_str("29b0562f7140574dd0d50dee8a271b22e1a0a7b78fca58f7c60370d8317ba2a9").unwrap(), + logs_bloom: Bloom::from_hex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), + difficulty: U256::from(0x020000), + number: 1u64, + gas_limit: U256::from(0x016345785d8a0000u128).to::(), + gas_used: U256::from(0x015534).to::(), + timestamp: 0x079e, + extra_data: hex::decode("42").unwrap().into(), + mix_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + nonce: 0, + base_fee_per_gas: Some(875), + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + }; + assert_eq!(header.hash(), expected_hash); + } + + #[test] + // Test vector from network + fn block_network_roundtrip() { + use alloy_rlp::Encodable; + + let data = hex::decode("f9034df90348a0fbdbd8d2d0ac5f14bd5fa90e547fe6f1d15019c724f8e7b60972d381cd5d9cf8a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794c9577e7945db22e38fc060909f2278c7746b0f9ba05017cfa3b0247e35197215ae8d610265ffebc8edca8ea66d6567eb0adecda867a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018355bb7b871fffffffffffff808462bd0e1ab9014bf90148a00000000000000000000000000000000000000000000000000000000000000000f85494319fa8f1bc4e53410e92d10d918659b16540e60a945a573efb304d04c1224cd012313e827eca5dce5d94a9c831c5a268031176ebf5f3de5051e8cba0dbfe94c9577e7945db22e38fc060909f2278c7746b0f9b808400000000f8c9b841a6946f2d16f68338cbcbd8b117374ab421128ce422467088456bceba9d70c34106128e6d4564659cf6776c08a4186063c0a05f7cffd695c10cf26a6f301b67f800b8412b782100c18c35102dc0a37ece1a152544f04ad7dc1868d18a9570f744ace60870f822f53d35e89a2ea9709ccbf1f4a25ee5003944faa845d02dde0a41d5704601b841d53caebd6c8a82456e85c2806a9e08381f959a31fb94a77e58f00e38ad97b2e0355b8519ab2122662cbe022f2a4ef7ff16adc0b2d5dcd123181ec79705116db300a063746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365880000000000000000c0c0").unwrap(); + + let block = ::decode(&mut data.as_slice()).unwrap(); + + // encode and check that it matches the original data + let mut encoded = Vec::new(); + block.encode(&mut encoded); + assert_eq!(data, encoded); + + // check that length of encoding is the same as the output of `length` + assert_eq!(block.length(), encoded.len()); + } +} diff --git a/crates/anvil/core/src/eth/alloy_proof.rs b/crates/anvil/core/src/eth/alloy_proof.rs new file mode 100644 index 000000000000..adef84ae93ef --- /dev/null +++ b/crates/anvil/core/src/eth/alloy_proof.rs @@ -0,0 +1,25 @@ +//! Return types for `eth_getProof` + +use crate::eth::trie::KECCAK_NULL_RLP; +use alloy_primitives::{B256, U256}; +use foundry_common::types::ToAlloy; +use revm::primitives::KECCAK_EMPTY; + +#[derive(Clone, Debug, PartialEq, Eq, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)] +pub struct BasicAccount { + pub nonce: U256, + pub balance: U256, + pub storage_root: B256, + pub code_hash: B256, +} + +impl Default for BasicAccount { + fn default() -> Self { + BasicAccount { + balance: U256::ZERO, + nonce: U256::ZERO, + code_hash: KECCAK_EMPTY, + storage_root: KECCAK_NULL_RLP.to_alloy(), + } + } +} diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index e6fb8789fcd7..799c6ff6b359 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -11,6 +11,8 @@ use alloy_rpc_types::{ }; use ethers_core::types::transaction::eip712::TypedData; +pub mod alloy_block; +pub mod alloy_proof; pub mod block; pub mod proof; pub mod receipt; diff --git a/crates/anvil/core/src/eth/transaction/alloy.rs b/crates/anvil/core/src/eth/transaction/alloy.rs new file mode 100644 index 000000000000..d0608fc5f33d --- /dev/null +++ b/crates/anvil/core/src/eth/transaction/alloy.rs @@ -0,0 +1,1073 @@ +use crate::eth::{ + transaction::optimism::{DepositTransaction, DepositTransactionRequest}, + utils::eip_to_revm_access_list, +}; +use alloy_consensus::{ReceiptWithBloom, TxEip1559, TxEip2930, TxLegacy}; +use alloy_network::{Signed, Transaction, TxKind}; +use alloy_primitives::{Address, Bloom, Bytes, Log, Signature, TxHash, B256, U256}; +use alloy_rlp::{Decodable, Encodable}; +use alloy_rpc_types::{request::TransactionRequest, AccessList, CallRequest}; +use foundry_evm::traces::CallTraceNode; +use revm::{ + interpreter::InstructionResult, + primitives::{CreateScheme, OptimismFields, TransactTo, TxEnv}, +}; +use std::ops::Deref; + +/// The signature used to bypass signing via the `eth_sendUnsignedTransaction` cheat RPC +#[cfg(feature = "impersonated-tx")] +pub fn impersonated_signature() -> Signature { + Signature::from_scalars_and_parity(B256::ZERO, B256::ZERO, false).unwrap() +} + +pub fn transaction_request_to_typed(tx: TransactionRequest) -> Option { + let TransactionRequest { + from, + to, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + gas, + value, + data, + nonce, + mut access_list, + transaction_type, + other, + .. + } = tx; + let transaction_type = transaction_type.map(|id| id.to::()); + + // Special case: OP-stack deposit tx + if transaction_type == Some(126) { + return Some(TypedTransactionRequest::Deposit(DepositTransactionRequest { + from: from.unwrap_or_default(), + source_hash: other.get_deserialized::("sourceHash")?.ok()?, + kind: TxKind::Create, + mint: other.get_deserialized::("mint")?.ok()?, + value: value.unwrap_or_default(), + gas_limit: gas.unwrap_or_default(), + is_system_tx: other.get_deserialized::("isSystemTx")?.ok()?, + input: data.unwrap_or_default(), + })) + } + + match ( + transaction_type, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + access_list.take(), + ) { + // legacy transaction + (Some(0), _, None, None, None) | (None, Some(_), None, None, None) => { + Some(TypedTransactionRequest::Legacy(TxLegacy { + nonce: nonce.unwrap_or_default().to::(), + gas_price: gas_price.unwrap_or_default().to::(), + gas_limit: gas.unwrap_or_default().to::(), + value: value.unwrap_or(U256::ZERO), + input: data.unwrap_or_default(), + to: match to { + Some(to) => TxKind::Call(to), + None => TxKind::Create, + }, + chain_id: None, + })) + } + // EIP2930 + (Some(1), _, None, None, _) | (None, _, None, None, Some(_)) => { + Some(TypedTransactionRequest::EIP2930(TxEip2930 { + nonce: nonce.unwrap_or_default().to::(), + gas_price: gas_price.unwrap_or_default().to(), + gas_limit: gas.unwrap_or_default().to::(), + value: value.unwrap_or(U256::ZERO), + input: data.unwrap_or_default(), + to: match to { + Some(to) => TxKind::Call(to), + None => TxKind::Create, + }, + chain_id: 0, + access_list: to_eip_access_list(access_list.unwrap_or_default()), + })) + } + // EIP1559 + (Some(2), None, _, _, _) | + (None, None, Some(_), _, _) | + (None, None, _, Some(_), _) | + (None, None, None, None, None) => { + // Empty fields fall back to the canonical transaction schema. + Some(TypedTransactionRequest::EIP1559(TxEip1559 { + nonce: nonce.unwrap_or_default().to::(), + max_fee_per_gas: max_fee_per_gas.unwrap_or_default().to::(), + max_priority_fee_per_gas: max_priority_fee_per_gas.unwrap_or_default().to::(), + gas_limit: gas.unwrap_or_default().to::(), + value: value.unwrap_or(U256::ZERO), + input: data.unwrap_or_default(), + to: match to { + Some(to) => TxKind::Call(to), + None => TxKind::Create, + }, + chain_id: 0, + access_list: to_eip_access_list(access_list.unwrap_or_default()), + })) + } + _ => None, + } +} + +pub fn call_request_to_typed(tx: CallRequest) -> Option { + let CallRequest { + to, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + gas, + value, + input, + chain_id, + nonce, + mut access_list, + transaction_type, + .. + } = tx; + let chain_id = chain_id.map(|id| id.to::()); + let transaction_type = transaction_type.map(|id| id.to::()); + + match ( + transaction_type, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + access_list.take(), + ) { + // legacy transaction + (Some(0), _, None, None, None) | (None, Some(_), None, None, None) => { + Some(TypedTransactionRequest::Legacy(TxLegacy { + nonce: nonce.unwrap_or_default().to::(), + gas_price: gas_price.unwrap_or_default().to::(), + gas_limit: gas.unwrap_or_default().to::(), + value: value.unwrap_or(U256::ZERO), + input: input.try_into_unique_input().unwrap_or_default().unwrap_or_default(), + to: match to { + Some(to) => TxKind::Call(to), + None => TxKind::Create, + }, + chain_id, + })) + } + // EIP2930 + (Some(1), _, None, None, _) | (None, _, None, None, Some(_)) => { + Some(TypedTransactionRequest::EIP2930(TxEip2930 { + nonce: nonce.unwrap_or_default().to::(), + gas_price: gas_price.unwrap_or_default().to(), + gas_limit: gas.unwrap_or_default().to::(), + value: value.unwrap_or(U256::ZERO), + input: input.try_into_unique_input().unwrap_or_default().unwrap_or_default(), + to: match to { + Some(to) => TxKind::Call(to), + None => TxKind::Create, + }, + chain_id: chain_id.unwrap_or_default(), + access_list: to_eip_access_list(access_list.unwrap_or_default()), + })) + } + // EIP1559 + (Some(2), None, _, _, _) | + (None, None, Some(_), _, _) | + (None, None, _, Some(_), _) | + (None, None, None, None, None) => { + // Empty fields fall back to the canonical transaction schema. + Some(TypedTransactionRequest::EIP1559(TxEip1559 { + nonce: nonce.unwrap_or_default().to::(), + max_fee_per_gas: max_fee_per_gas.unwrap_or_default().to::(), + max_priority_fee_per_gas: max_priority_fee_per_gas.unwrap_or_default().to::(), + gas_limit: gas.unwrap_or_default().to::(), + value: value.unwrap_or(U256::ZERO), + input: input.try_into_unique_input().unwrap_or_default().unwrap_or_default(), + to: match to { + Some(to) => TxKind::Call(to), + None => TxKind::Create, + }, + chain_id: chain_id.unwrap_or_default(), + access_list: to_eip_access_list(access_list.unwrap_or_default()), + })) + } + _ => None, + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TypedTransactionRequest { + Legacy(TxLegacy), + EIP2930(TxEip2930), + EIP1559(TxEip1559), + Deposit(DepositTransactionRequest), +} + +/// A wrapper for [TypedTransaction] that allows impersonating accounts. +/// +/// This is a helper that carries the `impersonated` sender so that the right hash +/// [TypedTransaction::impersonated_hash] can be created. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MaybeImpersonatedTransaction { + pub transaction: TypedTransaction, + pub impersonated_sender: Option
, +} + +impl MaybeImpersonatedTransaction { + /// Creates a new wrapper for the given transaction + pub fn new(transaction: TypedTransaction) -> Self { + Self { transaction, impersonated_sender: None } + } + + /// Creates a new impersonated transaction wrapper using the given sender + pub fn impersonated(transaction: TypedTransaction, impersonated_sender: Address) -> Self { + Self { transaction, impersonated_sender: Some(impersonated_sender) } + } + + /// Recovers the Ethereum address which was used to sign the transaction. + /// + /// Note: this is feature gated so it does not conflict with the `Deref`ed + /// [TypedTransaction::recover] function by default. + #[cfg(feature = "impersonated-tx")] + pub fn recover(&self) -> Result { + if let Some(sender) = self.impersonated_sender { + return Ok(sender) + } + self.transaction.recover() + } + + /// Returns the hash of the transaction + /// + /// Note: this is feature gated so it does not conflict with the `Deref`ed + /// [TypedTransaction::hash] function by default. + #[cfg(feature = "impersonated-tx")] + pub fn hash(&self) -> B256 { + if self.transaction.is_impersonated() { + if let Some(sender) = self.impersonated_sender { + return self.transaction.impersonated_hash(sender) + } + } + self.transaction.hash() + } +} + +impl Encodable for MaybeImpersonatedTransaction { + fn encode(&self, out: &mut dyn bytes::BufMut) { + self.transaction.encode(out) + } +} + +impl From for TypedTransaction { + fn from(value: MaybeImpersonatedTransaction) -> Self { + value.transaction + } +} + +impl From for MaybeImpersonatedTransaction { + fn from(value: TypedTransaction) -> Self { + MaybeImpersonatedTransaction::new(value) + } +} + +impl Decodable for MaybeImpersonatedTransaction { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + TypedTransaction::decode(buf).map(MaybeImpersonatedTransaction::new) + } +} + +impl AsRef for MaybeImpersonatedTransaction { + fn as_ref(&self) -> &TypedTransaction { + &self.transaction + } +} + +impl Deref for MaybeImpersonatedTransaction { + type Target = TypedTransaction; + + fn deref(&self) -> &Self::Target { + &self.transaction + } +} + +/// Queued transaction +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PendingTransaction { + /// The actual transaction + pub transaction: MaybeImpersonatedTransaction, + /// the recovered sender of this transaction + sender: Address, + /// hash of `transaction`, so it can easily be reused with encoding and hashing agan + hash: TxHash, +} + +impl PendingTransaction { + pub fn new(transaction: TypedTransaction) -> Result { + let sender = transaction.recover()?; + let hash = transaction.hash(); + Ok(Self { transaction: MaybeImpersonatedTransaction::new(transaction), sender, hash }) + } + + #[cfg(feature = "impersonated-tx")] + pub fn with_impersonated(transaction: TypedTransaction, sender: Address) -> Self { + let hash = transaction.impersonated_hash(sender); + Self { + transaction: MaybeImpersonatedTransaction::impersonated(transaction, sender), + sender, + hash, + } + } + + pub fn nonce(&self) -> U256 { + self.transaction.nonce() + } + + pub fn hash(&self) -> &TxHash { + &self.hash + } + + pub fn sender(&self) -> &Address { + &self.sender + } + + /// Converts the [PendingTransaction] into the [TxEnv] context that [`revm`](foundry_evm) + /// expects. + pub fn to_revm_tx_env(&self) -> TxEnv { + fn transact_to(kind: &TxKind) -> TransactTo { + match kind { + TxKind::Call(c) => TransactTo::Call(*c), + TxKind::Create => TransactTo::Create(CreateScheme::Create), + } + } + + let caller = *self.sender(); + match &self.transaction.transaction { + TypedTransaction::Legacy(tx) => { + let chain_id = tx.chain_id(); + let TxLegacy { nonce, gas_price, gas_limit, value, to, input, .. } = tx.tx(); + TxEnv { + caller, + transact_to: transact_to(to), + data: alloy_primitives::Bytes(input.0.clone()), + chain_id, + nonce: Some(*nonce), + value: (*value), + gas_price: U256::from(*gas_price), + gas_priority_fee: None, + gas_limit: *gas_limit, + access_list: vec![], + ..Default::default() + } + } + TypedTransaction::EIP2930(tx) => { + let TxEip2930 { + chain_id, + nonce, + gas_price, + gas_limit, + to, + value, + input, + access_list, + .. + } = tx.tx(); + TxEnv { + caller, + transact_to: transact_to(to), + data: alloy_primitives::Bytes(input.0.clone()), + chain_id: Some(*chain_id), + nonce: Some(*nonce), + value: *value, + gas_price: U256::from(*gas_price), + gas_priority_fee: None, + gas_limit: *gas_limit, + access_list: eip_to_revm_access_list(access_list.0.clone()), + ..Default::default() + } + } + TypedTransaction::EIP1559(tx) => { + let TxEip1559 { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit, + to, + value, + input, + access_list, + .. + } = tx.tx(); + TxEnv { + caller, + transact_to: transact_to(to), + data: alloy_primitives::Bytes(input.0.clone()), + chain_id: Some(*chain_id), + nonce: Some(*nonce), + value: *value, + gas_price: U256::from(*max_fee_per_gas), + gas_priority_fee: Some(U256::from(*max_priority_fee_per_gas)), + gas_limit: *gas_limit, + access_list: eip_to_revm_access_list(access_list.0.clone()), + ..Default::default() + } + } + TypedTransaction::Deposit(tx) => { + let chain_id = tx.chain_id(); + let DepositTransaction { + nonce, + source_hash, + gas_limit, + value, + kind, + mint, + input, + is_system_tx, + .. + } = tx; + TxEnv { + caller, + transact_to: transact_to(kind), + data: alloy_primitives::Bytes(input.0.clone()), + chain_id, + nonce: Some(nonce.to::()), + value: *value, + gas_price: U256::ZERO, + gas_priority_fee: None, + gas_limit: gas_limit.to::(), + access_list: vec![], + optimism: OptimismFields { + source_hash: Some(*source_hash), + mint: Some(mint.to::()), + is_system_transaction: Some(*is_system_tx), + enveloped_tx: None, + }, + ..Default::default() + } + } + } + } +} + +/// Container type for signed, typed transactions. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TypedTransaction { + /// Legacy transaction type + Legacy(Signed), + /// EIP-2930 transaction + EIP2930(Signed), + /// EIP-1559 transaction + EIP1559(Signed), + /// op-stack deposit transaction + Deposit(DepositTransaction), +} + +impl TypedTransaction { + /// Returns true if the transaction uses dynamic fees: EIP1559 + pub fn is_dynamic_fee(&self) -> bool { + matches!(self, TypedTransaction::EIP1559(_)) + } + + pub fn gas_price(&self) -> U256 { + U256::from(match self { + TypedTransaction::Legacy(tx) => tx.gas_price, + TypedTransaction::EIP2930(tx) => tx.gas_price, + TypedTransaction::EIP1559(tx) => tx.max_fee_per_gas, + TypedTransaction::Deposit(_) => 0, + }) + } + + pub fn gas_limit(&self) -> U256 { + U256::from(match self { + TypedTransaction::Legacy(tx) => tx.gas_limit, + TypedTransaction::EIP2930(tx) => tx.gas_limit, + TypedTransaction::EIP1559(tx) => tx.gas_limit, + TypedTransaction::Deposit(tx) => tx.gas_limit.to::(), + }) + } + + pub fn value(&self) -> U256 { + U256::from(match self { + TypedTransaction::Legacy(tx) => tx.value, + TypedTransaction::EIP2930(tx) => tx.value, + TypedTransaction::EIP1559(tx) => tx.value, + TypedTransaction::Deposit(tx) => tx.value, + }) + } + + pub fn data(&self) -> &Bytes { + match self { + TypedTransaction::Legacy(tx) => &tx.input, + TypedTransaction::EIP2930(tx) => &tx.input, + TypedTransaction::EIP1559(tx) => &tx.input, + TypedTransaction::Deposit(tx) => &tx.input, + } + } + + /// Returns the transaction type + pub fn r#type(&self) -> Option { + match self { + TypedTransaction::Legacy(_) => None, + TypedTransaction::EIP2930(_) => Some(1), + TypedTransaction::EIP1559(_) => Some(2), + TypedTransaction::Deposit(_) => Some(0x7E), + } + } + + /// Max cost of the transaction + pub fn max_cost(&self) -> U256 { + self.gas_limit().saturating_mul(self.gas_price()) + } + + /// Returns a helper type that contains commonly used values as fields + pub fn essentials(&self) -> TransactionEssentials { + match self { + TypedTransaction::Legacy(t) => TransactionEssentials { + kind: t.tx().to, + input: t.input.clone(), + nonce: U256::from(t.tx().nonce), + gas_limit: U256::from(t.tx().gas_limit), + gas_price: Some(U256::from(t.tx().gas_price)), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + value: t.value, + chain_id: t.tx().chain_id, + access_list: Default::default(), + }, + TypedTransaction::EIP2930(t) => TransactionEssentials { + kind: t.tx().to, + input: t.input.clone(), + nonce: U256::from(t.tx().nonce), + gas_limit: U256::from(t.tx().gas_limit), + gas_price: Some(U256::from(t.tx().gas_price)), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + value: t.value, + chain_id: Some(t.chain_id), + access_list: to_alloy_access_list(t.access_list.clone()), + }, + TypedTransaction::EIP1559(t) => TransactionEssentials { + kind: t.to, + input: t.input.clone(), + nonce: U256::from(t.nonce), + gas_limit: U256::from(t.gas_limit), + gas_price: None, + max_fee_per_gas: Some(U256::from(t.max_fee_per_gas)), + max_priority_fee_per_gas: Some(U256::from(t.max_priority_fee_per_gas)), + value: t.value, + chain_id: Some(t.chain_id), + access_list: to_alloy_access_list(t.access_list.clone()), + }, + TypedTransaction::Deposit(t) => TransactionEssentials { + kind: t.kind, + input: t.input.clone(), + nonce: t.nonce, + gas_limit: t.gas_limit, + gas_price: Some(U256::from(0)), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + value: t.value, + chain_id: t.chain_id(), + access_list: Default::default(), + }, + } + } + + pub fn nonce(&self) -> U256 { + match self { + TypedTransaction::Legacy(t) => U256::from(t.nonce), + TypedTransaction::EIP2930(t) => U256::from(t.nonce), + TypedTransaction::EIP1559(t) => U256::from(t.nonce), + TypedTransaction::Deposit(t) => U256::from(t.nonce), + } + } + + pub fn chain_id(&self) -> Option { + match self { + TypedTransaction::Legacy(t) => t.chain_id, + TypedTransaction::EIP2930(t) => Some(t.chain_id), + TypedTransaction::EIP1559(t) => Some(t.chain_id), + TypedTransaction::Deposit(t) => t.chain_id(), + } + } + + pub fn as_legacy(&self) -> Option<&Signed> { + match self { + TypedTransaction::Legacy(tx) => Some(tx), + _ => None, + } + } + + /// Returns true whether this tx is a legacy transaction + pub fn is_legacy(&self) -> bool { + matches!(self, TypedTransaction::Legacy(_)) + } + + /// Returns true whether this tx is a EIP1559 transaction + pub fn is_eip1559(&self) -> bool { + matches!(self, TypedTransaction::EIP1559(_)) + } + + /// Returns the hash of the transaction. + /// + /// Note: If this transaction has the Impersonated signature then this returns a modified unique + /// hash. This allows us to treat impersonated transactions as unique. + pub fn hash(&self) -> B256 { + match self { + TypedTransaction::Legacy(t) => *t.hash(), + TypedTransaction::EIP2930(t) => *t.hash(), + TypedTransaction::EIP1559(t) => *t.hash(), + TypedTransaction::Deposit(t) => t.hash(), + } + } + + /// Returns true if the transaction was impersonated (using the impersonate Signature) + #[cfg(feature = "impersonated-tx")] + pub fn is_impersonated(&self) -> bool { + self.signature() == impersonated_signature() + } + + /// Returns the hash if the transaction is impersonated (using a fake signature) + /// + /// This appends the `address` before hashing it + #[cfg(feature = "impersonated-tx")] + pub fn impersonated_hash(&self, sender: Address) -> B256 { + let mut buffer = Vec::::new(); + Encodable::encode(self, &mut buffer); + buffer.extend_from_slice(sender.as_ref()); + B256::from_slice(alloy_primitives::utils::keccak256(&buffer).as_slice()) + } + + /// Recovers the Ethereum address which was used to sign the transaction. + pub fn recover(&self) -> Result { + match self { + TypedTransaction::Legacy(tx) => tx.recover_signer(), + TypedTransaction::EIP2930(tx) => tx.recover_signer(), + TypedTransaction::EIP1559(tx) => tx.recover_signer(), + TypedTransaction::Deposit(tx) => tx.recover(), + } + } + + /// Returns what kind of transaction this is + pub fn kind(&self) -> &TxKind { + match self { + TypedTransaction::Legacy(tx) => &tx.to, + TypedTransaction::EIP2930(tx) => &tx.to, + TypedTransaction::EIP1559(tx) => &tx.to, + TypedTransaction::Deposit(tx) => &tx.kind, + } + } + + /// Returns the callee if this transaction is a call + pub fn to(&self) -> Option
{ + self.kind().to() + } + + /// Returns the Signature of the transaction + pub fn signature(&self) -> Signature { + match self { + TypedTransaction::Legacy(tx) => *tx.signature(), + TypedTransaction::EIP2930(tx) => *tx.signature(), + TypedTransaction::EIP1559(tx) => *tx.signature(), + TypedTransaction::Deposit(_) => { + Signature::from_scalars_and_parity(B256::ZERO, B256::ZERO, false).unwrap() + } + } + } +} + +impl Encodable for TypedTransaction { + fn encode(&self, out: &mut dyn bytes::BufMut) { + match self { + TypedTransaction::Legacy(tx) => tx.encode(out), + TypedTransaction::EIP2930(tx) => tx.encode(out), + TypedTransaction::EIP1559(tx) => tx.encode(out), + TypedTransaction::Deposit(tx) => tx.encode(out), + } + } +} + +impl Decodable for TypedTransaction { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + use bytes::Buf; + use std::cmp::Ordering; + + let first = *buf.first().ok_or(alloy_rlp::Error::Custom("empty slice"))?; + + // a signed transaction is either encoded as a string (non legacy) or a list (legacy). + // We should not consume the buffer if we are decoding a legacy transaction, so let's + // check if the first byte is between 0x80 and 0xbf. + match first.cmp(&alloy_rlp::EMPTY_LIST_CODE) { + Ordering::Less => { + // strip out the string header + // NOTE: typed transaction encodings either contain a "rlp header" which contains + // the type of the payload and its length, or they do not contain a header and + // start with the tx type byte. + // + // This line works for both types of encodings because byte slices starting with + // 0x01 and 0x02 return a Header { list: false, payload_length: 1 } when input to + // Header::decode. + // If the encoding includes a header, the header will be properly decoded and + // consumed. + // Otherwise, header decoding will succeed but nothing is consumed. + let _header = alloy_rlp::Header::decode(buf)?; + let tx_type = *buf.first().ok_or(alloy_rlp::Error::Custom( + "typed tx cannot be decoded from an empty slice", + ))?; + if tx_type == 0x01 { + buf.advance(1); + as Decodable>::decode(buf).map(TypedTransaction::EIP2930) + } else if tx_type == 0x02 { + buf.advance(1); + as Decodable>::decode(buf).map(TypedTransaction::EIP1559) + } else if tx_type == 0x7E { + buf.advance(1); + ::decode(buf).map(TypedTransaction::Deposit) + } else { + Err(alloy_rlp::Error::Custom("invalid tx type")) + } + } + Ordering::Equal => { + Err(alloy_rlp::Error::Custom("an empty list is not a valid transaction encoding")) + } + Ordering::Greater => { + as Decodable>::decode(buf).map(TypedTransaction::Legacy) + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TransactionEssentials { + pub kind: TxKind, + pub input: Bytes, + pub nonce: U256, + pub gas_limit: U256, + pub gas_price: Option, + pub max_fee_per_gas: Option, + pub max_priority_fee_per_gas: Option, + pub value: U256, + pub chain_id: Option, + pub access_list: AccessList, +} + +/// Represents all relevant information of an executed transaction +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TransactionInfo { + pub transaction_hash: B256, + pub transaction_index: u32, + pub from: Address, + pub to: Option
, + pub contract_address: Option
, + pub logs: Vec, + pub logs_bloom: Bloom, + pub traces: Vec, + pub exit: InstructionResult, + pub out: Option, + pub nonce: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TypedReceipt { + Legacy(ReceiptWithBloom), + EIP2930(ReceiptWithBloom), + EIP1559(ReceiptWithBloom), + Deposit(ReceiptWithBloom), +} + +impl TypedReceipt { + pub fn gas_used(&self) -> U256 { + match self { + TypedReceipt::Legacy(r) | + TypedReceipt::EIP1559(r) | + TypedReceipt::EIP2930(r) | + TypedReceipt::Deposit(r) => U256::from(r.receipt.cumulative_gas_used), + } + } + + pub fn logs_bloom(&self) -> &Bloom { + match self { + TypedReceipt::Legacy(r) | + TypedReceipt::EIP1559(r) | + TypedReceipt::EIP2930(r) | + TypedReceipt::Deposit(r) => &r.bloom, + } + } +} + +impl Encodable for TypedReceipt { + fn encode(&self, out: &mut dyn bytes::BufMut) { + use alloy_rlp::Header; + + match self { + TypedReceipt::Legacy(r) => r.encode(out), + receipt => { + let payload_len = match receipt { + TypedReceipt::EIP2930(r) => r.length() + 1, + TypedReceipt::EIP1559(r) => r.length() + 1, + TypedReceipt::Deposit(r) => r.length() + 1, + _ => unreachable!("receipt already matched"), + }; + + match receipt { + TypedReceipt::EIP2930(r) => { + Header { list: true, payload_length: payload_len }.encode(out); + 1u8.encode(out); + r.encode(out); + } + TypedReceipt::EIP1559(r) => { + Header { list: true, payload_length: payload_len }.encode(out); + 2u8.encode(out); + r.encode(out); + } + TypedReceipt::Deposit(r) => { + Header { list: true, payload_length: payload_len }.encode(out); + 0x7Eu8.encode(out); + r.encode(out); + } + _ => unreachable!("receipt already matched"), + } + } + } + } +} + +impl Decodable for TypedReceipt { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + use alloy_rlp::Header; + use bytes::Buf; + use std::cmp::Ordering; + + // a receipt is either encoded as a string (non legacy) or a list (legacy). + // We should not consume the buffer if we are decoding a legacy receipt, so let's + // check if the first byte is between 0x80 and 0xbf. + let rlp_type = *buf + .first() + .ok_or(alloy_rlp::Error::Custom("cannot decode a receipt from empty bytes"))?; + + match rlp_type.cmp(&alloy_rlp::EMPTY_LIST_CODE) { + Ordering::Less => { + // strip out the string header + let _header = Header::decode(buf)?; + let receipt_type = *buf.first().ok_or(alloy_rlp::Error::Custom( + "typed receipt cannot be decoded from an empty slice", + ))?; + if receipt_type == 0x01 { + buf.advance(1); + ::decode(buf).map(TypedReceipt::EIP2930) + } else if receipt_type == 0x02 { + buf.advance(1); + ::decode(buf).map(TypedReceipt::EIP1559) + } else if receipt_type == 0x7E { + buf.advance(1); + ::decode(buf).map(TypedReceipt::Deposit) + } else { + Err(alloy_rlp::Error::Custom("invalid receipt type")) + } + } + Ordering::Equal => { + Err(alloy_rlp::Error::Custom("an empty list is not a valid receipt encoding")) + } + Ordering::Greater => { + ::decode(buf).map(TypedReceipt::Legacy) + } + } + } +} + +/// Translates an EIP-2930 access list to an alloy-rpc-types access list. +pub fn to_alloy_access_list( + access_list: alloy_eips::eip2930::AccessList, +) -> alloy_rpc_types::AccessList { + alloy_rpc_types::AccessList( + access_list + .0 + .into_iter() + .map(|item| alloy_rpc_types::AccessListItem { + address: item.address, + storage_keys: item.storage_keys, + }) + .collect(), + ) +} + +/// Translates an alloy-rpc-types access list to an EIP-2930 access list. +pub fn to_eip_access_list( + access_list: alloy_rpc_types::AccessList, +) -> alloy_eips::eip2930::AccessList { + alloy_eips::eip2930::AccessList( + access_list + .0 + .into_iter() + .map(|item| alloy_eips::eip2930::AccessListItem { + address: item.address, + storage_keys: item.storage_keys, + }) + .collect(), + ) +} + +#[cfg(test)] +mod tests { + use alloy_consensus::Receipt; + use alloy_primitives::{b256, hex, LogData, Signature}; + use std::str::FromStr; + + use super::*; + + #[test] + fn test_decode_call() { + let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..]; + let decoded = TypedTransaction::decode(&mut &bytes_first[..]).unwrap(); + println!("{:?}", hex::encode(decoded.signature().as_bytes())); + let tx = TxLegacy { + nonce: 2u64, + gas_price: 1000000000u64.into(), + gas_limit: 100000u64, + to: TxKind::Call(Address::from_slice( + &hex::decode("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap()[..], + )), + value: U256::from(1000000000000000u64), + input: Bytes::default(), + chain_id: None, + }; + + let signature = Signature::from_str("0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b").unwrap(); + + let tx = TypedTransaction::Legacy(Signed::new_unchecked( + tx.clone(), + signature, + b256!("a517b206d2223278f860ea017d3626cacad4f52ff51030dc9a96b432f17f8d34"), + )); + + println!("{:#?}", decoded); + assert_eq!(tx, decoded); + } + + #[test] + fn test_decode_create_goerli() { + // test that an example create tx from goerli decodes properly + let tx_bytes = + hex::decode("02f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471") + .unwrap(); + let _decoded = + ::decode(&mut &tx_bytes[..]).unwrap(); + } + + #[test] + fn can_recover_sender() { + // random mainnet tx: https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f + let bytes = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap(); + + let Ok(TypedTransaction::EIP1559(tx)) = TypedTransaction::decode(&mut &bytes[..]) else { + panic!("decoding TypedTransaction failed"); + }; + + assert_eq!( + tx.hash(), + &"0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f" + .parse::() + .unwrap() + ); + assert_eq!( + tx.recover_signer().unwrap(), + "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5".parse::
().unwrap() + ); + } + + #[test] + fn can_recover_sender_not_normalized() { + let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap(); + + let Ok(TypedTransaction::Legacy(tx)) = TypedTransaction::decode(&mut &bytes[..]) else { + panic!("decoding TypedTransaction failed"); + }; + + assert_eq!(tx.input, Bytes::from(b"")); + assert_eq!(tx.gas_price, 1); + assert_eq!(tx.gas_limit, 21000); + assert_eq!(tx.nonce, 0); + if let TxKind::Call(to) = tx.to { + assert_eq!( + to, + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87".parse::
().unwrap() + ); + } else { + panic!("expected a call transaction"); + } + assert_eq!(tx.value, U256::from(0x0au64)); + assert_eq!( + tx.recover_signer().unwrap(), + "0f65fe9276bc9a24ae7083ae28e2660ef72df99e".parse::
().unwrap() + ); + } + + #[test] + fn encode_legacy_receipt() { + let expected = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap(); + + let mut data = vec![]; + let receipt = TypedReceipt::Legacy(ReceiptWithBloom { + receipt: Receipt { + success: false, + cumulative_gas_used: 0x1u64, + logs: vec![Log { + address: Address::from_str("0000000000000000000000000000000000000011").unwrap(), + data: LogData::new_unchecked( + vec![ + B256::from_str( + "000000000000000000000000000000000000000000000000000000000000dead", + ) + .unwrap(), + B256::from_str( + "000000000000000000000000000000000000000000000000000000000000beef", + ) + .unwrap(), + ], + Bytes::from_str("0100ff").unwrap(), + ), + }], + }, + bloom: [0; 256].into(), + }); + + receipt.encode(&mut data); + + // check that the rlp length equals the length of the expected rlp + assert_eq!(receipt.length(), expected.len()); + assert_eq!(data, expected); + } + + #[test] + fn decode_legacy_receipt() { + let data = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap(); + + let expected = TypedReceipt::Legacy(ReceiptWithBloom { + receipt: Receipt { + success: false, + cumulative_gas_used: 0x1u64, + logs: vec![Log { + address: Address::from_str("0000000000000000000000000000000000000011").unwrap(), + data: LogData::new_unchecked( + vec![ + B256::from_str( + "000000000000000000000000000000000000000000000000000000000000dead", + ) + .unwrap(), + B256::from_str( + "000000000000000000000000000000000000000000000000000000000000beef", + ) + .unwrap(), + ], + Bytes::from_str("0100ff").unwrap(), + ), + }], + }, + bloom: [0; 256].into(), + }); + + let receipt = TypedReceipt::decode(&mut &data[..]).unwrap(); + + assert_eq!(receipt, expected); + } +} diff --git a/crates/anvil/core/src/eth/transaction/mod.rs b/crates/anvil/core/src/eth/transaction/mod.rs index 5fa6d1fc0bfd..4a878ddfbbf4 100644 --- a/crates/anvil/core/src/eth/transaction/mod.rs +++ b/crates/anvil/core/src/eth/transaction/mod.rs @@ -22,9 +22,10 @@ use revm::{ }; use std::ops::Deref; +pub mod alloy; /// compatibility with `ethers-rs` types mod ethers_compat; - +pub mod optimism; pub use ethers_compat::{ call_to_internal_tx_request, from_ethers_access_list, to_alloy_proof, to_alloy_signature, to_ethers_access_list, to_ethers_signature, @@ -512,7 +513,7 @@ impl Encodable for DepositTransactionRequest { /// /// This is a helper that carries the `impersonated` sender so that the right hash /// [TypedTransaction::impersonated_hash] can be created. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MaybeImpersonatedTransaction { #[cfg_attr(feature = "serde", serde(flatten))] @@ -617,7 +618,7 @@ impl Deref for MaybeImpersonatedTransaction { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TypedTransaction { /// Legacy transaction type diff --git a/crates/anvil/core/src/eth/transaction/optimism.rs b/crates/anvil/core/src/eth/transaction/optimism.rs new file mode 100644 index 000000000000..f6b3b47e13be --- /dev/null +++ b/crates/anvil/core/src/eth/transaction/optimism.rs @@ -0,0 +1,179 @@ +use alloy_network::TxKind; +use alloy_primitives::{Address, Bytes, B256, U256}; +use alloy_rlp::{Decodable, Encodable, Error as DecodeError, Header as RlpHeader}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DepositTransactionRequest { + pub from: Address, + pub source_hash: B256, + pub kind: TxKind, + pub mint: U256, + pub value: U256, + pub gas_limit: U256, + pub is_system_tx: bool, + pub input: Bytes, +} + +impl DepositTransactionRequest { + pub fn hash(&self) -> B256 { + B256::from_slice(alloy_primitives::keccak256(alloy_rlp::encode(self)).as_slice()) + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) { + self.from.encode(out); + self.source_hash.encode(out); + self.kind.encode(out); + self.mint.encode(out); + self.value.encode(out); + self.gas_limit.encode(out); + self.is_system_tx.encode(out); + self.input.encode(out); + } + + /// Calculates the length of the RLP-encoded transaction's fields. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.source_hash.length(); + len += self.from.length(); + len += self.kind.length(); + len += self.mint.length(); + len += self.value.length(); + len += self.gas_limit.length(); + len += self.is_system_tx.length(); + len += self.input.length(); + len + } +} + +impl From for DepositTransactionRequest { + fn from(tx: DepositTransaction) -> Self { + Self { + from: tx.from, + source_hash: tx.source_hash, + kind: tx.kind, + mint: tx.mint, + value: tx.value, + gas_limit: tx.gas_limit, + is_system_tx: tx.is_system_tx, + input: tx.input, + } + } +} + +impl Encodable for DepositTransactionRequest { + fn encode(&self, out: &mut dyn bytes::BufMut) { + RlpHeader { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } +} + +/// An op-stack deposit transaction. +/// See +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct DepositTransaction { + pub nonce: U256, + pub source_hash: B256, + pub from: Address, + pub kind: TxKind, + pub mint: U256, + pub value: U256, + pub gas_limit: U256, + pub is_system_tx: bool, + pub input: Bytes, +} + +impl DepositTransaction { + pub fn nonce(&self) -> &U256 { + &self.nonce + } + + pub fn hash(&self) -> B256 { + B256::from_slice(alloy_primitives::keccak256(alloy_rlp::encode(self)).as_slice()) + } + + // /// Recovers the Ethereum address which was used to sign the transaction. + pub fn recover(&self) -> Result { + Ok(self.from) + } + + pub fn chain_id(&self) -> Option { + None + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) { + self.nonce.encode(out); + self.source_hash.encode(out); + self.from.encode(out); + self.kind.encode(out); + self.mint.encode(out); + self.value.encode(out); + self.gas_limit.encode(out); + self.is_system_tx.encode(out); + self.input.encode(out); + } + + /// Calculates the length of the RLP-encoded transaction's fields. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.nonce.length(); + len += self.source_hash.length(); + len += self.from.length(); + len += self.kind.length(); + len += self.mint.length(); + len += self.value.length(); + len += self.gas_limit.length(); + len += self.is_system_tx.length(); + len += self.input.length(); + len + } + + /// Decodes the inner [TxDeposit] fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `nonce` + /// - `source_hash` + /// - `from` + /// - `kind` + /// - `mint` + /// - `value` + /// - `gas_limit` + /// - `is_system_tx` + /// - `input` + pub fn decode_inner(buf: &mut &[u8]) -> Result { + Ok(Self { + nonce: Decodable::decode(buf)?, + source_hash: Decodable::decode(buf)?, + from: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + mint: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + is_system_tx: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + }) + } +} + +impl Encodable for DepositTransaction { + fn encode(&self, out: &mut dyn bytes::BufMut) { + RlpHeader { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } +} + +impl Decodable for DepositTransaction { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let header = RlpHeader::decode(buf)?; + let remaining_len = buf.len(); + + if header.payload_length > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } + + Self::decode_inner(buf) + } +} diff --git a/crates/anvil/core/src/eth/utils.rs b/crates/anvil/core/src/eth/utils.rs index e67e5dd4a905..d9d8c09872e5 100644 --- a/crates/anvil/core/src/eth/utils.rs +++ b/crates/anvil/core/src/eth/utils.rs @@ -1,3 +1,4 @@ +use alloy_eips::eip2930::AccessListItem as AlloyEipAccessListItem; use alloy_primitives::{Address, U256}; use alloy_rpc_types::AccessListItem as AlloyAccessListItem; use ethers_core::{ @@ -33,3 +34,10 @@ pub fn alloy_to_revm_access_list(list: Vec) -> Vec<(Address .map(|item| (item.address, item.storage_keys.into_iter().map(|k| k.into()).collect())) .collect() } + +/// Translates a vec of [AlloyEipAccessListItem] to a revm style Access List. +pub fn eip_to_revm_access_list(list: Vec) -> Vec<(Address, Vec)> { + list.into_iter() + .map(|item| (item.address, item.storage_keys.into_iter().map(|k| k.into()).collect())) + .collect() +}