diff --git a/Cargo.lock b/Cargo.lock index 12355e728b75c..797bca799a0b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,6 +306,7 @@ dependencies = [ "ctrlc", "ethereum-forkid", "ethers", + "ethers-core", "ethers-solc", "fdlimit", "flate2", @@ -342,7 +343,10 @@ dependencies = [ "alloy-primitives", "anvil-core", "bytes", + "ethers-contract", "ethers-core", + "ethers-middleware", + "ethers-providers", "foundry-common", "foundry-evm", "hash-db", @@ -2071,7 +2075,7 @@ dependencies = [ [[package]] name = "ethers" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -2086,7 +2090,7 @@ dependencies = [ [[package]] name = "ethers-addressbook" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "ethers-core", "once_cell", @@ -2097,7 +2101,7 @@ dependencies = [ [[package]] name = "ethers-contract" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "const-hex", "ethers-contract-abigen", @@ -2115,7 +2119,7 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "Inflector", "const-hex", @@ -2138,7 +2142,7 @@ dependencies = [ [[package]] name = "ethers-contract-derive" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "Inflector", "const-hex", @@ -2153,7 +2157,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "arrayvec", "bytes", @@ -2182,7 +2186,7 @@ dependencies = [ [[package]] name = "ethers-etherscan" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "chrono", "ethers-core", @@ -2197,7 +2201,7 @@ dependencies = [ [[package]] name = "ethers-middleware" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "async-trait", "auto_impl", @@ -2222,7 +2226,7 @@ dependencies = [ [[package]] name = "ethers-providers" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "async-trait", "auto_impl", @@ -2260,7 +2264,7 @@ dependencies = [ [[package]] name = "ethers-signers" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "async-trait", "coins-bip32", @@ -2288,7 +2292,7 @@ dependencies = [ [[package]] name = "ethers-solc" version = "2.0.11" -source = "git+https://github.com/gakonst/ethers-rs?rev=4e09be88730045dd6c4ed8a4e198a9956931e7bd#4e09be88730045dd6c4ed8a4e198a9956931e7bd" +source = "git+https://github.com/gakonst/ethers-rs?rev=546ea029362a7502365667c6f99fac92ad0aeb9f#546ea029362a7502365667c6f99fac92ad0aeb9f" dependencies = [ "cfg-if", "const-hex", @@ -2617,8 +2621,7 @@ dependencies = [ [[package]] name = "foundry-block-explorers" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc59cf4c18884c485b20f376e98946774b76f3b8e2e71e4f35723ffb34b8544" +source = "git+https://github.com/foundry-rs/block-explorers#714c1537cf09d9a96293aadc633166c97d19da95" dependencies = [ "alloy-chains", "alloy-json-abi", diff --git a/Cargo.toml b/Cargo.toml index 68d4ccc90eefe..1aee8f58d2ab5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,16 +189,16 @@ tower-http = "0.4" #ethers-solc = { path = "../ethers-rs/ethers-solc" } [patch.crates-io] -ethers = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } -ethers-addressbook = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } -ethers-core = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } -ethers-contract = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } -ethers-contract-abigen = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } -ethers-providers = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } -ethers-signers = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } -ethers-middleware = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } -ethers-etherscan = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } -ethers-solc = { git = "https://github.com/gakonst/ethers-rs", rev = "4e09be88730045dd6c4ed8a4e198a9956931e7bd" } +ethers = { git = "https://github.com/gakonst/ethers-rs", rev = "546ea029362a7502365667c6f99fac92ad0aeb9f" } +ethers-core = { git = "https://github.com/gakonst/ethers-rs", rev = "546ea029362a7502365667c6f99fac92ad0aeb9f" } +ethers-contract = { git = "https://github.com/gakonst/ethers-rs", rev = "546ea029362a7502365667c6f99fac92ad0aeb9f" } +ethers-contract-abigen = { git = "https://github.com/gakonst/ethers-rs", rev = "546ea029362a7502365667c6f99fac92ad0aeb9f" } +ethers-providers = { git = "https://github.com/gakonst/ethers-rs", rev = "546ea029362a7502365667c6f99fac92ad0aeb9f" } +ethers-signers = { git = "https://github.com/gakonst/ethers-rs", rev = "546ea029362a7502365667c6f99fac92ad0aeb9f" } +ethers-middleware = { git = "https://github.com/gakonst/ethers-rs", rev = "546ea029362a7502365667c6f99fac92ad0aeb9f" } +ethers-solc = { git = "https://github.com/gakonst/ethers-rs", rev = "546ea029362a7502365667c6f99fac92ad0aeb9f" } + +foundry-block-explorers = { git = "https://github.com/foundry-rs/block-explorers" } alloy-dyn-abi = { git = "https://github.com/alloy-rs/core/" } alloy-json-abi = { git = "https://github.com/alloy-rs/core/" } diff --git a/crates/anvil/Cargo.toml b/crates/anvil/Cargo.toml index c6430cffdb8c2..dbced4c69d71f 100644 --- a/crates/anvil/Cargo.toml +++ b/crates/anvil/Cargo.toml @@ -31,7 +31,7 @@ foundry-evm.workspace = true bytes = "1.4.0" # needed as documented in https://github.com/foundry-rs/foundry/pull/6358 k256 = "=0.13.1" -ethers = { workspace = true, features = ["rustls", "ws", "ipc"] } +ethers = { workspace = true, features = ["rustls", "ws", "ipc", "optimism"] } trie-db = "0.23" hash-db = "0.15" memory-db = "0.29" @@ -75,6 +75,7 @@ ethereum-forkid = "0.12" [dev-dependencies] ethers = { workspace = true, features = ["abigen"] } +ethers-core = { workspace = true, features = ["optimism"] } ethers-solc = { workspace = true, features = ["project-util", "full"] } pretty_assertions = "1.3.0" tokio = { version = "1", features = ["full"] } diff --git a/crates/anvil/core/Cargo.toml b/crates/anvil/core/Cargo.toml index 034a40737108a..91d86986da824 100644 --- a/crates/anvil/core/Cargo.toml +++ b/crates/anvil/core/Cargo.toml @@ -13,10 +13,14 @@ repository.workspace = true foundry-common.workspace = true foundry-evm.workspace = true +alloy-primitives = { workspace = true, features = ["serde"] } revm = { workspace = true, default-features = false, features = ["std", "serde", "memory_limit"] } +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"] } +ethers-providers = { workspace = true, features = ["optimism"] } +ethers-middleware = { workspace = true, features = ["optimism"] } -alloy-primitives = { workspace = true, features = ["serde"] } -ethers-core.workspace = true serde = { workspace = true, optional = true } serde_json.workspace = true bytes = { version = "1.4" } @@ -36,4 +40,4 @@ anvil-core = { path = ".", features = ["serde"] } default = ["serde"] impersonated-tx = [] fastrlp = ["dep:open-fastrlp"] -serde = ["dep:serde"] +serde = ["dep:serde"] \ No newline at end of file diff --git a/crates/anvil/core/src/eth/receipt.rs b/crates/anvil/core/src/eth/receipt.rs index 02b696582ef63..c651124878a73 100644 --- a/crates/anvil/core/src/eth/receipt.rs +++ b/crates/anvil/core/src/eth/receipt.rs @@ -94,6 +94,7 @@ impl Decodable for EIP658Receipt { // same underlying data structure pub type EIP2930Receipt = EIP658Receipt; pub type EIP1559Receipt = EIP658Receipt; +pub type DepositReceipt = EIP658Receipt; #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -104,6 +105,8 @@ pub enum TypedReceipt { EIP2930(EIP2930Receipt), /// EIP-1559 receipt EIP1559(EIP1559Receipt), + /// op-stack deposit receipt + Deposit(DepositReceipt), } // == impl TypedReceipt == @@ -112,18 +115,20 @@ impl TypedReceipt { /// Returns the gas used by the transactions pub fn gas_used(&self) -> U256 { match self { - TypedReceipt::Legacy(r) | TypedReceipt::EIP2930(r) | TypedReceipt::EIP1559(r) => { - r.gas_used - } + TypedReceipt::Legacy(r) | + TypedReceipt::EIP2930(r) | + TypedReceipt::EIP1559(r) | + TypedReceipt::Deposit(r) => r.gas_used, } } /// Returns the gas used by the transactions pub fn logs_bloom(&self) -> &Bloom { match self { - TypedReceipt::Legacy(r) | TypedReceipt::EIP2930(r) | TypedReceipt::EIP1559(r) => { - &r.logs_bloom - } + TypedReceipt::Legacy(r) | + TypedReceipt::EIP2930(r) | + TypedReceipt::EIP1559(r) | + TypedReceipt::Deposit(r) => &r.logs_bloom, } } } @@ -134,6 +139,7 @@ impl Encodable for TypedReceipt { TypedReceipt::Legacy(r) => r.rlp_append(s), TypedReceipt::EIP2930(r) => enveloped(1, r, s), TypedReceipt::EIP1559(r) => enveloped(2, r, s), + TypedReceipt::Deposit(r) => enveloped(0x7E, r, s), } } } @@ -158,6 +164,10 @@ impl Decodable for TypedReceipt { return rlp::decode(s).map(TypedReceipt::EIP1559) } + if first == 0x7E { + return rlp::decode(s).map(TypedReceipt::Deposit) + } + Err(DecoderError::Custom("unknown receipt type")) } } @@ -171,6 +181,7 @@ impl open_fastrlp::Encodable for TypedReceipt { 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"), }; @@ -188,6 +199,7 @@ impl open_fastrlp::Encodable for TypedReceipt { 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"), }; @@ -208,6 +220,14 @@ impl open_fastrlp::Encodable for TypedReceipt { out.put_u8(0x02); r.encode(out); } + TypedReceipt::Deposit(r) => { + let receipt_string_header = + Header { list: false, payload_length: payload_len }; + + receipt_string_header.encode(out); + out.put_u8(0x7E); + r.encode(out); + } _ => unreachable!("receipt already matched"), } } @@ -244,6 +264,10 @@ impl open_fastrlp::Decodable for TypedReceipt { buf.advance(1); ::decode(buf) .map(TypedReceipt::EIP1559) + } else if receipt_type == 0x7E { + buf.advance(1); + ::decode(buf) + .map(TypedReceipt::Deposit) } else { Err(open_fastrlp::DecodeError::Custom("invalid receipt type")) } @@ -264,6 +288,7 @@ impl From for EIP658Receipt { TypedReceipt::Legacy(receipt) => receipt, TypedReceipt::EIP2930(receipt) => receipt, TypedReceipt::EIP1559(receipt) => receipt, + TypedReceipt::Deposit(receipt) => receipt, } } } diff --git a/crates/anvil/core/src/eth/transaction/ethers_compat.rs b/crates/anvil/core/src/eth/transaction/ethers_compat.rs index 21e2cb8b91307..54e2c16c535a8 100644 --- a/crates/anvil/core/src/eth/transaction/ethers_compat.rs +++ b/crates/anvil/core/src/eth/transaction/ethers_compat.rs @@ -2,12 +2,15 @@ use super::EthTransactionRequest; use crate::eth::transaction::{ - EIP1559TransactionRequest, EIP2930TransactionRequest, LegacyTransactionRequest, - MaybeImpersonatedTransaction, TypedTransaction, TypedTransactionRequest, + DepositTransactionRequest, EIP1559TransactionRequest, EIP2930TransactionRequest, + LegacyTransactionRequest, MaybeImpersonatedTransaction, TypedTransaction, + TypedTransactionRequest, }; use ethers_core::types::{ - transaction::eip2718::TypedTransaction as EthersTypedTransactionRequest, Address, - Eip1559TransactionRequest as EthersEip1559TransactionRequest, + transaction::{ + eip2718::TypedTransaction as EthersTypedTransactionRequest, optimism::DepositTransaction, + }, + Address, Eip1559TransactionRequest as EthersEip1559TransactionRequest, Eip2930TransactionRequest as EthersEip2930TransactionRequest, NameOrAddress, Transaction as EthersTransaction, TransactionRequest as EthersLegacyTransactionRequest, TransactionRequest, H256, U256, U64, @@ -87,6 +90,33 @@ impl From for EthersTypedTransactionRequest { chain_id: Some(chain_id.into()), }) } + TypedTransactionRequest::Deposit(tx) => { + let DepositTransactionRequest { + source_hash, + from, + kind, + mint, + value, + gas_limit, + is_system_tx, + input, + } = tx; + EthersTypedTransactionRequest::DepositTransaction(DepositTransaction { + tx: TransactionRequest { + from: Some(from), + to: kind.as_call().cloned().map(Into::into), + gas: Some(gas_limit), + value: Some(value), + data: Some(input), + gas_price: Some(0.into()), + nonce: Some(0.into()), + chain_id: None, + }, + source_hash, + mint: Some(mint), + is_system_tx, + }) + } } } } @@ -117,6 +147,9 @@ fn to_ethers_transaction_with_hash_and_sender( s: t.signature.s, access_list: None, transaction_type: None, + source_hash: H256::zero(), + mint: None, + is_system_tx: false, other: Default::default(), }, TypedTransaction::EIP2930(t) => EthersTransaction { @@ -139,6 +172,9 @@ fn to_ethers_transaction_with_hash_and_sender( s: U256::from(t.s.as_bytes()), access_list: Some(t.access_list), transaction_type: Some(1u64.into()), + source_hash: H256::zero(), + mint: None, + is_system_tx: false, other: Default::default(), }, TypedTransaction::EIP1559(t) => EthersTransaction { @@ -161,6 +197,34 @@ fn to_ethers_transaction_with_hash_and_sender( s: U256::from(t.s.as_bytes()), access_list: Some(t.access_list), transaction_type: Some(2u64.into()), + source_hash: H256::zero(), + mint: None, + is_system_tx: false, + other: Default::default(), + }, + TypedTransaction::Deposit(t) => EthersTransaction { + hash, + nonce: t.nonce, + block_hash: None, + block_number: None, + transaction_index: None, + from, + to: None, + value: t.value, + gas_price: Some(0.into()), + max_fee_per_gas: Some(0.into()), + max_priority_fee_per_gas: Some(0.into()), + gas: t.gas_limit, + input: t.input.clone(), + chain_id: t.chain_id().map(Into::into), + v: 0.into(), + r: 0.into(), + s: 0.into(), + access_list: None, + transaction_type: Some(126u64.into()), + source_hash: t.source_hash, + mint: Some(t.mint), + is_system_tx: t.is_system_tx, other: Default::default(), }, } @@ -201,6 +265,7 @@ impl From for EthTransactionRequest { chain_id, access_list: None, transaction_type: None, + optimism_fields: None, } } } diff --git a/crates/anvil/core/src/eth/transaction/mod.rs b/crates/anvil/core/src/eth/transaction/mod.rs index a1747706e4beb..68cd9b48fa8dc 100644 --- a/crates/anvil/core/src/eth/transaction/mod.rs +++ b/crates/anvil/core/src/eth/transaction/mod.rs @@ -18,7 +18,7 @@ use foundry_common::types::ToAlloy; use foundry_evm::traces::CallTraceArena; use revm::{ interpreter::InstructionResult, - primitives::{CreateScheme, TransactTo, TxEnv}, + primitives::{CreateScheme, OptimismFields, TransactTo, TxEnv}, }; use std::ops::Deref; @@ -36,11 +36,13 @@ pub const IMPERSONATED_SIGNATURE: Signature = /// 1. Legacy (pre-EIP2718) [`LegacyTransactionRequest`] /// 2. EIP2930 (state access lists) [`EIP2930TransactionRequest`] /// 3. EIP1559 [`EIP1559TransactionRequest`] +/// 4. Deposit [`DepositTransactionRequest`] #[derive(Debug, Clone, Eq, PartialEq)] pub enum TypedTransactionRequest { Legacy(LegacyTransactionRequest), EIP2930(EIP2930TransactionRequest), EIP1559(EIP1559TransactionRequest), + Deposit(DepositTransactionRequest), } /// Represents _all_ transaction requests received from RPC @@ -80,6 +82,22 @@ pub struct EthTransactionRequest { /// EIP-2718 type #[cfg_attr(feature = "serde", serde(rename = "type"))] pub transaction_type: Option, + /// Optimism Deposit Request Fields + #[serde(flatten)] + pub optimism_fields: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Default, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct OptimismDepositRequestFields { + /// op-stack deposit source hash + pub source_hash: H256, + /// op-stack deposit mint + pub mint: U256, + /// op-stack deposit system tx + pub is_system_tx: bool, } // == impl EthTransactionRequest == @@ -88,6 +106,7 @@ impl EthTransactionRequest { /// Converts the request into a [TypedTransactionRequest] pub fn into_typed_request(self) -> Option { let EthTransactionRequest { + from, to, gas_price, max_fee_per_gas, @@ -99,10 +118,27 @@ impl EthTransactionRequest { mut access_list, chain_id, transaction_type, + optimism_fields, .. } = self; let chain_id = chain_id.map(|id| id.as_u64()); let transaction_type = transaction_type.map(|id| id.as_u64()); + // op-stack deposit tx + if optimism_fields.is_some() && transaction_type == Some(126) { + return Some(TypedTransactionRequest::Deposit(DepositTransactionRequest { + source_hash: optimism_fields.clone()?.source_hash, + from: from.unwrap_or_default(), + kind: match to { + Some(to) => TransactionKind::Call(to), + None => TransactionKind::Create, + }, + mint: optimism_fields.clone()?.mint, + value: value.unwrap_or_default(), + gas_limit: gas.unwrap_or_default(), + is_system_tx: optimism_fields.clone()?.is_system_tx, + input: data.clone().unwrap_or_default(), + })); + } match ( transaction_type, gas_price, @@ -414,6 +450,55 @@ impl Encodable for EIP1559TransactionRequest { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DepositTransactionRequest { + pub from: Address, + pub source_hash: H256, + pub kind: TransactionKind, + pub mint: U256, + pub value: U256, + pub gas_limit: U256, + pub is_system_tx: bool, + pub input: Bytes, +} + +// == impl DepositTransactionRequest == + +impl DepositTransactionRequest { + pub fn hash(&self) -> H256 { + H256::from_slice(keccak256(&rlp::encode(self)).as_slice()) + } +} + +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 rlp_append(&self, s: &mut RlpStream) { + s.begin_list(8); + s.append(&self.from); + s.append(&self.source_hash); + s.append(&self.kind); + s.append(&self.mint); + s.append(&self.value); + s.append(&self.gas_limit); + s.append(&self.is_system_tx); + s.append(&self.input.as_ref()); + } +} + /// A wrapper for `TypedTransaction` that allows impersonating accounts. /// /// This is a helper that carries the `impersonated` sender so that the right hash @@ -532,6 +617,8 @@ pub enum TypedTransaction { EIP2930(EIP2930Transaction), /// EIP-1559 transaction EIP1559(EIP1559Transaction), + /// op-stack deposit transaction + Deposit(DepositTransaction), } // == impl TypedTransaction == @@ -547,6 +634,7 @@ impl TypedTransaction { TypedTransaction::Legacy(tx) => tx.gas_price, TypedTransaction::EIP2930(tx) => tx.gas_price, TypedTransaction::EIP1559(tx) => tx.max_fee_per_gas, + TypedTransaction::Deposit(_) => U256::from(0), } } @@ -555,6 +643,7 @@ impl TypedTransaction { TypedTransaction::Legacy(tx) => tx.gas_limit, TypedTransaction::EIP2930(tx) => tx.gas_limit, TypedTransaction::EIP1559(tx) => tx.gas_limit, + TypedTransaction::Deposit(tx) => tx.gas_limit, } } @@ -563,6 +652,7 @@ impl TypedTransaction { TypedTransaction::Legacy(tx) => tx.value, TypedTransaction::EIP2930(tx) => tx.value, TypedTransaction::EIP1559(tx) => tx.value, + TypedTransaction::Deposit(tx) => tx.value, } } @@ -571,6 +661,7 @@ impl TypedTransaction { TypedTransaction::Legacy(tx) => &tx.input, TypedTransaction::EIP2930(tx) => &tx.input, TypedTransaction::EIP1559(tx) => &tx.input, + TypedTransaction::Deposit(tx) => &tx.input, } } @@ -580,6 +671,7 @@ impl TypedTransaction { TypedTransaction::Legacy(_) => None, TypedTransaction::EIP2930(_) => Some(1), TypedTransaction::EIP1559(_) => Some(2), + TypedTransaction::Deposit(_) => Some(0x7E), } } @@ -627,6 +719,18 @@ impl TypedTransaction { chain_id: Some(t.chain_id), 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(), + }, } } @@ -635,6 +739,7 @@ impl TypedTransaction { TypedTransaction::Legacy(t) => t.nonce(), TypedTransaction::EIP2930(t) => t.nonce(), TypedTransaction::EIP1559(t) => t.nonce(), + TypedTransaction::Deposit(t) => t.nonce(), } } @@ -643,6 +748,7 @@ impl TypedTransaction { 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(), } } @@ -672,6 +778,7 @@ impl TypedTransaction { TypedTransaction::Legacy(t) => t.hash(), TypedTransaction::EIP2930(t) => t.hash(), TypedTransaction::EIP1559(t) => t.hash(), + TypedTransaction::Deposit(t) => t.hash(), } } @@ -697,6 +804,7 @@ impl TypedTransaction { TypedTransaction::Legacy(tx) => tx.recover(), TypedTransaction::EIP2930(tx) => tx.recover(), TypedTransaction::EIP1559(tx) => tx.recover(), + TypedTransaction::Deposit(tx) => tx.recover(), } } @@ -706,6 +814,7 @@ impl TypedTransaction { TypedTransaction::Legacy(tx) => &tx.kind, TypedTransaction::EIP2930(tx) => &tx.kind, TypedTransaction::EIP1559(tx) => &tx.kind, + TypedTransaction::Deposit(tx) => &tx.kind, } } @@ -730,6 +839,7 @@ impl TypedTransaction { let s = U256::from_big_endian(&tx.s[..]); Signature { r, s, v: v.into() } } + TypedTransaction::Deposit(_) => Signature { r: U256::zero(), s: U256::zero(), v: 0 }, } } } @@ -740,6 +850,7 @@ impl Encodable for TypedTransaction { TypedTransaction::Legacy(tx) => tx.rlp_append(s), TypedTransaction::EIP2930(tx) => enveloped(1, tx, s), TypedTransaction::EIP1559(tx) => enveloped(2, tx, s), + TypedTransaction::Deposit(tx) => enveloped(0x7E, tx, s), } } } @@ -754,9 +865,11 @@ impl Decodable for TypedTransaction { }; // "advance" the header, see comments in fastrlp impl below let s = if s.is_empty() { &rlp.as_raw()[1..] } else { s }; + match *first { 0x01 => rlp::decode(s).map(TypedTransaction::EIP2930), 0x02 => rlp::decode(s).map(TypedTransaction::EIP1559), + 0x7E => rlp::decode(s).map(TypedTransaction::Deposit), _ => Err(DecoderError::Custom("invalid tx type")), } } @@ -771,6 +884,7 @@ impl open_fastrlp::Encodable for TypedTransaction { let payload_len = match tx { TypedTransaction::EIP2930(tx) => tx.length() + 1, TypedTransaction::EIP1559(tx) => tx.length() + 1, + TypedTransaction::Deposit(tx) => tx.length() + 1, _ => unreachable!("legacy tx length already matched"), }; @@ -791,6 +905,14 @@ impl open_fastrlp::Encodable for TypedTransaction { out.put_u8(0x02); tx.encode(out); } + TypedTransaction::Deposit(tx) => { + let tx_string_header = + open_fastrlp::Header { list: false, payload_length: payload_len }; + + tx_string_header.encode(out); + out.put_u8(0x7E); + tx.encode(out); + } _ => unreachable!("legacy tx encode already matched"), } } @@ -803,6 +925,7 @@ impl open_fastrlp::Encodable for TypedTransaction { let payload_len = match tx { TypedTransaction::EIP2930(tx) => tx.length() + 1, TypedTransaction::EIP1559(tx) => tx.length() + 1, + TypedTransaction::Deposit(tx) => tx.length() + 1, _ => unreachable!("legacy tx length already matched"), }; // we include a string header for signed types txs, so include the length here @@ -848,6 +971,10 @@ impl open_fastrlp::Decodable for TypedTransaction { buf.advance(1); ::decode(buf) .map(TypedTransaction::EIP1559) + } else if tx_type == 0x7E { + buf.advance(1); + ::decode(buf) + .map(TypedTransaction::Deposit) } else { Err(open_fastrlp::DecodeError::Custom("invalid tx type")) } @@ -1123,6 +1250,76 @@ impl Decodable for EIP1559Transaction { } } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "fastrlp", derive(open_fastrlp::RlpEncodable, open_fastrlp::RlpDecodable))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + +pub struct DepositTransaction { + pub nonce: U256, + pub source_hash: H256, + pub from: Address, + pub kind: TransactionKind, + 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) -> H256 { + H256::from_slice(keccak256(&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 + } +} + +impl Encodable for DepositTransaction { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(9); + s.append(&self.nonce); + s.append(&self.source_hash); + s.append(&self.from); + s.append(&self.kind); + s.append(&self.mint); + s.append(&self.value); + s.append(&self.gas_limit); + s.append(&self.is_system_tx); + s.append(&self.input.as_ref()); + } +} + +impl Decodable for DepositTransaction { + fn decode(rlp: &Rlp) -> Result { + if rlp.item_count()? != 8 { + return Err(DecoderError::RlpIncorrectListLen) + } + + Ok(Self { + source_hash: rlp.val_at(0)?, + from: rlp.val_at(1)?, + kind: rlp.val_at(2)?, + mint: rlp.val_at(3)?, + value: rlp.val_at(4)?, + gas_limit: rlp.val_at(5)?, + is_system_tx: rlp.val_at(6)?, + input: rlp.val_at::>(7)?.into(), + nonce: U256::from(0), + }) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct TransactionEssentials { pub kind: TransactionKind, @@ -1264,6 +1461,39 @@ impl PendingTransaction { ..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: caller.to_alloy(), + transact_to: transact_to(kind), + data: alloy_primitives::Bytes(input.0.clone()), + chain_id, + nonce: Some(nonce.as_u64()), + value: (*value).to_alloy(), + gas_price: 0.to_alloy(), + gas_priority_fee: None, + gas_limit: gas_limit.as_u64(), + access_list: vec![], + optimism: OptimismFields { + source_hash: Some(source_hash.to_alloy()), + mint: Some(mint.as_u128()), + is_system_transaction: Some(*is_system_tx), + enveloped_tx: None, + }, + ..Default::default() + } + } } } } @@ -1281,6 +1511,7 @@ pub struct TransactionInfo { pub traces: CallTraceArena, pub exit: InstructionResult, pub out: Option, + pub nonce: u64, } // === impl TransactionInfo === @@ -1585,6 +1816,28 @@ mod tests { expected, ::decode(bytes_fifth).unwrap() ); + + let bytes_sixth = &mut &hex::decode("b8587ef85507a0000000000000000000000000000000000000000000000000000000000000000094cf7f9e66af820a19257a2108375b180b0ec491679461815774383099e24810ab832a5b2a5425c154d5808230398287fb0180").unwrap()[..]; + let expected: TypedTransaction = TypedTransaction::Deposit(DepositTransaction { + nonce: 7u64.into(), + source_hash: H256::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + from: "cf7f9e66af820a19257a2108375b180b0ec49167".parse().unwrap(), + kind: TransactionKind::Call(Address::from_slice( + &hex::decode("61815774383099e24810ab832a5b2a5425c154d5").unwrap()[..], + )), + mint: U256::zero(), + value: 12345u64.into(), + gas_limit: 34811u64.into(), + input: Bytes::default(), + is_system_tx: true, + }); + assert_eq!( + expected, + ::decode(bytes_sixth).unwrap() + ); } // diff --git a/crates/anvil/src/cmd.rs b/crates/anvil/src/cmd.rs index b6dbfbc8c2653..5f184cb80dd74 100644 --- a/crates/anvil/src/cmd.rs +++ b/crates/anvil/src/cmd.rs @@ -230,6 +230,7 @@ impl NodeArgs { .set_pruned_history(self.prune_history) .with_init_state(self.load_state.or_else(|| self.state.and_then(|s| s.state))) .with_transaction_block_keeper(self.transaction_block_keeper) + .with_optimism(self.evm_opts.optimism) } fn account_generator(&self) -> AccountGenerator { @@ -500,6 +501,10 @@ pub struct AnvilEvmArgs { /// Enable autoImpersonate on startup #[clap(long, visible_alias = "auto-impersonate")] pub auto_impersonate: bool, + + /// Run an Optimism chain + #[clap(long, visible_alias = "optimism")] + pub optimism: bool, } /// Resolves an alias passed as fork-url to the matching url defined in the rpc_endpoints section diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index cad1d7f628479..2b6f9fad7cbad 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -169,6 +169,8 @@ pub struct NodeConfig { pub transaction_block_keeper: Option, /// Disable the default CREATE2 deployer pub disable_default_create2_deployer: bool, + /// Enable Optimism deposit transaction + pub enable_optimism: bool, } impl NodeConfig { @@ -406,6 +408,7 @@ impl Default for NodeConfig { init_state: None, transaction_block_keeper: None, disable_default_create2_deployer: false, + enable_optimism: false, } } } @@ -784,6 +787,13 @@ impl NodeConfig { Config::foundry_block_cache_file(chain_id, block) } + /// Sets whether to enable optimism support + #[must_use] + pub fn with_optimism(mut self, enable_optimism: bool) -> Self { + self.enable_optimism = enable_optimism; + self + } + /// Configures everything related to env, backend and database and returns the /// [Backend](mem::Backend) /// @@ -800,6 +810,7 @@ impl NodeConfig { // caller is a contract. So we disable the check by default. cfg.disable_eip3607 = true; cfg.disable_block_gas_limit = self.disable_block_gas_limit; + cfg.optimism = self.enable_optimism; let mut env = revm::primitives::Env { cfg, diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index ff9a0cbab7e1e..232045e7552d9 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -56,8 +56,8 @@ use ethers::{ eip712::TypedData, }, Address, Block, BlockId, BlockNumber, Bytes, FeeHistory, Filter, FilteredParams, - GethDebugTracingOptions, GethTrace, Log, Trace, Transaction, TransactionReceipt, TxHash, - TxpoolContent, TxpoolInspectSummary, TxpoolStatus, H256, U256, U64, + GethDebugTracingOptions, GethTrace, Log, Signature, Trace, Transaction, TransactionReceipt, + TxHash, TxpoolContent, TxpoolInspectSummary, TxpoolStatus, H256, U256, U64, }, utils::rlp, }; @@ -408,10 +408,19 @@ impl EthApi { from: &Address, request: TypedTransactionRequest, ) -> Result { - for signer in self.signers.iter() { - if signer.accounts().contains(from) { - let signature = signer.sign_transaction(request.clone(), from)?; - return build_typed_transaction(request, signature) + match request { + TypedTransactionRequest::Deposit(_) => { + const NIL_SIGNATURE: ethers::types::Signature = + Signature { r: U256::zero(), s: U256::zero(), v: 0 }; + return build_typed_transaction(request, NIL_SIGNATURE) + } + _ => { + for signer in self.signers.iter() { + if signer.accounts().contains(from) { + let signature = signer.sign_transaction(request.clone(), from)?; + return build_typed_transaction(request, signature) + } + } } } Err(BlockchainError::NoSignerAvailable) @@ -835,7 +844,6 @@ impl EthApi { let (nonce, on_chain_nonce) = self.request_nonce(&request, from).await?; let request = self.build_typed_tx_request(request, nonce)?; - // if the sender is currently impersonated we need to "bypass" signing let pending_transaction = if self.is_impersonated(from) { let bypass_signature = self.backend.cheats().bypass_signature(); @@ -2427,6 +2435,10 @@ impl EthApi { } TypedTransactionRequest::EIP1559(m) } + Some(TypedTransactionRequest::Deposit(mut m)) => { + m.gas_limit = gas_limit; + TypedTransactionRequest::Deposit(m) + } _ => return Err(BlockchainError::FailedToDecodeTransaction), }; Ok(request) @@ -2503,6 +2515,7 @@ impl EthApi { match &tx { TypedTransaction::EIP2930(_) => self.backend.ensure_eip2930_active(), TypedTransaction::EIP1559(_) => self.backend.ensure_eip1559_active(), + TypedTransaction::Deposit(_) => self.backend.ensure_op_deposits_active(), TypedTransaction::Legacy(_) => Ok(()), } } @@ -2591,6 +2604,10 @@ fn determine_base_gas_by_kind(request: EthTransactionRequest) -> U256 { TransactionKind::Call(_) => MIN_TRANSACTION_GAS, TransactionKind::Create => MIN_CREATE_GAS, }, + TypedTransactionRequest::Deposit(req) => match req.kind { + TransactionKind::Call(_) => MIN_TRANSACTION_GAS, + TransactionKind::Create => MIN_CREATE_GAS, + }, }, // Tighten the gas limit upwards if we don't know the transaction type to avoid deployments // failing. diff --git a/crates/anvil/src/eth/backend/executor.rs b/crates/anvil/src/eth/backend/executor.rs index 1f3f8f45f7606..df71bc1fd1518 100644 --- a/crates/anvil/src/eth/backend/executor.rs +++ b/crates/anvil/src/eth/backend/executor.rs @@ -8,7 +8,7 @@ use crate::{ }; use anvil_core::eth::{ block::{Block, BlockInfo, Header, PartialHeader}, - receipt::{EIP1559Receipt, EIP2930Receipt, EIP658Receipt, Log, TypedReceipt}, + receipt::{DepositReceipt, EIP1559Receipt, EIP2930Receipt, EIP658Receipt, Log, TypedReceipt}, transaction::{PendingTransaction, TransactionInfo, TypedTransaction}, trie, }; @@ -38,6 +38,7 @@ pub struct ExecutedTransaction { gas_used: u64, logs: Vec, traces: Vec, + nonce: u64, } // == impl ExecutedTransaction == @@ -71,6 +72,12 @@ impl ExecutedTransaction { logs_bloom: bloom, logs, }), + TypedTransaction::Deposit(_) => TypedReceipt::Deposit(DepositReceipt { + status_code, + gas_used: used_gas, + logs_bloom: bloom, + logs, + }), } } } @@ -171,6 +178,7 @@ impl<'a, DB: Db + ?Sized, Validator: TransactionValidator> TransactionExecutor<' Some(Output::Create(b, _)) => Some(ethers::types::Bytes(b.0)), _ => None, }, + nonce: tx.nonce, }; transaction_infos.push(info); @@ -249,6 +257,8 @@ impl<'a, 'b, DB: Db + ?Sized, Validator: TransactionValidator> Iterator return Some(TransactionExecutionOutcome::Invalid(transaction, err)) } + let nonce = account.nonce; + let mut evm = revm::EVM::new(); evm.env = env; evm.database(&mut self.db); @@ -312,6 +322,7 @@ impl<'a, 'b, DB: Db + ?Sized, Validator: TransactionValidator> Iterator gas_used, logs: logs.unwrap_or_default().into_iter().map(Into::into).collect(), traces: inspector.tracer.unwrap_or_default().traces.arena, + nonce, }; Some(TransactionExecutionOutcome::Executed(tx)) diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 72d105a0ac767..b82591d1532c4 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -553,6 +553,11 @@ impl Backend { (self.spec_id() as u8) >= (SpecId::BERLIN as u8) } + /// Returns true if op-stack deposits are active + pub fn is_optimism(&self) -> bool { + self.env.read().cfg.optimism + } + /// Returns an error if EIP1559 is not active (pre Berlin) pub fn ensure_eip1559_active(&self) -> Result<(), BlockchainError> { if self.is_eip1559() { @@ -569,6 +574,14 @@ impl Backend { Err(BlockchainError::EIP2930TransactionUnsupportedAtHardfork) } + /// Returns an error if op-stack deposits are not active + pub fn ensure_op_deposits_active(&self) -> Result<(), BlockchainError> { + if self.is_optimism() { + return Ok(()) + } + Err(BlockchainError::DepositTransactionUnsupported) + } + /// Returns the block gas limit pub fn gas_limit(&self) -> U256 { self.env.read().block.gas_limit.to_ethers() @@ -1913,8 +1926,11 @@ impl Backend { .unwrap_or(self.base_fee()) .checked_add(t.max_priority_fee_per_gas) .unwrap_or_else(U256::max_value), + TypedTransaction::Deposit(_) => U256::from(0), }; + let deposit_nonce = transaction_type.and_then(|x| (x == 0x7E).then_some(info.nonce)); + let inner = TransactionReceipt { transaction_hash: info.transaction_hash, transaction_index: info.transaction_index.into(), @@ -1956,6 +1972,11 @@ impl Backend { logs_bloom, transaction_type: transaction_type.map(Into::into), effective_gas_price: Some(effective_gas_price), + deposit_nonce, + l1_fee: None, + l1_fee_scalar: None, + l1_gas_price: None, + l1_gas_used: None, other: OtherFields::default(), }; @@ -2234,15 +2255,17 @@ impl TransactionValidator for Backend { } // check nonce + let is_deposit_tx = + matches!(&pending.transaction.transaction, TypedTransaction::Deposit(_)); let nonce: u64 = (*tx.nonce()).try_into().map_err(|_| InvalidTransactionError::NonceMaxValue)?; - if nonce < account.nonce { + if nonce < account.nonce && !is_deposit_tx { warn!(target: "backend", "[{:?}] nonce too low", tx.hash()); return Err(InvalidTransactionError::NonceTooLow) } if (env.cfg.spec_id as u8) >= (SpecId::LONDON as u8) { - if tx.gas_price() < env.block.basefee.to_ethers() { + if tx.gas_price() < env.block.basefee.to_ethers() && !is_deposit_tx { warn!(target: "backend", "max fee per gas={}, too low, block basefee={}",tx.gas_price(), env.block.basefee); return Err(InvalidTransactionError::FeeCapTooLow) } @@ -2297,6 +2320,9 @@ pub fn transaction_build( base_fee: Option, ) -> Transaction { let mut transaction: Transaction = eth_transaction.clone().into(); + if info.is_some() && transaction.transaction_type.unwrap_or(U64::zero()).as_u64() == 0x7E { + transaction.nonce = U256::from(info.as_ref().unwrap().nonce); + } if eth_transaction.is_dynamic_fee() { if block.is_none() && info.is_none() { diff --git a/crates/anvil/src/eth/error.rs b/crates/anvil/src/eth/error.rs index 971ef9956111c..db17c003b9b60 100644 --- a/crates/anvil/src/eth/error.rs +++ b/crates/anvil/src/eth/error.rs @@ -84,6 +84,8 @@ pub enum BlockchainError { EIP1559TransactionUnsupportedAtHardfork, #[error("Access list received but is not supported by the current hardfork.\n\nYou can use it by running anvil with '--hardfork berlin' or later.")] EIP2930TransactionUnsupportedAtHardfork, + #[error("op-stack deposit tx received but is not supported.\n\nYou can use it by running anvil with '--optimism'.")] + DepositTransactionUnsupported, #[error("Excess blob gas not set.")] ExcessBlobGasNotSet, } @@ -405,6 +407,9 @@ impl ToRpcResponseResult for Result { err @ BlockchainError::EIP2930TransactionUnsupportedAtHardfork => { RpcError::invalid_params(err.to_string()) } + err @ BlockchainError::DepositTransactionUnsupported => { + RpcError::invalid_params(err.to_string()) + } err @ BlockchainError::ExcessBlobGasNotSet => { RpcError::invalid_params(err.to_string()) } diff --git a/crates/anvil/src/eth/fees.rs b/crates/anvil/src/eth/fees.rs index 2206d302b8d91..67bc73740c4f0 100644 --- a/crates/anvil/src/eth/fees.rs +++ b/crates/anvil/src/eth/fees.rs @@ -256,6 +256,7 @@ impl FeeHistoryService { .max_priority_fee_per_gas .min(t.max_fee_per_gas.saturating_sub(base_fee)) .as_u64(), + Some(TypedTransaction::Deposit(_)) => 0, None => 0, }; diff --git a/crates/anvil/src/eth/sign.rs b/crates/anvil/src/eth/sign.rs index 46fbecb93725a..112f8c7ab46b6 100644 --- a/crates/anvil/src/eth/sign.rs +++ b/crates/anvil/src/eth/sign.rs @@ -1,7 +1,8 @@ use crate::eth::error::BlockchainError; use anvil_core::eth::transaction::{ - EIP1559Transaction, EIP1559TransactionRequest, EIP2930Transaction, EIP2930TransactionRequest, - LegacyTransaction, LegacyTransactionRequest, TypedTransaction, TypedTransactionRequest, + DepositTransaction, DepositTransactionRequest, EIP1559Transaction, EIP1559TransactionRequest, + EIP2930Transaction, EIP2930TransactionRequest, LegacyTransaction, LegacyTransactionRequest, + TypedTransaction, TypedTransactionRequest, }; use ethers::{ core::k256::ecdsa::SigningKey, @@ -11,7 +12,7 @@ use ethers::{ transaction::{ eip2718::TypedTransaction as EthersTypedTransactionRequest, eip712::TypedData, }, - Signature, H256, + Signature, H256, U256, }, }; use std::collections::HashMap; @@ -194,6 +195,30 @@ pub fn build_typed_transaction( }, }) } + TypedTransactionRequest::Deposit(tx) => { + let DepositTransactionRequest { + from, + gas_limit, + kind, + value, + input, + source_hash, + mint, + is_system_tx, + .. + } = tx; + TypedTransaction::Deposit(DepositTransaction { + from, + gas_limit, + kind, + value, + input, + source_hash, + mint, + is_system_tx, + nonce: U256::zero(), + }) + } }; Ok(tx) diff --git a/crates/anvil/src/eth/util.rs b/crates/anvil/src/eth/util.rs index 183b37e8eea08..41f1d23bf4df1 100644 --- a/crates/anvil/src/eth/util.rs +++ b/crates/anvil/src/eth/util.rs @@ -79,6 +79,8 @@ pub fn to_precompile_id(spec_id: SpecId) -> revm::precompile::SpecId { SpecId::MERGE | SpecId::SHANGHAI | SpecId::CANCUN | + SpecId::BEDROCK | + SpecId::REGOLITH | SpecId::LATEST => revm::precompile::SpecId::BERLIN, } } diff --git a/crates/anvil/tests/it/main.rs b/crates/anvil/tests/it/main.rs index cd99a9f15a011..057a9fdea596d 100644 --- a/crates/anvil/tests/it/main.rs +++ b/crates/anvil/tests/it/main.rs @@ -9,6 +9,7 @@ mod genesis; mod geth; mod ipc; mod logs; +mod optimism; mod proof; mod pubsub; // mod revert; // TODO uncomment diff --git a/crates/anvil/tests/it/optimism.rs b/crates/anvil/tests/it/optimism.rs new file mode 100644 index 0000000000000..5432a487fe268 --- /dev/null +++ b/crates/anvil/tests/it/optimism.rs @@ -0,0 +1,132 @@ +//! tests for OP chain support +use anvil::{spawn, NodeConfig}; +use ethers::{ + abi::Address, + providers::Middleware, + types::{ + transaction::{eip2718::TypedTransaction, optimism::DepositTransaction}, + TransactionRequest, U256, + }, +}; +use ethers_core::types::{Bytes, H256}; +use std::str::FromStr; + +#[tokio::test(flavor = "multi_thread")] +async fn test_deposits_not_supported_if_optimism_disabled() { + // optimism disabled by default + let (_, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let from_addr: Address = "cf7f9e66af820a19257a2108375b180b0ec49167".parse().unwrap(); + let to_addr: Address = "71562b71999873db5b286df957af199ec94617f7".parse().unwrap(); + let deposit_tx: TypedTransaction = TypedTransaction::DepositTransaction(DepositTransaction { + tx: TransactionRequest { + chain_id: None, + from: Some(from_addr), + to: Some(ethers::types::NameOrAddress::Address(to_addr)), + value: Some("1234".parse().unwrap()), + gas: Some(U256::from(21000)), + gas_price: None, + data: Some(Bytes::default()), + nonce: None, + }, + source_hash: H256::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + mint: Some(U256::zero()), + is_system_tx: true, + }); + + // sending the deposit transaction should fail with error saying not supported + let res = provider.send_transaction(deposit_tx.clone(), None).await; + assert!(res.is_err()); + assert!(res + .unwrap_err() + .to_string() + .contains("op-stack deposit tx received but is not supported")); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_send_value_deposit_transaction() { + // enable the Optimism flag + let (api, handle) = spawn(NodeConfig::test().with_optimism(true)).await; + let provider = handle.http_provider(); + + let send_value: U256 = "1234".parse().unwrap(); + let from_addr: Address = "cf7f9e66af820a19257a2108375b180b0ec49167".parse().unwrap(); + let to_addr: Address = "71562b71999873db5b286df957af199ec94617f7".parse().unwrap(); + + // fund the sender + api.anvil_set_balance(from_addr, send_value).await.unwrap(); + + let deposit_tx: TypedTransaction = TypedTransaction::DepositTransaction(DepositTransaction { + tx: TransactionRequest { + chain_id: None, + from: Some(from_addr), + to: Some(ethers::types::NameOrAddress::Address(to_addr)), + value: Some(send_value), + gas: Some(U256::from(21000)), + gas_price: None, + data: Some(Bytes::default()), + nonce: None, + }, + source_hash: H256::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + mint: Some(U256::zero()), + is_system_tx: true, + }); + provider.send_transaction(deposit_tx.clone(), None).await.unwrap().await.unwrap().unwrap(); + + // mine block + api.evm_mine(None).await.unwrap(); + + // the recipient should have received the value + let balance = provider.get_balance(to_addr, None).await.unwrap(); + assert_eq!(balance, send_value); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_send_value_raw_deposit_transaction() { + // enable the Optimism flag + let (api, handle) = spawn(NodeConfig::test().with_optimism(true)).await; + let provider = handle.http_provider(); + + let send_value: U256 = "1234".parse().unwrap(); + let from_addr: Address = "cf7f9e66af820a19257a2108375b180b0ec49167".parse().unwrap(); + let to_addr: Address = "71562b71999873db5b286df957af199ec94617f7".parse().unwrap(); + + // fund the sender + api.anvil_set_balance(from_addr, send_value).await.unwrap(); + + let deposit_tx: TypedTransaction = TypedTransaction::DepositTransaction(DepositTransaction { + tx: TransactionRequest { + chain_id: None, + from: Some(from_addr), + to: Some(ethers::types::NameOrAddress::Address(to_addr)), + value: Some(send_value), + gas: Some(U256::from(21000)), + gas_price: None, + data: Some(Bytes::default()), + nonce: None, + }, + source_hash: H256::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + mint: Some(U256::zero()), + is_system_tx: true, + }); + + let rlpbytes = deposit_tx.rlp(); + provider.send_raw_transaction(rlpbytes).await.unwrap().await.unwrap().unwrap(); + + // mine block + api.evm_mine(None).await.unwrap(); + + // the recipient should have received the value + let balance = provider.get_balance(to_addr, None).await.unwrap(); + assert_eq!(balance, send_value); +} diff --git a/crates/evm/core/Cargo.toml b/crates/evm/core/Cargo.toml index 65d2e4fe01a71..51d9ad6df13ca 100644 --- a/crates/evm/core/Cargo.toml +++ b/crates/evm/core/Cargo.toml @@ -29,6 +29,7 @@ revm = { workspace = true, default-features = false, features = [ "optional_block_gas_limit", "optional_no_base_fee", "arbitrary", + "optimism", ] } ethers-core.workspace = true diff --git a/crates/evm/core/src/fork/cache.rs b/crates/evm/core/src/fork/cache.rs index 4e4aa80e38ac7..ca467dbdf4b5e 100644 --- a/crates/evm/core/src/fork/cache.rs +++ b/crates/evm/core/src/fork/cache.rs @@ -189,6 +189,11 @@ impl<'de> Deserialize<'de> for BlockchainDbMeta { // keep default value obj.insert(key.to_string(), false.into()); } + let key = "optimism"; + if !obj.contains_key(key) { + // keep default value + obj.insert(key.to_string(), false.into()); + } } let cfg_env: revm::primitives::CfgEnv = diff --git a/crates/evm/core/src/utils.rs b/crates/evm/core/src/utils.rs index c4cf12517631f..8edaa4e21e0e4 100644 --- a/crates/evm/core/src/utils.rs +++ b/crates/evm/core/src/utils.rs @@ -132,6 +132,7 @@ pub fn halt_to_instruction_result(halt: Halt) -> InstructionResult { Halt::CallNotAllowedInsideStatic => InstructionResult::CallNotAllowedInsideStatic, Halt::OutOfFund => InstructionResult::OutOfFund, Halt::CallTooDeep => InstructionResult::CallTooDeep, + Halt::FailedDeposit => InstructionResult::Return, } } diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 636cfdce60e74..e17fb9f30f5fe 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -67,9 +67,6 @@ impl ScriptArgs { // Make a one-time gas price estimation let (gas_price, eip1559_fees) = { match deployment_sequence.transactions.front().unwrap().typed_tx() { - TypedTransaction::Legacy(_) | TypedTransaction::Eip2930(_) => { - (provider.get_gas_price().await.ok(), None) - } TypedTransaction::Eip1559(_) => { let fees = estimate_eip1559_fees(&provider, Some(chain)) .await @@ -77,6 +74,7 @@ impl ScriptArgs { (None, Some(fees)) } + _ => (provider.get_gas_price().await.ok(), None), } }; @@ -102,9 +100,6 @@ impl ScriptArgs { } else { // fill gas price match tx { - TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => { - tx.set_gas_price(gas_price.expect("Could not get gas_price.")); - } TypedTransaction::Eip1559(ref mut inner) => { let eip1559_fees = eip1559_fees.expect("Could not get eip1559 fee estimation."); @@ -116,6 +111,9 @@ impl ScriptArgs { } inner.max_fee_per_gas = Some(eip1559_fees.0); } + _ => { + tx.set_gas_price(gas_price.expect("Could not get gas_price.")); + } } }