diff --git a/Cargo.lock b/Cargo.lock index 00a5198dd..ca924f802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6826,6 +6826,7 @@ name = "reth-ethereum-payload-builder" version = "0.2.0-beta.7" dependencies = [ "reth-basic-payload-builder", + "reth-errors", "reth-evm", "reth-evm-ethereum", "reth-payload-builder", @@ -6865,7 +6866,9 @@ dependencies = [ name = "reth-evm-ethereum" version = "0.2.0-beta.7" dependencies = [ + "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7320d4c)", "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7320d4c)", + "alloy-sol-types", "reth-ethereum-consensus", "reth-evm", "reth-primitives", @@ -6873,6 +6876,7 @@ dependencies = [ "reth-testing-utils", "revm-primitives", "secp256k1 0.28.2", + "serde_json", ] [[package]] diff --git a/crates/ethereum/evm/Cargo.toml b/crates/ethereum/evm/Cargo.toml index 1b562254b..815881d45 100644 --- a/crates/ethereum/evm/Cargo.toml +++ b/crates/ethereum/evm/Cargo.toml @@ -20,8 +20,14 @@ reth-ethereum-consensus.workspace = true # Ethereum revm-primitives.workspace = true +# Alloy +alloy-consensus.workspace = true +alloy-eips.workspace = true +alloy-sol-types.workspace = true + [dev-dependencies] reth-testing-utils.workspace = true reth-revm = { workspace = true, features = ["test-utils"] } -alloy-eips.workspace = true secp256k1.workspace = true +serde_json.workspace = true + diff --git a/crates/ethereum/evm/src/eip6110.rs b/crates/ethereum/evm/src/eip6110.rs new file mode 100644 index 000000000..2251a1d0e --- /dev/null +++ b/crates/ethereum/evm/src/eip6110.rs @@ -0,0 +1,125 @@ +//! EIP-6110 deposit requests parsing +use alloy_consensus::Request; +use alloy_eips::eip6110::{DepositRequest, MAINNET_DEPOSIT_CONTRACT_ADDRESS}; +use alloy_sol_types::{sol, SolEvent}; +use reth_evm::execute::BlockValidationError; +use reth_primitives::{ChainSpec, Receipt}; +use revm_primitives::Log; + +sol! { + #[allow(missing_docs)] + event DepositEvent( + bytes pubkey, + bytes withdrawal_credentials, + bytes amount, + bytes signature, + bytes index + ); +} + +/// Parse [deposit contract](https://etherscan.io/address/0x00000000219ab540356cbb839cbe05303d7705fa) +/// (address is from the passed [ChainSpec]) deposits from receipts, and return them as a +/// [vector](Vec) of (requests)[Request]. +pub fn parse_deposits_from_receipts<'a, I>( + chain_spec: &ChainSpec, + receipts: I, +) -> Result, BlockValidationError> +where + I: IntoIterator, +{ + let deposit_contract_address = chain_spec + .deposit_contract + .as_ref() + .map_or(MAINNET_DEPOSIT_CONTRACT_ADDRESS, |contract| contract.address); + receipts + .into_iter() + .flat_map(|receipt| receipt.logs.iter()) + // No need to filter for topic because there's only one event and that's the Deposit event + // in the deposit contract. + .filter(|log| log.address == deposit_contract_address) + .map(|log| { + let decoded_log = DepositEvent::decode_log(log, false)?; + let deposit = parse_deposit_from_log(&decoded_log); + Ok(Request::DepositRequest(deposit)) + }) + .collect::, _>>() + .map_err(|err: alloy_sol_types::Error| { + BlockValidationError::DepositRequestDecode(err.to_string()) + }) +} + +fn parse_deposit_from_log(log: &Log) -> DepositRequest { + // SAFETY: These `expect` https://github.com/ethereum/consensus-specs/blob/5f48840f4d768bf0e0a8156a3ed06ec333589007/solidity_deposit_contract/deposit_contract.sol#L107-L110 + // are safe because the `DepositEvent` is the only event in the deposit contract and the length + // checks are done there. + DepositRequest { + pubkey: log + .pubkey + .as_ref() + .try_into() + .expect("pubkey length should be enforced in deposit contract"), + withdrawal_credentials: log + .withdrawal_credentials + .as_ref() + .try_into() + .expect("withdrawal_credentials length should be enforced in deposit contract"), + amount: u64::from_le_bytes( + log.amount + .as_ref() + .try_into() + .expect("amount length should be enforced in deposit contract"), + ), + signature: log + .signature + .as_ref() + .try_into() + .expect("signature length should be enforced in deposit contract"), + index: u64::from_le_bytes( + log.index + .as_ref() + .try_into() + .expect("deposit index length should be enforced in deposit contract"), + ), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use reth_primitives::{TxType, MAINNET}; + + #[test] + fn test_parse_deposit_from_log() { + let receipts = vec![ + // https://etherscan.io/tx/0xa5239d4c542063d29022545835815b78b09f571f2bf1c8427f4765d6f5abbce9 + #[allow(clippy::needless_update)] // side-effect of optimism fields + Receipt { + // these don't matter + tx_type: TxType::Legacy, + success: true, + cumulative_gas_used: 0, + logs: serde_json::from_str( + r#"[{"address":"0x00000000219ab540356cbb839cbe05303d7705fa","topics":["0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"],"data":"0x00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030998c8086669bf65e24581cda47d8537966e9f5066fc6ffdcba910a1bfb91eae7a4873fcce166a1c4ea217e6b1afd396200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002001000000000000000000000001c340fb72ed14d4eaa71f7633ee9e33b88d4f3900000000000000000000000000000000000000000000000000000000000000080040597307000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006098ddbffd700c1aac324cfdf0492ff289223661eb26718ce3651ba2469b22f480d56efab432ed91af05a006bde0c1ea68134e0acd8cacca0c13ad1f716db874b44abfcc966368019753174753bca3af2ea84bc569c46f76592a91e97f311eddec0000000000000000000000000000000000000000000000000000000000000008e474160000000000000000000000000000000000000000000000000000000000","blockHash":"0x8d1289c5a7e0965b1d1bb75cdc4c3f73dda82d4ebb94ff5b98d1389cebd53b56","blockNumber":"0x12f0d8d","transactionHash":"0xa5239d4c542063d29022545835815b78b09f571f2bf1c8427f4765d6f5abbce9","transactionIndex":"0xc4","logIndex":"0x18f","removed":false}]"# + ).unwrap(), + ..Default::default() + }, + // https://etherscan.io/tx/0xd9734d4e3953bcaa939fd1c1d80950ee54aeecc02eef6ae8179f47f5b7103338 + #[allow(clippy::needless_update)] // side-effect of optimism fields + Receipt { + // these don't matter + tx_type: TxType::Legacy, + success: true, + cumulative_gas_used: 0, + logs: serde_json::from_str( + r#"[{"address":"0x00000000219ab540356cbb839cbe05303d7705fa","topics":["0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"],"data":"0x00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030a1a2ba870a90e889aa594a0cc1c6feffb94c2d8f65646c937f1f456a315ef649533e25a4614d8f4f66ebdb06481b90af0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200100000000000000000000000a0f04a231efbc29e1db7d086300ff550211c2f6000000000000000000000000000000000000000000000000000000000000000800405973070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060ad416d590e1a7f52baff770a12835b68904efad22cc9f8ba531e50cbbd26f32b9c7373cf6538a0577f501e4d3e3e63e208767bcccaae94e1e3720bfb734a286f9c017d17af46536545ccb7ca94d71f295e71f6d25bf978c09ada6f8d3f7ba0390000000000000000000000000000000000000000000000000000000000000008e374160000000000000000000000000000000000000000000000000000000000","blockHash":"0x8d1289c5a7e0965b1d1bb75cdc4c3f73dda82d4ebb94ff5b98d1389cebd53b56","blockNumber":"0x12f0d8d","transactionHash":"0xd9734d4e3953bcaa939fd1c1d80950ee54aeecc02eef6ae8179f47f5b7103338","transactionIndex":"0x7c","logIndex":"0xe2","removed":false}]"#, + ).unwrap(), + ..Default::default() + }, + ]; + + let requests = parse_deposits_from_receipts(&MAINNET, &receipts).unwrap(); + assert_eq!(requests.len(), 2); + assert_eq!(requests[0].as_deposit_request().unwrap().amount, 32e9 as u64); + assert_eq!(requests[1].as_deposit_request().unwrap().amount, 32e9 as u64); + } +} diff --git a/crates/ethereum/evm/src/execute.rs b/crates/ethereum/evm/src/execute.rs index 1d876d39d..9d81358d8 100644 --- a/crates/ethereum/evm/src/execute.rs +++ b/crates/ethereum/evm/src/execute.rs @@ -203,8 +203,14 @@ where } let requests = if self.chain_spec.is_prague_active_at_timestamp(block.timestamp) { + // Collect all EIP-6110 deposits + let deposit_requests = + crate::eip6110::parse_deposits_from_receipts(&self.chain_spec, &receipts)?; + // Collect all EIP-7685 requests - apply_withdrawal_requests_contract_call(&mut evm)? + let withdrawal_requests = apply_withdrawal_requests_contract_call(&mut evm)?; + + [deposit_requests, withdrawal_requests].concat() } else { vec![] }; diff --git a/crates/ethereum/evm/src/lib.rs b/crates/ethereum/evm/src/lib.rs index d94ebc968..b6deece37 100644 --- a/crates/ethereum/evm/src/lib.rs +++ b/crates/ethereum/evm/src/lib.rs @@ -21,6 +21,9 @@ pub mod execute; /// Ethereum DAO hardfork state change data. pub mod dao_fork; +/// [EIP-6110](https://eips.ethereum.org/EIPS/eip-6110) handling. +pub mod eip6110; + /// Ethereum-related EVM configuration. #[derive(Debug, Clone, Copy, Default)] #[non_exhaustive] diff --git a/crates/evm/execution-errors/Cargo.toml b/crates/evm/execution-errors/Cargo.toml index c04b2b122..f370f8297 100644 --- a/crates/evm/execution-errors/Cargo.toml +++ b/crates/evm/execution-errors/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true workspace = true [dependencies] +# reth reth-consensus.workspace = true reth-primitives.workspace = true reth-storage-errors.workspace = true diff --git a/crates/evm/execution-errors/src/lib.rs b/crates/evm/execution-errors/src/lib.rs index c2b5d2f0d..4d954ba1e 100644 --- a/crates/evm/execution-errors/src/lib.rs +++ b/crates/evm/execution-errors/src/lib.rs @@ -77,15 +77,24 @@ pub enum BlockValidationError { /// The error message. message: String, }, - /// Provider error during the [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) block hash account loading. + /// Provider error during the [EIP-2935] block hash account loading. + /// + /// [EIP-2935]: https://eips.ethereum.org/EIPS/eip-2935 #[error(transparent)] BlockHashAccountLoadingFailed(#[from] ProviderError), - /// EVM error during withdrawal requests contract call + /// EVM error during withdrawal requests contract call [EIP-7002] + /// + /// [EIP-7002]: https://eips.ethereum.org/EIPS/eip-7002 #[error("failed to apply withdrawal requests contract call: {message}")] WithdrawalRequestsContractCall { /// The error message. message: String, }, + /// Error when decoding deposit requests from receipts [EIP-6110] + /// + /// [EIP-6110]: https://eips.ethereum.org/EIPS/eip-6110 + #[error("failed to decode deposit requests from receipts: {0}")] + DepositRequestDecode(String), } /// BlockExecutor Errors diff --git a/crates/payload/basic/Cargo.toml b/crates/payload/basic/Cargo.toml index b5c795815..a19d32abb 100644 --- a/crates/payload/basic/Cargo.toml +++ b/crates/payload/basic/Cargo.toml @@ -35,4 +35,4 @@ reth-metrics.workspace = true metrics.workspace = true # misc -tracing.workspace = true \ No newline at end of file +tracing.workspace = true diff --git a/crates/payload/ethereum/Cargo.toml b/crates/payload/ethereum/Cargo.toml index 245bc8ebf..883752a96 100644 --- a/crates/payload/ethereum/Cargo.toml +++ b/crates/payload/ethereum/Cargo.toml @@ -21,6 +21,7 @@ reth-payload-builder.workspace = true reth-basic-payload-builder.workspace = true reth-evm.workspace = true reth-evm-ethereum.workspace = true +reth-errors.workspace = true # ethereum revm.workspace = true diff --git a/crates/payload/ethereum/src/lib.rs b/crates/payload/ethereum/src/lib.rs index 86ad733f9..8a0ac50ce 100644 --- a/crates/payload/ethereum/src/lib.rs +++ b/crates/payload/ethereum/src/lib.rs @@ -14,8 +14,9 @@ use reth_basic_payload_builder::{ pre_block_beacon_root_contract_call, BuildArguments, BuildOutcome, PayloadBuilder, PayloadConfig, WithdrawalsOutcome, }; +use reth_errors::RethError; use reth_evm::ConfigureEvm; -use reth_evm_ethereum::EthEvmConfig; +use reth_evm_ethereum::{eip6110::parse_deposits_from_receipts, EthEvmConfig}; use reth_payload_builder::{ error::PayloadBuilderError, EthBuiltPayload, EthPayloadBuilderAttributes, }; @@ -417,21 +418,23 @@ where } // calculate the requests and the requests root - let (requests, requests_root) = - if chain_spec.is_prague_active_at_timestamp(attributes.timestamp) { - let withdrawal_requests = post_block_withdrawal_requests_contract_call( - &mut db, - &initialized_cfg, - &initialized_block_env, - )?; + let (requests, requests_root) = if chain_spec + .is_prague_active_at_timestamp(attributes.timestamp) + { + let deposit_requests = parse_deposits_from_receipts(&chain_spec, receipts.iter().flatten()) + .map_err(|err| PayloadBuilderError::Internal(RethError::Execution(err.into())))?; + let withdrawal_requests = post_block_withdrawal_requests_contract_call( + &mut db, + &initialized_cfg, + &initialized_block_env, + )?; - // TODO: add deposit requests - let requests = withdrawal_requests; - let requests_root = calculate_requests_root(&requests); - (Some(requests.into()), Some(requests_root)) - } else { - (None, None) - }; + let requests = [deposit_requests, withdrawal_requests].concat(); + let requests_root = calculate_requests_root(&requests); + (Some(requests.into()), Some(requests_root)) + } else { + (None, None) + }; let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals(&mut db, &chain_spec, attributes.timestamp, attributes.withdrawals)?; diff --git a/crates/primitives/src/chain/mod.rs b/crates/primitives/src/chain/mod.rs index b04e88ee0..727abe038 100644 --- a/crates/primitives/src/chain/mod.rs +++ b/crates/primitives/src/chain/mod.rs @@ -2,7 +2,8 @@ pub use alloy_chains::{Chain, ChainKind, NamedChain}; pub use info::ChainInfo; pub use spec::{ AllGenesisFormats, BaseFeeParams, BaseFeeParamsKind, ChainSpec, ChainSpecBuilder, - DisplayHardforks, ForkBaseFeeParams, ForkCondition, DEV, GOERLI, HOLESKY, MAINNET, SEPOLIA, + DepositContract, DisplayHardforks, ForkBaseFeeParams, ForkCondition, DEV, GOERLI, HOLESKY, + MAINNET, SEPOLIA, }; #[cfg(feature = "optimism")] pub use spec::{BASE_MAINNET, BASE_SEPOLIA, OP_MAINNET, OP_SEPOLIA}; diff --git a/crates/primitives/src/chain/spec.rs b/crates/primitives/src/chain/spec.rs index 5c56fbecd..85da33a98 100644 --- a/crates/primitives/src/chain/spec.rs +++ b/crates/primitives/src/chain/spec.rs @@ -9,7 +9,7 @@ use crate::{ revm_primitives::{address, b256}, Address, BlockNumber, Chain, ChainKind, ForkFilter, ForkFilterKey, ForkHash, ForkId, Genesis, Hardfork, Head, Header, NamedChain, NodeRecord, SealedHeader, B256, EMPTY_OMMER_ROOT_HASH, - U256, + MAINNET_DEPOSIT_CONTRACT, U256, }; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -1053,13 +1053,21 @@ impl From for ChainSpec { hardforks.extend(time_hardforks); + // NOTE: in full node, we prune all receipts except the deposit contract's. We do not + // have the deployment block in the genesis file, so we use block zero. We use the same + // deposit topic as the mainnet contract if we have the deposit contract address in the + // genesis json. + let deposit_contract = genesis.config.deposit_contract_address.map(|address| { + DepositContract { address, block: 0, topic: MAINNET_DEPOSIT_CONTRACT.topic } + }); + Self { chain: genesis.config.chain_id.into(), genesis, genesis_hash: None, hardforks, paris_block_and_final_difficulty, - deposit_contract: None, + deposit_contract, ..Default::default() } } @@ -1601,7 +1609,8 @@ pub struct DepositContract { } impl DepositContract { - const fn new(address: Address, block: BlockNumber, topic: B256) -> Self { + /// Creates a new [DepositContract]. + pub const fn new(address: Address, block: BlockNumber, topic: B256) -> Self { Self { address, block, topic } } } diff --git a/crates/primitives/src/constants/mod.rs b/crates/primitives/src/constants/mod.rs index 1df3d0284..f04868c6c 100644 --- a/crates/primitives/src/constants/mod.rs +++ b/crates/primitives/src/constants/mod.rs @@ -1,6 +1,10 @@ //! Ethereum protocol-related constants -use crate::{revm_primitives::b256, B256, U256}; +use crate::{ + chain::DepositContract, + revm_primitives::{address, b256}, + B256, U256, +}; use std::time::Duration; #[cfg(feature = "optimism")] @@ -64,6 +68,13 @@ pub const EIP1559_DEFAULT_ELASTICITY_MULTIPLIER: u64 = 2; /// Minimum gas limit allowed for transactions. pub const MINIMUM_GAS_LIMIT: u64 = 5000; +/// Deposit contract address +pub const MAINNET_DEPOSIT_CONTRACT: DepositContract = DepositContract::new( + address!("00000000219ab540356cbb839cbe05303d7705fa"), + 11052984, + b256!("649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"), +); + /// Base fee max change denominator for Optimism Mainnet as defined in the Optimism /// [transaction costs](https://community.optimism.io/docs/developers/build/differences/#transaction-costs) doc. #[cfg(feature = "optimism")] diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index d3ea34037..b9a74d7da 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -56,14 +56,14 @@ pub use block::{ }; pub use chain::{ AllGenesisFormats, BaseFeeParams, BaseFeeParamsKind, Chain, ChainInfo, ChainKind, ChainSpec, - ChainSpecBuilder, DisplayHardforks, ForkBaseFeeParams, ForkCondition, NamedChain, DEV, GOERLI, - HOLESKY, MAINNET, SEPOLIA, + ChainSpecBuilder, DepositContract, DisplayHardforks, ForkBaseFeeParams, ForkCondition, + NamedChain, DEV, GOERLI, HOLESKY, MAINNET, SEPOLIA, }; #[cfg(feature = "zstd-codec")] pub use compression::*; pub use constants::{ DEV_GENESIS_HASH, EMPTY_OMMER_ROOT_HASH, GOERLI_GENESIS_HASH, HOLESKY_GENESIS_HASH, - KECCAK_EMPTY, MAINNET_GENESIS_HASH, SEPOLIA_GENESIS_HASH, + KECCAK_EMPTY, MAINNET_DEPOSIT_CONTRACT, MAINNET_GENESIS_HASH, SEPOLIA_GENESIS_HASH, }; pub use error::{GotExpected, GotExpectedBoxed}; pub use exex::FinishedExExHeight;