diff --git a/Cargo.lock b/Cargo.lock index acd81f9f0..a6b4b8985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4116,6 +4116,7 @@ dependencies = [ name = "reth-consensus" version = "0.1.0" dependencies = [ + "assert_matches", "reth-interfaces", "reth-primitives", "reth-provider", diff --git a/bin/reth/src/test_eth_chain/models.rs b/bin/reth/src/test_eth_chain/models.rs index 0bb5006da..858c320d7 100644 --- a/bin/reth/src/test_eth_chain/models.rs +++ b/bin/reth/src/test_eth_chain/models.rs @@ -1,6 +1,6 @@ use reth_primitives::{ Address, BigEndianHash, Bloom, Bytes, ChainSpec, ChainSpecBuilder, Header as RethHeader, - JsonU256, SealedHeader, H160, H256, H64, U256, U64, + JsonU256, SealedHeader, Withdrawal, H160, H256, H64, }; use serde::{self, Deserialize}; use std::collections::BTreeMap; @@ -71,6 +71,8 @@ pub struct Header { pub uncle_hash: H256, /// Base fee per gas. pub base_fee_per_gas: Option, + /// Withdrawals root. + pub withdrawals_root: Option, } impl From
for SealedHeader { @@ -92,7 +94,8 @@ impl From
for SealedHeader { ommers_hash: value.uncle_hash, state_root: value.state_root, parent_hash: value.parent_hash, - logs_bloom: Bloom::default(), // TODO: ? + logs_bloom: value.bloom, + withdrawals_root: value.withdrawals_root, }, value.hash, ) @@ -128,16 +131,6 @@ pub struct TransactionSequence { valid: String, } -/// Withdrawal in block -#[derive(Default, Clone, Debug, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Withdrawal { - index: U64, - validator_index: U64, - address: Address, - amount: U256, -} - /// Ethereum blockchain test data state. #[derive(Clone, Debug, Eq, PartialEq, Deserialize)] pub struct State(pub BTreeMap); diff --git a/bin/reth/src/test_eth_chain/runner.rs b/bin/reth/src/test_eth_chain/runner.rs index b6f479f24..0b616ce57 100644 --- a/bin/reth/src/test_eth_chain/runner.rs +++ b/bin/reth/src/test_eth_chain/runner.rs @@ -118,8 +118,6 @@ pub async fn run_test(path: PathBuf) -> eyre::Result { continue } - // if matches!(suite.pre, State(RootOrState::Root(_))) {} - let pre_state = suite.pre.0; debug!(target: "reth::cli", name, network = ?suite.network, "Running test"); @@ -134,14 +132,14 @@ pub async fn run_test(path: PathBuf) -> eyre::Result { // insert genesis let header: SealedHeader = suite.genesis_block_header.into(); - let genesis_block = SealedBlock { header, body: vec![], ommers: vec![] }; + let genesis_block = SealedBlock { header, body: vec![], ommers: vec![], withdrawals: None }; reth_provider::insert_canonical_block(&tx, &genesis_block, has_block_reward)?; let mut last_block = None; suite.blocks.iter().try_for_each(|block| -> eyre::Result<()> { let decoded = SealedBlock::decode(&mut block.rlp.as_ref())?; - last_block = Some(decoded.number); reth_provider::insert_canonical_block(&tx, &decoded, has_block_reward)?; + last_block = Some(decoded.number); Ok(()) })?; diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index 769bd7332..1cd67f69d 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -18,3 +18,4 @@ tokio = { version = "1", features = ["sync"] } [dev-dependencies] reth-interfaces = { path = "../interfaces", features = ["test-utils"] } reth-provider = { path = "../storage/provider", features = ["test-utils"] } +assert_matches = "1.5.0" diff --git a/crates/consensus/src/beacon/beacon_consensus.rs b/crates/consensus/src/beacon/beacon_consensus.rs index 6da2eb472..779073d50 100644 --- a/crates/consensus/src/beacon/beacon_consensus.rs +++ b/crates/consensus/src/beacon/beacon_consensus.rs @@ -78,7 +78,7 @@ impl Consensus for BeaconConsensus { } fn pre_validate_block(&self, block: &SealedBlock) -> Result<(), Error> { - validation::validate_block_standalone(block) + validation::validate_block_standalone(block, &self.chain_spec) } fn has_block_reward(&self, total_difficulty: U256, difficulty: U256) -> bool { diff --git a/crates/consensus/src/validation.rs b/crates/consensus/src/validation.rs index 553263585..d1d71cfcf 100644 --- a/crates/consensus/src/validation.rs +++ b/crates/consensus/src/validation.rs @@ -45,6 +45,15 @@ pub fn validate_header_standalone( return Err(Error::BaseFeeMissing) } + // EIP-4895: Beacon chain push withdrawals as operations + if chain_spec.fork(Hardfork::Shanghai).active_at_timestamp(header.timestamp) && + header.withdrawals_root.is_none() + { + return Err(Error::WithdrawalsRootMissing) + } else if header.withdrawals_root.is_some() { + return Err(Error::WithdrawalsRootUnexpected) + } + Ok(()) } @@ -167,7 +176,7 @@ pub fn validate_all_transaction_regarding_block_and_nonces< /// - Compares the transactions root in the block header to the block body /// - Pre-execution transaction validation /// - (Optionally) Compares the receipts root in the block header to the block body -pub fn validate_block_standalone(block: &SealedBlock) -> Result<(), Error> { +pub fn validate_block_standalone(block: &SealedBlock, chain_spec: &ChainSpec) -> Result<(), Error> { // Check ommers hash // TODO(onbjerg): This should probably be accessible directly on [Block] let ommers_hash = @@ -189,6 +198,33 @@ pub fn validate_block_standalone(block: &SealedBlock) -> Result<(), Error> { }) } + // EIP-4895: Beacon chain push withdrawals as operations + if chain_spec.fork(Hardfork::Shanghai).active_at_timestamp(block.timestamp) { + let withdrawals = block.withdrawals.as_ref().ok_or(Error::BodyWithdrawalsMissing)?; + let withdrawals_root = + reth_primitives::proofs::calculate_withdrawals_root(withdrawals.iter()); + let header_withdrawals_root = + block.withdrawals_root.as_ref().ok_or(Error::WithdrawalsRootMissing)?; + if withdrawals_root != *header_withdrawals_root { + return Err(Error::BodyWithdrawalsRootDiff { + got: withdrawals_root, + expected: *header_withdrawals_root, + }) + } + + // Validate that withdrawal index is monotonically increasing within a block. + if let Some(first) = withdrawals.first() { + let mut prev_index = first.index; + for withdrawal in withdrawals.iter().skip(1) { + let expected = prev_index + 1; + if expected != withdrawal.index { + return Err(Error::WithdrawalIndexInvalid { got: withdrawal.index, expected }) + } + prev_index = withdrawal.index; + } + } + } + Ok(()) } @@ -326,7 +362,7 @@ pub fn full_validation( chain_spec: &ChainSpec, ) -> RethResult<()> { validate_header_standalone(&block.header, chain_spec)?; - validate_block_standalone(block)?; + validate_block_standalone(block, chain_spec)?; let parent = validate_block_regarding_chain(block, &provider)?; validate_header_regarding_parent(&parent, &block.header, chain_spec)?; @@ -348,15 +384,15 @@ pub fn full_validation( #[cfg(test)] mod tests { + use super::*; + use assert_matches::assert_matches; use reth_interfaces::Result; use reth_primitives::{ - hex_literal::hex, Account, Address, BlockHash, Bytes, Header, Signature, TransactionKind, - TransactionSigned, MAINNET, U256, + hex_literal::hex, proofs, Account, Address, BlockHash, Bytes, ChainSpecBuilder, Header, + Signature, TransactionKind, TransactionSigned, Withdrawal, MAINNET, U256, }; use std::ops::RangeBounds; - use super::*; - #[test] fn calculate_base_fee_success() { let base_fee = [ @@ -447,6 +483,7 @@ mod tests { let signer = Address::zero(); TransactionSignedEcRecovered::from_signed_transaction(tx, signer) } + /// got test block fn mock_block() -> (SealedBlock, Header) { // https://etherscan.io/block/15867168 where transaction root and receipts root are cleared @@ -469,6 +506,7 @@ mod tests { mix_hash: hex!("0000000000000000000000000000000000000000000000000000000000000000").into(), nonce: 0x0000000000000000, base_fee_per_gas: 0x28f0001df.into(), + withdrawals_root: None }; // size: 0x9b5 @@ -481,7 +519,7 @@ mod tests { let ommers = Vec::new(); let body = Vec::new(); - (SealedBlock { header: header.seal(), body, ommers }, parent) + (SealedBlock { header: header.seal(), body, ommers, withdrawals: None }, parent) } #[test] @@ -557,4 +595,47 @@ mod tests { Err(Error::TransactionNonceNotConsistent.into()) ); } + + #[test] + fn valid_withdrawal_index() { + let chain_spec = ChainSpecBuilder::mainnet().shanghai_activated().build(); + + let create_block_with_withdrawals = |indexes: &[u64]| { + let withdrawals = indexes + .iter() + .map(|idx| Withdrawal { index: *idx, ..Default::default() }) + .collect::>(); + SealedBlock { + header: Header { + withdrawals_root: Some(proofs::calculate_withdrawals_root(withdrawals.iter())), + ..Default::default() + } + .seal(), + withdrawals: Some(withdrawals), + ..Default::default() + } + }; + + // Single withdrawal + let block = create_block_with_withdrawals(&[1]); + assert_eq!(validate_block_standalone(&block, &chain_spec), Ok(())); + + // Multiple increasing withdrawals + let block = create_block_with_withdrawals(&[1, 2, 3]); + assert_eq!(validate_block_standalone(&block, &chain_spec), Ok(())); + let block = create_block_with_withdrawals(&[5, 6, 7, 8, 9]); + assert_eq!(validate_block_standalone(&block, &chain_spec), Ok(())); + + // Invalid withdrawal index + let block = create_block_with_withdrawals(&[100, 102]); + assert_matches!( + validate_block_standalone(&block, &chain_spec), + Err(Error::WithdrawalIndexInvalid { .. }) + ); + let block = create_block_with_withdrawals(&[5, 6, 7, 9]); + assert_matches!( + validate_block_standalone(&block, &chain_spec), + Err(Error::WithdrawalIndexInvalid { .. }) + ); + } } diff --git a/crates/executor/src/execution_result.rs b/crates/executor/src/execution_result.rs index 1c9480d84..3f1b433fa 100644 --- a/crates/executor/src/execution_result.rs +++ b/crates/executor/src/execution_result.rs @@ -8,10 +8,10 @@ use std::collections::BTreeMap; #[derive(Debug)] pub struct ExecutionResult { /// Transaction changeset containing [Receipt], changed [Accounts][Account] and Storages. - pub changesets: Vec, - /// Block reward if present. It represent changeset for block reward slot in - /// [tables::AccountChangeSet] . - pub block_reward: Option>, + pub tx_changesets: Vec, + /// Post block account changesets. This might include block reward, uncle rewards, withdrawals + /// or irregular state changes (DAO fork). + pub block_changesets: BTreeMap, } /// After transaction is executed this structure contain diff --git a/crates/executor/src/executor.rs b/crates/executor/src/executor.rs index e4c60e265..ec3bb77fb 100644 --- a/crates/executor/src/executor.rs +++ b/crates/executor/src/executor.rs @@ -9,7 +9,7 @@ use hashbrown::hash_map::Entry; use reth_interfaces::executor::{BlockExecutor, Error}; use reth_primitives::{ bloom::logs_bloom, Account, Address, Block, Bloom, ChainSpec, Hardfork, Head, Header, Log, - Receipt, TransactionSigned, H160, H256, U256, + Receipt, TransactionSigned, H256, U256, }; use reth_provider::StateProvider; use revm::{ @@ -19,7 +19,7 @@ use revm::{ }, EVM, }; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; /// Main block executor pub struct Executor<'a, DB> @@ -93,7 +93,7 @@ where /// BTreeMap is used to have sorted values fn commit_changes( &mut self, - changes: hashbrown::HashMap, + changes: hashbrown::HashMap, ) -> (BTreeMap, BTreeMap) { let db = self.db(); @@ -205,23 +205,58 @@ where (change, new_bytecodes) } - /// Calculate Block reward changeset - pub fn block_reward_changeset( + /// Collect all balance changes at the end of the block. Balance changes might include block + /// reward, uncle rewards, withdrawals or irregular state changes (DAO fork). + fn post_block_balance_increments( &mut self, - header: &Header, - total_difficulty: U256, - ommers: &[Header], - ) -> Result>, Error> { - // NOTE: Related to Ethereum reward change, for other network this is probably going to be - // moved to config. + block: &Block, + td: U256, + ) -> Result, Error> { + let mut balance_increments = HashMap::::default(); - // From yellowpapper Page 15: - // 11.3. Reward Application. The application of rewards to a block involves raising the - // balance of the accounts of the beneficiary address of the block and each ommer by - // a certain amount. We raise the block’s beneficiary account by Rblock; for each - // ommer, we raise the block’s beneficiary by an additional 1/32 of the block reward - // and the beneficiary of the ommer gets rewarded depending on the blocknumber. - // Formally we define the function Ω: + // Collect balance increments for block and uncle rewards. + if let Some(reward) = self.get_block_reward(block, td) { + // Calculate Uncle reward + // OpenEthereum code: https://github.com/openethereum/openethereum/blob/6c2d392d867b058ff867c4373e40850ca3f96969/crates/ethcore/src/ethereum/ethash.rs#L319-L333 + for ommer in block.ommers.iter() { + let ommer_reward = + U256::from(((8 + ommer.number - block.number) as u128 * reward) >> 3); + // From yellowpaper Page 15: + // If there are collisions of the beneficiary addresses between ommers and the + // block (i.e. two ommers with the same beneficiary address + // or an ommer with the same beneficiary address as the + // present block), additions are applied cumulatively + *balance_increments.entry(ommer.beneficiary).or_default() += ommer_reward; + } + + // Increment balance for main block reward. + let block_reward = U256::from(reward + (reward >> 5) * block.ommers.len() as u128); + *balance_increments.entry(block.beneficiary).or_default() += block_reward; + } + + if self.chain_spec.fork(Hardfork::Shanghai).active_at_timestamp(block.timestamp) { + if let Some(withdrawals) = block.withdrawals.as_ref() { + for withdrawal in withdrawals { + *balance_increments.entry(withdrawal.address).or_default() += + withdrawal.amount_wei(); + } + } + } + + Ok(balance_increments) + } + + /// From yellowpapper Page 15: + /// 11.3. Reward Application. The application of rewards to a block involves raising the + /// balance of the accounts of the beneficiary address of the block and each ommer by + /// a certain amount. We raise the block’s beneficiary account by Rblock; for each + /// ommer, we raise the block’s beneficiary by an additional 1/32 of the block reward + /// and the beneficiary of the ommer gets rewarded depending on the blocknumber. + /// Formally we define the function Ω. + /// + /// NOTE: Related to Ethereum reward change, for other network this is probably going to be + /// moved to config. + fn get_block_reward(&self, header: &Header, total_difficulty: U256) -> Option { if self.chain_spec.fork(Hardfork::Paris).active_at_ttd(total_difficulty, header.difficulty) { None @@ -232,78 +267,10 @@ where } else { Some(WEI_5ETH) } - .map(|reward| -> Result<_, _> { - let mut reward_beneficiaries: BTreeMap = BTreeMap::new(); - // Calculate Uncle reward - // OpenEthereum code: https://github.com/openethereum/openethereum/blob/6c2d392d867b058ff867c4373e40850ca3f96969/crates/ethcore/src/ethereum/ethash.rs#L319-L333 - for ommer in ommers { - let ommer_reward = ((8 + ommer.number - header.number) as u128 * reward) >> 3; - // From yellowpaper Page 15: - // If there are collisions of the beneficiary addresses between ommers and the block - // (i.e. two ommers with the same beneficiary address or an ommer with the - // same beneficiary address as the present block), additions are applied - // cumulatively - *reward_beneficiaries.entry(ommer.beneficiary).or_default() += ommer_reward; - } - // insert main block reward - *reward_beneficiaries.entry(header.beneficiary).or_default() += - reward + (reward >> 5) * ommers.len() as u128; - - // - let db = self.db(); - - // create changesets for beneficiaries rewards (Main block and ommers); - reward_beneficiaries - .into_iter() - .map(|(beneficiary, reward)| -> Result<_, _> { - let changeset = db - .load_account(beneficiary) - .map_err(|_| Error::ProviderError) - // if account is present append `Changed` changeset for block reward - .map(|db_acc| { - let old = to_reth_acc(&db_acc.info); - let mut new = old; - new.balance += U256::from(reward); - db_acc.info.balance = new.balance; - match db_acc.account_state { - AccountState::NotExisting => { - // if account was not existing that means that storage is not - // present. - db_acc.account_state = AccountState::StorageCleared; - - // if account was not present append `Created` changeset - AccountInfoChangeSet::Created { - new: Account { - nonce: 0, - balance: new.balance, - bytecode_hash: None, - }, - } - } - - AccountState::StorageCleared | - AccountState::Touched | - AccountState::None => { - // If account is None that means that EVM didn't touch it. - // we are changing the state to Touched as account can have - // storage in db. - if db_acc.account_state == AccountState::None { - db_acc.account_state = AccountState::Touched; - } - // if account was present, append changed changeset. - AccountInfoChangeSet::Changed { new, old } - } - } - })?; - Ok((beneficiary, changeset)) - }) - .collect::, _>>() - }) - .transpose() } /// Irregular state change at Ethereum DAO hardfork - pub fn dao_fork_changeset(&mut self) -> Result, Error> { + fn dao_fork_changeset(&mut self) -> Result, Error> { let db = self.db(); let mut drained_balance = U256::ZERO; @@ -320,7 +287,7 @@ where // assume it is changeset as it is irregular state change Ok((address, AccountInfoChangeSet::Changed { new, old })) }) - .collect::, _>>()?; + .collect::, _>>()?; // add drained ether to beneficiary. let beneficiary = crate::eth_dao_fork::DAO_HARDFORK_BENEFICIARY; @@ -338,6 +305,43 @@ where Ok(changesets) } + + /// Generate balance increment account changeset and mutate account database entry in place. + fn account_balance_increment_changeset( + &mut self, + address: Address, + increment: U256, + ) -> Result { + let db = self.db(); + let beneficiary = db.load_account(address).map_err(|_| Error::ProviderError)?; + let old = to_reth_acc(&beneficiary.info); + // Increment beneficiary balance by mutating db entry in place. + beneficiary.info.balance += increment; + let new = to_reth_acc(&beneficiary.info); + match beneficiary.account_state { + AccountState::NotExisting => { + // if account was not existing that means that storage is not + // present. + beneficiary.account_state = AccountState::StorageCleared; + + // if account was not present append `Created` changeset + Ok(AccountInfoChangeSet::Created { + new: Account { nonce: 0, balance: new.balance, bytecode_hash: None }, + }) + } + + AccountState::StorageCleared | AccountState::Touched | AccountState::None => { + // If account is None that means that EVM didn't touch it. + // we are changing the state to Touched as account can have + // storage in db. + if beneficiary.account_state == AccountState::None { + beneficiary.account_state = AccountState::Touched; + } + // if account was present, append changed changeset. + Ok(AccountInfoChangeSet::Changed { new, old }) + } + } + } } impl<'a, DB> BlockExecutor for Executor<'a, DB> @@ -350,19 +354,18 @@ where total_difficulty: U256, senders: Option>, ) -> Result { - let Block { header, body, ommers } = block; - let senders = self.recover_senders(body, senders)?; + let senders = self.recover_senders(&block.body, senders)?; - self.init_block_env(header, total_difficulty); + self.init_block_env(&block.header, total_difficulty); let mut cumulative_gas_used = 0; // output of execution - let mut changesets = Vec::with_capacity(body.len()); + let mut tx_changesets = Vec::with_capacity(block.body.len()); - for (transaction, sender) in body.iter().zip(senders.into_iter()) { + for (transaction, sender) in block.body.iter().zip(senders.into_iter()) { // The sum of the transaction’s gas limit, Tg, and the gas utilised in this block prior, // must be no greater than the block’s gasLimit. - let block_available_gas = header.gas_limit - cumulative_gas_used; + let block_available_gas = block.header.gas_limit - cumulative_gas_used; if transaction.gas_limit() > block_available_gas { return Err(Error::TransactionGasLimitMoreThenAvailableBlockGas { transaction_gas_limit: transaction.gas_limit(), @@ -376,13 +379,13 @@ where // Execute transaction. let out = if self.use_printer_tracer { // execution with inspector. - let out = self.evm.inspect(revm::inspectors::CustomPrintTracer::default()); - - tracing::trace!(target:"evm","Executed transaction: {:?}, - \nExecution output:{out:?}: - \nTransaction data:{transaction:?} - \nenv data:{:?}",transaction.hash(),self.evm.env); - out + let output = self.evm.inspect(revm::inspectors::CustomPrintTracer::default()); + tracing::trace!( + target: "evm", + hash = ?transaction.hash(), ?output, ?transaction, env = ?self.evm.env, + "Executed transaction" + ); + output } else { // main execution. self.evm.transact() @@ -401,7 +404,7 @@ where let logs: Vec = result.logs().into_iter().map(into_reth_log).collect(); // Push transaction changeset and calculate header bloom filter for receipt. - changesets.push(TransactionChangeSet { + tx_changesets.push(TransactionChangeSet { receipt: Receipt { tx_type: transaction.tx_type(), // Success flag was added in `EIP-658: Embedding transaction status code in @@ -417,19 +420,25 @@ where } // Check if gas used matches the value set in header. - if header.gas_used != cumulative_gas_used { - return Err(Error::BlockGasUsed { got: cumulative_gas_used, expected: header.gas_used }) + if block.gas_used != cumulative_gas_used { + return Err(Error::BlockGasUsed { got: cumulative_gas_used, expected: block.gas_used }) } - let mut block_reward = self.block_reward_changeset(header, total_difficulty, ommers)?; - - if self.chain_spec.fork(Hardfork::Dao).transitions_at_block(header.number) { - let mut irregular_state_changeset = self.dao_fork_changeset()?; - irregular_state_changeset.extend(block_reward.take().unwrap_or_default().into_iter()); - block_reward = Some(irregular_state_changeset); + let mut block_changesets = BTreeMap::default(); + let balance_increments = self.post_block_balance_increments(block, total_difficulty)?; + for (address, increment) in balance_increments { + let changeset = self.account_balance_increment_changeset(address, increment)?; + block_changesets.insert(address, changeset); } - Ok(ExecutionResult { changesets, block_reward }) + if self.chain_spec.fork(Hardfork::Dao).transitions_at_block(block.number) { + for (address, changeset) in self.dao_fork_changeset()? { + // No account collision between rewarded accounts and DAO fork related accounts. + block_changesets.insert(address, changeset); + } + } + + Ok(ExecutionResult { tx_changesets, block_changesets }) } } @@ -443,7 +452,7 @@ pub fn execute_and_verify_receipt( ) -> Result { let execution_result = execute(block, total_difficulty, senders, chain_spec, db)?; - let receipts_iter = execution_result.changesets.iter().map(|changeset| &changeset.receipt); + let receipts_iter = execution_result.tx_changesets.iter().map(|changeset| &changeset.receipt); if chain_spec.fork(Hardfork::Byzantium).active_at_block(block.header.number) { verify_receipt(block.header.receipts_root, block.header.logs_bloom, receipts_iter)?; @@ -497,17 +506,15 @@ pub fn execute( #[cfg(test)] mod tests { - use std::collections::HashMap; - + use super::*; use crate::revm_wrap::State; use reth_primitives::{ hex_literal::hex, keccak256, Account, Address, Bytes, ChainSpecBuilder, ForkCondition, - SealedBlock, StorageKey, H160, H256, MAINNET, U256, + StorageKey, H256, MAINNET, U256, }; use reth_provider::{AccountProvider, BlockHashProvider, StateProvider}; use reth_rlp::Decodable; - - use super::*; + use std::{collections::HashMap, str::FromStr}; #[derive(Debug, Default, Clone, Eq, PartialEq)] struct StateProviderTest { @@ -569,23 +576,22 @@ mod tests { // Got rlp block from: src/GeneralStateTestsFiller/stChainId/chainIdGasCostFiller.json let mut block_rlp = hex!("f90262f901f9a075c371ba45999d87f4542326910a11af515897aebce5265d3f6acd1f1161f82fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa098f2dcd87c8ae4083e7017a05456c14eea4b1db2032126e27b3b1563d57d7cc0a08151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcba03f4e5c2ec5b2170b711d97ee755c160457bb58d8daa338e835ec02ae6860bbabb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018502540be40082a8798203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f863f861800a8405f5e10094100000000000000000000000000000000000000080801ba07e09e26678ed4fac08a249ebe8ed680bf9051a5e14ad223e4b2b9d26e0208f37a05f6e3f188e3e6eab7d7d3b6568f5eac7d687b08d307d3154ccd8c87b4630509bc0").as_slice(); - let block = SealedBlock::decode(&mut block_rlp).unwrap(); + let mut block = Block::decode(&mut block_rlp).unwrap(); let mut ommer = Header::default(); - let ommer_beneficiary = H160(hex!("3000000000000000000000000000000000000000")); + let ommer_beneficiary = + Address::from_str("3000000000000000000000000000000000000000").unwrap(); ommer.beneficiary = ommer_beneficiary; ommer.number = block.number; - let ommers = vec![ommer]; - - let block = Block { header: block.header.unseal(), body: block.body, ommers }; + block.ommers = vec![ommer]; let mut db = StateProviderTest::default(); - let account1 = H160(hex!("1000000000000000000000000000000000000000")); - let account2 = H160(hex!("2adc25665018aa1fe0e6bc666dac8fc2697ff9ba")); - let account3 = H160(hex!("a94f5374fce5edbc8e2a8697c15331677e6ebf0b")); + let account1 = Address::from_str("1000000000000000000000000000000000000000").unwrap(); + let account2 = Address::from_str("2adc25665018aa1fe0e6bc666dac8fc2697ff9ba").unwrap(); + let account3 = Address::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); - // pre staet + // pre state db.insert_account( account1, Account { balance: U256::ZERO, nonce: 0x00, bytecode_hash: None }, @@ -619,9 +625,9 @@ mod tests { let out = execute_and_verify_receipt(&block, U256::ZERO, None, &chain_spec, &mut db).unwrap(); - assert_eq!(out.changesets.len(), 1, "Should executed one transaction"); + assert_eq!(out.tx_changesets.len(), 1, "Should executed one transaction"); - let changesets = out.changesets[0].clone(); + let changesets = out.tx_changesets[0].clone(); assert_eq!(changesets.new_bytecodes.len(), 0, "Should have zero new bytecodes"); let account1_info = Account { balance: U256::ZERO, nonce: 0x00, bytecode_hash: None }; @@ -644,7 +650,7 @@ mod tests { let cached_acc1 = db.accounts.get(&account1).unwrap(); assert_eq!(cached_acc1.info.balance, account1_info.balance); assert_eq!(cached_acc1.info.nonce, account1_info.nonce); - assert!(matches!(cached_acc1.account_state, AccountState::Touched)); + assert_eq!(cached_acc1.account_state, AccountState::Touched); assert_eq!(cached_acc1.storage.len(), 1); assert_eq!(cached_acc1.storage.get(&U256::from(1)), Some(&U256::from(2))); @@ -659,7 +665,7 @@ mod tests { let cached_acc3 = db.accounts.get(&account3).unwrap(); assert_eq!(cached_acc3.info.balance, account3_info.balance); assert_eq!(cached_acc3.info.nonce, account3_info.nonce); - assert!(matches!(cached_acc3.account_state, AccountState::Touched)); + assert_eq!(cached_acc3.account_state, AccountState::Touched); assert_eq!(cached_acc3.storage.len(), 0); assert_eq!( @@ -685,8 +691,8 @@ mod tests { // check block reward changeset assert_eq!( - out.block_reward, - Some(BTreeMap::from([ + out.block_changesets, + BTreeMap::from([ ( account2, AccountInfoChangeSet::Changed { @@ -704,7 +710,7 @@ mod tests { } } ) - ])) + ]) ); assert_eq!(changesets.new_bytecodes.len(), 0, "No new bytecodes"); @@ -744,14 +750,14 @@ mod tests { let mut db = SubState::new(State::new(db)); // execute chain and verify receipts let out = execute_and_verify_receipt( - &Block { header, body: vec![], ommers: vec![] }, + &Block { header, body: vec![], ommers: vec![], withdrawals: None }, U256::ZERO, None, &chain_spec, &mut db, ) .unwrap(); - assert_eq!(out.changesets.len(), 0, "No tx"); + assert_eq!(out.tx_changesets.len(), 0, "No tx"); // Check if cache is set // beneficiary @@ -765,8 +771,8 @@ mod tests { } // check changesets - let block_reward = out.block_reward.unwrap(); - let change_set = block_reward.get(&crate::eth_dao_fork::DAO_HARDFORK_BENEFICIARY).unwrap(); + let change_set = + out.block_changesets.get(&crate::eth_dao_fork::DAO_HARDFORK_BENEFICIARY).unwrap(); assert_eq!( *change_set, AccountInfoChangeSet::Changed { @@ -775,7 +781,7 @@ mod tests { } ); for (i, address) in crate::eth_dao_fork::DAO_HARDKFORK_ACCOUNTS.iter().enumerate() { - let change_set = block_reward.get(address).unwrap(); + let change_set = out.block_changesets.get(address).unwrap(); assert_eq!( *change_set, AccountInfoChangeSet::Changed { @@ -793,11 +799,12 @@ mod tests { // Got rlp block from: src/GeneralStateTestsFiller/stArgsZeroOneBalance/suicideNonConst.json let mut block_rlp = hex!("f9025ff901f7a0c86e8cc0310ae7c531c758678ddbfd16fc51c8cef8cec650b032de9869e8b94fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa050554882fbbda2c2fd93fdc466db9946ea262a67f7a76cc169e714f105ab583da00967f09ef1dfed20c0eacfaa94d5cd4002eda3242ac47eae68972d07b106d192a0e3c8b47fbfc94667ef4cceb17e5cc21e3b1eebd442cebb27f07562b33836290db90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001830f42408238108203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f862f860800a83061a8094095e7baea6a6c7c4c2dfeb977efac326af552d8780801ba072ed817487b84ba367d15d2f039b5fc5f087d0a8882fbdf73e8cb49357e1ce30a0403d800545b8fc544f92ce8124e2255f8c3c6af93f28243a120585d4c4c6a2a3c0").as_slice(); - let block = SealedBlock::decode(&mut block_rlp).unwrap(); + let block = Block::decode(&mut block_rlp).unwrap(); let mut db = StateProviderTest::default(); - let address_caller = H160(hex!("a94f5374fce5edbc8e2a8697c15331677e6ebf0b")); - let address_selfdestruct = H160(hex!("095e7baea6a6c7c4c2dfeb977efac326af552d87")); + let address_caller = Address::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + let address_selfdestruct = + Address::from_str("095e7baea6a6c7c4c2dfeb977efac326af552d87").unwrap(); // pre state let pre_account_caller = Account { @@ -834,12 +841,11 @@ mod tests { // execute chain and verify receipts let out = - execute_and_verify_receipt(&block.unseal(), U256::ZERO, None, &chain_spec, &mut db) - .unwrap(); + execute_and_verify_receipt(&block, U256::ZERO, None, &chain_spec, &mut db).unwrap(); - assert_eq!(out.changesets.len(), 1, "Should executed one transaction"); + assert_eq!(out.tx_changesets.len(), 1, "Should executed one transaction"); - let changesets = out.changesets[0].clone(); + let changesets = out.tx_changesets[0].clone(); assert_eq!(changesets.new_bytecodes.len(), 0, "Should have zero new bytecodes"); let post_account_caller = Account { @@ -865,4 +871,58 @@ mod tests { assert!(selfdestroyer_changeset.wipe_storage); } + + // Test vector from https://github.com/ethereum/tests/blob/3156db5389921125bb9e04142d18e0e7b0cf8d64/BlockchainTests/EIPTests/bc4895-withdrawals/twoIdenticalIndexDifferentValidator.json + #[test] + fn test_withdrawals() { + let block_rlp = hex!("f9028cf90219a0151934ad9b654c50197f37018ee5ee9bb922dec0a1b5e24a6d679cb111cdb107a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa048cd9a5957e45beebf80278a5208b0cbe975ab4b4adb0da1509c67b26f2be3ffa056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8082079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a04a220ebe55034d51f8a58175bb504b6ebf883105010a1f6d42e557c18bbd5d69c0c0f86cda808094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da028094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da018094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da020194c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710"); + let block = Block::decode(&mut block_rlp.as_slice()).unwrap(); + let withdrawals = block.withdrawals.as_ref().unwrap(); + assert_eq!(withdrawals.len(), 4); + + let withdrawal_beneficiary = + Address::from_str("c94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + // spec at shanghai fork + let chain_spec = ChainSpecBuilder::mainnet().shanghai_activated().build(); + + let mut db = SubState::new(State::new(StateProviderTest::default())); + + // execute chain and verify receipts + let out = + execute_and_verify_receipt(&block, U256::ZERO, None, &chain_spec, &mut db).unwrap(); + assert_eq!(out.tx_changesets.len(), 0, "No tx"); + + let withdrawal_sum = withdrawals.iter().fold(U256::ZERO, |sum, w| sum + w.amount_wei()); + let beneficiary_account = db.accounts.get(&withdrawal_beneficiary).unwrap(); + assert_eq!(beneficiary_account.info.balance, withdrawal_sum); + assert_eq!(beneficiary_account.info.nonce, 0); + assert_eq!(beneficiary_account.account_state, AccountState::StorageCleared); + + assert_eq!(out.block_changesets.len(), 1); + assert_eq!( + out.block_changesets.get(&withdrawal_beneficiary), + Some(&AccountInfoChangeSet::Created { + new: Account { nonce: 0, balance: withdrawal_sum, bytecode_hash: None }, + }) + ); + + // Execute same block again + let out = + execute_and_verify_receipt(&block, U256::ZERO, None, &chain_spec, &mut db).unwrap(); + assert_eq!(out.tx_changesets.len(), 0, "No tx"); + + assert_eq!(out.block_changesets.len(), 1); + assert_eq!( + out.block_changesets.get(&withdrawal_beneficiary), + Some(&AccountInfoChangeSet::Changed { + old: Account { nonce: 0, balance: withdrawal_sum, bytecode_hash: None }, + new: Account { + nonce: 0, + balance: withdrawal_sum + withdrawal_sum, + bytecode_hash: None + }, + }) + ); + } } diff --git a/crates/interfaces/src/consensus.rs b/crates/interfaces/src/consensus.rs index b3dd3d7bb..c940733b6 100644 --- a/crates/interfaces/src/consensus.rs +++ b/crates/interfaces/src/consensus.rs @@ -54,14 +54,16 @@ pub trait Consensus: Debug + Send + Sync { pub enum Error { #[error("Block used gas ({gas_used:?}) is greater than gas limit ({gas_limit:?}).")] HeaderGasUsedExceedsGasLimit { gas_used: u64, gas_limit: u64 }, - #[error("Block ommer hash ({got:?}) is different then expected: ({expected:?})")] + #[error("Block ommer hash ({got:?}) is different from expected: ({expected:?})")] BodyOmmersHashDiff { got: H256, expected: H256 }, - #[error("Block state root ({got:?}) is different then expected: ({expected:?})")] + #[error("Block state root ({got:?}) is different from expected: ({expected:?})")] BodyStateRootDiff { got: H256, expected: H256 }, - #[error("Block transaction root ({got:?}) is different then expected: ({expected:?})")] + #[error("Block transaction root ({got:?}) is different from expected ({expected:?})")] BodyTransactionRootDiff { got: H256, expected: H256 }, - #[error("Block receipts root ({got:?}) is different then expected: ({expected:?}).")] + #[error("Block receipts root ({got:?}) is different from expected ({expected:?})")] BodyReceiptsRootDiff { got: H256, expected: H256 }, + #[error("Block withdrawals root ({got:?}) is different from expected ({expected:?})")] + BodyWithdrawalsRootDiff { got: H256, expected: H256 }, #[error("Block with [hash:{hash:?},number: {number:}] is already known.")] BlockKnown { hash: BlockHash, number: BlockNumber }, #[error("Block parent [hash:{hash:?}] is not known.")] @@ -120,4 +122,12 @@ pub enum Error { TheMergeOmmerRootIsNotEmpty, #[error("Mix hash after merge is not zero")] TheMergeMixHashIsNotZero, + #[error("Missing withdrawals root")] + WithdrawalsRootMissing, + #[error("Unexpected withdrawals root")] + WithdrawalsRootUnexpected, + #[error("Withdrawal index #{got} is invalid. Expected: #{expected}.")] + WithdrawalIndexInvalid { got: u64, expected: u64 }, + #[error("Missing withdrawals")] + BodyWithdrawalsMissing, } diff --git a/crates/interfaces/src/test_utils/generators.rs b/crates/interfaces/src/test_utils/generators.rs index 0fc42312a..10002a037 100644 --- a/crates/interfaces/src/test_utils/generators.rs +++ b/crates/interfaces/src/test_utils/generators.rs @@ -136,6 +136,7 @@ pub fn random_block( .seal(), body: transactions, ommers: ommers.into_iter().map(|ommer| ommer.seal()).collect(), + withdrawals: None, } } diff --git a/crates/net/downloaders/src/bodies/bodies.rs b/crates/net/downloaders/src/bodies/bodies.rs index 4ad0b8a42..f11adc4b3 100644 --- a/crates/net/downloaders/src/bodies/bodies.rs +++ b/crates/net/downloaders/src/bodies/bodies.rs @@ -585,6 +585,7 @@ mod tests { BlockBody { transactions: block.body, ommers: block.ommers.into_iter().map(|header| header.unseal()).collect(), + withdrawals: None, }, ) }) diff --git a/crates/net/downloaders/src/bodies/request.rs b/crates/net/downloaders/src/bodies/request.rs index 1ecc435f7..04f6404b1 100644 --- a/crates/net/downloaders/src/bodies/request.rs +++ b/crates/net/downloaders/src/bodies/request.rs @@ -171,6 +171,7 @@ where header: next_header, body: next_body.transactions, ommers: next_body.ommers.into_iter().map(|header| header.seal()).collect(), + withdrawals: next_body.withdrawals, }; if let Err(error) = self.consensus.pre_validate_block(&block) { diff --git a/crates/net/downloaders/src/bodies/test_utils.rs b/crates/net/downloaders/src/bodies/test_utils.rs index 76d8cd6de..a95d40354 100644 --- a/crates/net/downloaders/src/bodies/test_utils.rs +++ b/crates/net/downloaders/src/bodies/test_utils.rs @@ -26,6 +26,7 @@ pub(crate) fn zip_blocks<'a>( header: header.clone(), body: body.transactions, ommers: body.ommers.into_iter().map(|o| o.seal()).collect(), + withdrawals: body.withdrawals, }) } }) diff --git a/crates/net/downloaders/src/test_utils/file_client.rs b/crates/net/downloaders/src/test_utils/file_client.rs index b25ba8962..064e03dd2 100644 --- a/crates/net/downloaders/src/test_utils/file_client.rs +++ b/crates/net/downloaders/src/test_utils/file_client.rs @@ -102,7 +102,14 @@ impl FileClient { // add to the internal maps headers.insert(block.header.number, block.header.clone()); hash_to_number.insert(block_hash, block.header.number); - bodies.insert(block_hash, BlockBody { transactions: block.body, ommers: block.ommers }); + bodies.insert( + block_hash, + BlockBody { + transactions: block.body, + ommers: block.ommers, + withdrawals: block.withdrawals, + }, + ); } trace!(blocks = headers.len(), "Initialized file client"); diff --git a/crates/net/downloaders/src/test_utils/mod.rs b/crates/net/downloaders/src/test_utils/mod.rs index 90f40d381..93128af09 100644 --- a/crates/net/downloaders/src/test_utils/mod.rs +++ b/crates/net/downloaders/src/test_utils/mod.rs @@ -30,6 +30,7 @@ pub(crate) fn generate_bodies( BlockBody { transactions: block.body, ommers: block.ommers.into_iter().map(|header| header.unseal()).collect(), + withdrawals: block.withdrawals, }, ) }) diff --git a/crates/net/eth-wire/src/types/blocks.rs b/crates/net/eth-wire/src/types/blocks.rs index 37f523a3e..2f93b2dc4 100644 --- a/crates/net/eth-wire/src/types/blocks.rs +++ b/crates/net/eth-wire/src/types/blocks.rs @@ -2,7 +2,7 @@ //! types. use reth_codecs::derive_arbitrary; use reth_primitives::{ - Block, BlockHashOrNumber, Header, HeadersDirection, TransactionSigned, H256, + Block, BlockHashOrNumber, Header, HeadersDirection, TransactionSigned, Withdrawal, H256, }; use reth_rlp::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper}; @@ -70,14 +70,19 @@ impl From> for GetBlockBodies { // TODO(onbjerg): We should have this type in primitives /// A response to [`GetBlockBodies`], containing bodies if any bodies were found. +/// +/// Withdrawals can be optionally included at the end of the RLP encoded message. #[derive_arbitrary(rlp, 10)] -#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Default)] +#[derive(Clone, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[rlp(trailing)] pub struct BlockBody { /// Transactions in the block pub transactions: Vec, /// Uncle headers for the given block pub ommers: Vec
, + /// Withdrawals in the block. + pub withdrawals: Option>, } impl BlockBody { @@ -87,6 +92,7 @@ impl BlockBody { header: header.clone(), body: self.transactions.clone(), ommers: self.ommers.clone(), + withdrawals: self.withdrawals.clone(), } } } @@ -276,6 +282,7 @@ mod test { mix_hash: hex!("0000000000000000000000000000000000000000000000000000000000000000").into(), nonce: 0x0000000000000000u64, base_fee_per_gas: None, + withdrawals_root: None, }, ]), }.encode(&mut data); @@ -306,6 +313,7 @@ mod test { mix_hash: hex!("0000000000000000000000000000000000000000000000000000000000000000").into(), nonce: 0x0000000000000000u64, base_fee_per_gas: None, + withdrawals_root: None, }, ]), }; @@ -417,8 +425,10 @@ mod test { hex!("0000000000000000000000000000000000000000000000000000000000000000").into(), nonce: 0x0000000000000000u64, base_fee_per_gas: None, + withdrawals_root: None, }, ], + withdrawals: None, } ]), }; @@ -499,8 +509,10 @@ mod test { hex!("0000000000000000000000000000000000000000000000000000000000000000").into(), nonce: 0x0000000000000000u64, base_fee_per_gas: None, + withdrawals_root: None, }, ], + withdrawals: None, } ]), }; diff --git a/crates/net/network/src/eth_requests.rs b/crates/net/network/src/eth_requests.rs index 7fc739abe..837a818ce 100644 --- a/crates/net/network/src/eth_requests.rs +++ b/crates/net/network/src/eth_requests.rs @@ -164,7 +164,11 @@ where if let Some(block) = self.client.block(rpc::BlockId::Hash(rpc::H256(hash.0))).unwrap_or_default() { - let body = BlockBody { transactions: block.body, ommers: block.ommers }; + let body = BlockBody { + transactions: block.body, + ommers: block.ommers, + withdrawals: block.withdrawals, + }; bodies.push(body); diff --git a/crates/net/network/tests/it/requests.rs b/crates/net/network/tests/it/requests.rs index f127b8c62..496461408 100644 --- a/crates/net/network/tests/it/requests.rs +++ b/crates/net/network/tests/it/requests.rs @@ -70,7 +70,8 @@ async fn test_get_body() { let blocks = res.unwrap().1; assert_eq!(blocks.len(), 1); - let expected = BlockBody { transactions: block.body, ommers: block.ommers }; + let expected = + BlockBody { transactions: block.body, ommers: block.ommers, withdrawals: None }; assert_eq!(blocks[0], expected); } } diff --git a/crates/primitives/src/block.rs b/crates/primitives/src/block.rs index f34737a20..19fc72d36 100644 --- a/crates/primitives/src/block.rs +++ b/crates/primitives/src/block.rs @@ -1,4 +1,4 @@ -use crate::{Header, SealedHeader, TransactionSigned, H256}; +use crate::{Header, SealedHeader, TransactionSigned, Withdrawal, H256}; use reth_codecs::derive_arbitrary; use reth_rlp::{Decodable, DecodeError, Encodable, RlpDecodable, RlpEncodable}; use serde::{ @@ -9,17 +9,22 @@ use serde::{ use std::{fmt, fmt::Formatter, ops::Deref, str::FromStr}; /// Ethereum full block. +/// +/// Withdrawals can be optionally included at the end of the RLP encoded message. #[derive_arbitrary(rlp, 25)] #[derive( - Debug, Clone, PartialEq, Eq, Default, RlpEncodable, RlpDecodable, Serialize, Deserialize, + Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, RlpEncodable, RlpDecodable, )] +#[rlp(trailing)] pub struct Block { /// Block header. pub header: Header, /// Transactions in this block. pub body: Vec, - /// Ommers/uncles header + /// Ommers/uncles header. pub ommers: Vec
, + /// Block withdrawals. + pub withdrawals: Option>, } impl Deref for Block { @@ -30,10 +35,13 @@ impl Deref for Block { } /// Sealed Ethereum full block. +/// +/// Withdrawals can be optionally included at the end of the RLP encoded message. #[derive_arbitrary(rlp, 10)] #[derive( - Debug, Clone, PartialEq, Eq, Default, RlpEncodable, RlpDecodable, Serialize, Deserialize, + Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, RlpEncodable, RlpDecodable, )] +#[rlp(trailing)] pub struct SealedBlock { /// Locked block header. pub header: SealedHeader, @@ -41,6 +49,8 @@ pub struct SealedBlock { pub body: Vec, /// Ommer/uncle headers pub ommers: Vec, + /// Block withdrawals. + pub withdrawals: Option>, } impl SealedBlock { @@ -60,6 +70,7 @@ impl SealedBlock { header: self.header.unseal(), body: self.body, ommers: self.ommers.into_iter().map(|o| o.unseal()).collect(), + withdrawals: self.withdrawals, } } } diff --git a/crates/primitives/src/chain/spec.rs b/crates/primitives/src/chain/spec.rs index eeb29ed2e..5991df62a 100644 --- a/crates/primitives/src/chain/spec.rs +++ b/crates/primitives/src/chain/spec.rs @@ -426,6 +426,13 @@ impl ChainSpecBuilder { self } + /// Enable Shanghai at genesis. + pub fn shanghai_activated(mut self) -> Self { + self = self.paris_activated(); + self.hardforks.insert(Hardfork::Shanghai, ForkCondition::Timestamp(0)); + self + } + /// Build the resulting [`ChainSpec`]. /// /// # Panics diff --git a/crates/primitives/src/constants.rs b/crates/primitives/src/constants.rs index d551a9706..4739074f8 100644 --- a/crates/primitives/src/constants.rs +++ b/crates/primitives/src/constants.rs @@ -12,6 +12,9 @@ pub const EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) pub const EIP1559_ELASTICITY_MULTIPLIER: u64 = 2; +/// Multiplier for converting gwei to wei. +pub const GWEI_TO_WEI: u64 = 1_000_000_000; + /// The Ethereum mainnet genesis hash. pub const MAINNET_GENESIS: H256 = H256(hex!("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3")); diff --git a/crates/primitives/src/header.rs b/crates/primitives/src/header.rs index 54e37cbdd..3de5708cd 100644 --- a/crates/primitives/src/header.rs +++ b/crates/primitives/src/header.rs @@ -3,10 +3,10 @@ use crate::{ proofs::{EMPTY_LIST_HASH, EMPTY_ROOT}, BlockHash, BlockNumber, Bloom, Bytes, H160, H256, U256, }; -use bytes::{BufMut, BytesMut}; +use bytes::{Buf, BufMut, BytesMut}; use ethers_core::types::{Block, H256 as EthersH256, H64}; use reth_codecs::{add_arbitrary_tests, derive_arbitrary, main_codec, Compact}; -use reth_rlp::{length_of_length, Decodable, Encodable}; +use reth_rlp::{length_of_length, Decodable, Encodable, EMPTY_STRING_CODE}; use serde::{Deserialize, Serialize}; use std::ops::Deref; @@ -48,13 +48,14 @@ pub struct Header { /// executed and finalisations applied; formally Hr. pub state_root: H256, /// The Keccak 256-bit hash of the root node of the trie structure populated with each - /// transaction in the transactions list portion of the - /// block; formally Ht. + /// transaction in the transactions list portion of the block; formally Ht. pub transactions_root: H256, - /// The Keccak 256-bit hash of the root - /// node of the trie structure populated with the receipts of each transaction in the - /// transactions list portion of the block; formally He. + /// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts + /// of each transaction in the transactions list portion of the block; formally He. pub receipts_root: H256, + /// The Keccak 256-bit hash of the withdrawals list portion of this block. + /// + pub withdrawals_root: Option, /// The Bloom filter composed from indexable information (logger address and log topics) /// contained in each log entry from the receipt of each transaction in the transactions list; /// formally Hb. @@ -110,6 +111,7 @@ impl Default for Header { mix_hash: Default::default(), nonce: 0, base_fee_per_gas: None, + withdrawals_root: None, } } } @@ -125,7 +127,12 @@ impl Header { /// Checks if the header is empty - has no transactions and no ommers pub fn is_empty(&self) -> bool { - self.transaction_root_is_empty() && self.ommers_hash_is_empty() + let txs_and_ommers_empty = self.transaction_root_is_empty() && self.ommers_hash_is_empty(); + if let Some(withdrawals_root) = self.withdrawals_root { + txs_and_ommers_empty && withdrawals_root == EMPTY_ROOT + } else { + txs_and_ommers_empty + } } /// Check if the ommers hash equals to empty hash list. @@ -161,7 +168,16 @@ impl Header { length += self.extra_data.length(); length += self.mix_hash.length(); length += H64::from_low_u64_be(self.nonce).length(); - length += self.base_fee_per_gas.map(|fee| U256::from(fee).length()).unwrap_or_default(); + + if let Some(base_fee) = self.base_fee_per_gas { + length += U256::from(base_fee).length(); + } else if self.withdrawals_root.is_some() { + length += 1; // EMTY STRING CODE + } + if let Some(root) = self.withdrawals_root { + length += root.length(); + } + length } } @@ -186,8 +202,17 @@ impl Encodable for Header { self.extra_data.encode(out); self.mix_hash.encode(out); H64::from_low_u64_be(self.nonce).encode(out); + + // Encode base fee. Put empty string if base fee is missing, + // but withdrawals root is present. if let Some(ref base_fee) = self.base_fee_per_gas { U256::from(*base_fee).encode(out); + } else if self.withdrawals_root.is_some() { + out.put_u8(EMPTY_STRING_CODE); + } + + if let Some(ref root) = self.withdrawals_root { + root.encode(out); } } @@ -223,10 +248,17 @@ impl Decodable for Header { mix_hash: Decodable::decode(buf)?, nonce: H64::decode(buf)?.to_low_u64_be(), base_fee_per_gas: None, + withdrawals_root: None, }; - let consumed = started_len - buf.len(); - if consumed < rlp_head.payload_length { - this.base_fee_per_gas = Some(U256::decode(buf)?.to::()); + if started_len - buf.len() < rlp_head.payload_length { + if buf.first().map(|b| *b == EMPTY_STRING_CODE).unwrap_or_default() { + buf.advance(1) + } else { + this.base_fee_per_gas = Some(U256::decode(buf)?.to::()); + } + } + if started_len - buf.len() < rlp_head.payload_length { + this.withdrawals_root = Some(Decodable::decode(buf)?); } let consumed = started_len - buf.len(); if consumed != rlp_head.payload_length { @@ -493,6 +525,7 @@ mod tests { mix_hash: H256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), nonce: 0, base_fee_per_gas: Some(0x036b_u64), + withdrawals_root: None, }; assert_eq!(header.hash_slow(), expected_hash); } @@ -518,10 +551,56 @@ mod tests { assert_eq!(header, expected); // make sure the hash matches - let expected_hash = H256::from_slice( - &hex::decode("8c2f2af15b7b563b6ab1e09bed0e9caade7ed730aec98b70a993597a797579a9") - .unwrap(), - ); + let expected_hash = + H256::from_str("8c2f2af15b7b563b6ab1e09bed0e9caade7ed730aec98b70a993597a797579a9") + .unwrap(); + assert_eq!(header.hash_slow(), expected_hash); + } + + // Test vector from: https://github.com/ethereum/tests/blob/970503935aeb76f59adfa3b3224aabf25e77b83d/BlockchainTests/ValidBlocks/bcExample/shanghaiExample.json#L15-L34 + #[test] + fn test_decode_block_header_with_withdrawals() { + let data = hex::decode("f9021ca018db39e19931515b30b16b3a92c292398039e31d6c267111529c3f2ba0a26c17a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa095efce3d6972874ca8b531b233b7a1d1ff0a56f08b20c8f1b89bef1b001194a5a071e515dd89e8a7973402c2e11646081b4e2209b2d3a1550df5095289dabcb3fba0ed9c51ea52c968e552e370a77a41dac98606e98b915092fb5f949d6452fce1c4b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff830125b882079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a027f166f1d7c789251299535cb176ba34116e44894476a7886fe5d73d9be5c973").unwrap(); + let expected = Header { + parent_hash: H256::from_str( + "18db39e19931515b30b16b3a92c292398039e31d6c267111529c3f2ba0a26c17", + ) + .unwrap(), + beneficiary: Address::from_str("2adc25665018aa1fe0e6bc666dac8fc2697ff9ba").unwrap(), + state_root: H256::from_str( + "95efce3d6972874ca8b531b233b7a1d1ff0a56f08b20c8f1b89bef1b001194a5", + ) + .unwrap(), + transactions_root: H256::from_str( + "71e515dd89e8a7973402c2e11646081b4e2209b2d3a1550df5095289dabcb3fb", + ) + .unwrap(), + receipts_root: H256::from_str( + "ed9c51ea52c968e552e370a77a41dac98606e98b915092fb5f949d6452fce1c4", + ) + .unwrap(), + number: 0x01, + gas_limit: 0x7fffffffffffffff, + gas_used: 0x0125b8, + timestamp: 0x079e, + extra_data: Bytes::from_str("42").unwrap(), + mix_hash: H256::from_str( + "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + ) + .unwrap(), + base_fee_per_gas: Some(0x09), + withdrawals_root: Some( + H256::from_str("27f166f1d7c789251299535cb176ba34116e44894476a7886fe5d73d9be5c973") + .unwrap(), + ), + ..Default::default() + }; + let header =
::decode(&mut data.as_slice()).unwrap(); + assert_eq!(header, expected); + + let expected_hash = + H256::from_str("85fdec94c534fa0a1534720f167b899d1fc268925c71c0cbf5aaa213483f5a69") + .unwrap(); assert_eq!(header.hash_slow(), expected_hash); } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 032cb5f4d..1129790f3 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -29,6 +29,7 @@ mod peer; mod receipt; mod storage; mod transaction; +mod withdrawal; /// Helper function for calculating Merkle proofs and hashes pub mod proofs; @@ -61,6 +62,7 @@ pub use transaction::{ Transaction, TransactionKind, TransactionSigned, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxLegacy, TxType, }; +pub use withdrawal::Withdrawal; /// A block hash. pub type BlockHash = H256; diff --git a/crates/primitives/src/proofs.rs b/crates/primitives/src/proofs.rs index 268485782..bfba3e505 100644 --- a/crates/primitives/src/proofs.rs +++ b/crates/primitives/src/proofs.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use crate::{ - keccak256, Address, Bytes, GenesisAccount, Header, Log, Receipt, TransactionSigned, H256, + keccak256, Address, Bytes, GenesisAccount, Header, Log, Receipt, TransactionSigned, Withdrawal, + H256, }; use bytes::BytesMut; use hash_db::Hasher; @@ -47,6 +48,17 @@ pub fn calculate_transaction_root<'a>( })) } +/// Calculates the root hash of the withdrawals. +pub fn calculate_withdrawals_root<'a>( + withdrawals: impl IntoIterator, +) -> H256 { + ordered_trie_root::(withdrawals.into_iter().map(|withdrawal| { + let mut withdrawal_rlp = Vec::new(); + withdrawal.encode(&mut withdrawal_rlp); + withdrawal_rlp + })) +} + /// Calculates the receipt root for a header. pub fn calculate_receipt_root<'a>(receipts: impl Iterator) -> H256 { ordered_trie_root::(receipts.into_iter().map(|receipt| { @@ -96,7 +108,7 @@ mod tests { }; use reth_rlp::Decodable; - use super::EMPTY_ROOT; + use super::{calculate_withdrawals_root, EMPTY_ROOT}; #[test] fn check_transaction_root() { @@ -105,7 +117,7 @@ mod tests { let block: Block = Block::decode(block_rlp).unwrap(); let tx_root = calculate_transaction_root(block.body.iter()); - assert_eq!(block.transactions_root, tx_root, "Should be same"); + assert_eq!(block.transactions_root, tx_root, "Must be the same"); } #[test] @@ -127,6 +139,29 @@ mod tests { ); } + #[test] + fn check_withdrawals_root() { + // Single withdrawal, amount 0 + // https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/amountIs0.json + let data = &hex!("f90238f90219a0151934ad9b654c50197f37018ee5ee9bb922dec0a1b5e24a6d679cb111cdb107a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa0046119afb1ab36aaa8f66088677ed96cd62762f6d3e65642898e189fbe702d51a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8082079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a048a703da164234812273ea083e4ec3d09d028300cd325b46a6a75402e5a7ab95c0c0d9d8808094c94f5374fce5edbc8e2a8697c15331677e6ebf0b80"); + let block: Block = Block::decode(&mut data.as_slice()).unwrap(); + assert!(block.withdrawals.is_some()); + let withdrawals = block.withdrawals.as_ref().unwrap(); + assert_eq!(withdrawals.len(), 1); + let withdrawals_root = calculate_withdrawals_root(withdrawals.iter()); + assert_eq!(block.withdrawals_root, Some(withdrawals_root)); + + // 4 withdrawals, identical indices + // https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/twoIdenticalIndex.json + let data = &hex!("f9028cf90219a0151934ad9b654c50197f37018ee5ee9bb922dec0a1b5e24a6d679cb111cdb107a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa0ccf7b62d616c2ad7af862d67b9dcd2119a90cebbff8c3cd1e5d7fc99f8755774a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001887fffffffffffffff8082079e42a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b42188000000000000000009a0a95b9a7b58a6b3cb4001eb0be67951c5517141cb0183a255b5cae027a7b10b36c0c0f86cda808094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da028094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da018094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710da028094c94f5374fce5edbc8e2a8697c15331677e6ebf0b822710"); + let block: Block = Block::decode(&mut data.as_slice()).unwrap(); + assert!(block.withdrawals.is_some()); + let withdrawals = block.withdrawals.as_ref().unwrap(); + assert_eq!(withdrawals.len(), 4); + let withdrawals_root = calculate_withdrawals_root(withdrawals.iter()); + assert_eq!(block.withdrawals_root, Some(withdrawals_root)); + } + #[test] fn check_empty_state_root() { let genesis_alloc = HashMap::new(); diff --git a/crates/primitives/src/withdrawal.rs b/crates/primitives/src/withdrawal.rs new file mode 100644 index 000000000..ae49d3a38 --- /dev/null +++ b/crates/primitives/src/withdrawal.rs @@ -0,0 +1,24 @@ +use crate::{constants::GWEI_TO_WEI, Address, U256}; +use reth_codecs::{main_codec, Compact}; +use reth_rlp::{RlpDecodable, RlpEncodable}; + +/// Withdrawal represents a validator withdrawal from the consensus layer. +#[main_codec] +#[derive(Debug, Clone, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)] +pub struct Withdrawal { + /// Monotonically increasing identifier issued by consensus layer. + pub index: u64, + /// Index of validator associated with withdrawal. + pub validator_index: u64, + /// Target address for withdrawn ether. + pub address: Address, + /// Value of the withdrawal in gwei. + pub amount: u64, +} + +impl Withdrawal { + /// Return the withdrawal amount in wei. + pub fn amount_wei(&self) -> U256 { + U256::from(self.amount) * U256::from(GWEI_TO_WEI) + } +} diff --git a/crates/rpc/rpc-engine-api/src/engine_api.rs b/crates/rpc/rpc-engine-api/src/engine_api.rs index 1c1130b03..8aefd3a9c 100644 --- a/crates/rpc/rpc-engine-api/src/engine_api.rs +++ b/crates/rpc/rpc-engine-api/src/engine_api.rs @@ -85,12 +85,17 @@ impl EngineApi { .map(|tx| TransactionSigned::decode(&mut tx.as_ref())) .collect::, _>>()?; let transactions_root = proofs::calculate_transaction_root(transactions.iter()); + + let withdrawals_root = + payload.withdrawals.as_ref().map(|w| proofs::calculate_withdrawals_root(w.iter())); + let header = Header { parent_hash: payload.parent_hash, beneficiary: payload.fee_recipient, state_root: payload.state_root, transactions_root, receipts_root: payload.receipts_root, + withdrawals_root, logs_bloom: payload.logs_bloom, number: payload.block_number.as_u64(), gas_limit: payload.gas_limit.as_u64(), @@ -113,7 +118,12 @@ impl EngineApi { }) } - Ok(SealedBlock { header, body: transactions, ommers: Default::default() }) + Ok(SealedBlock { + header, + body: transactions, + withdrawals: payload.withdrawals, + ommers: Default::default(), + }) } /// Called to retrieve the latest state of the network, validate new blocks, and maintain @@ -333,6 +343,7 @@ mod tests { header: transformed.header.seal(), body: transformed.body, ommers: transformed.ommers.into_iter().map(Header::seal).collect(), + withdrawals: transformed.withdrawals, } } diff --git a/crates/rpc/rpc-types/src/eth/block.rs b/crates/rpc/rpc-types/src/eth/block.rs index 0013fa5e5..983f18db3 100644 --- a/crates/rpc/rpc-types/src/eth/block.rs +++ b/crates/rpc/rpc-types/src/eth/block.rs @@ -1,7 +1,8 @@ //! Contains types that represent ethereum types in [reth_primitives] when used in RPC use crate::Transaction; use reth_primitives::{ - Address, Block as PrimitiveBlock, Bloom, Bytes, Header as RethHeader, H256, H64, U256, + Address, Block as PrimitiveBlock, Bloom, Bytes, Header as RethHeader, Withdrawal, H256, H64, + U256, }; use reth_rlp::Encodable; use serde::{ser::Error, Deserialize, Serialize, Serializer}; @@ -43,6 +44,8 @@ pub struct Block { /// Base Fee for post-EIP1559 blocks. #[serde(skip_serializing_if = "Option::is_none")] pub base_fee_per_gas: Option, + /// Withdrawals + pub withdrawals: Option>, } impl Block { @@ -74,6 +77,7 @@ impl Block { nonce, base_fee_per_gas, extra_data, + withdrawals_root, } = block.header; let header = Header { @@ -85,6 +89,7 @@ impl Block { state_root, transactions_root, receipts_root, + withdrawals_root, number: Some(U256::from(number)), gas_used: U256::from(gas_used), gas_limit: U256::from(gas_limit), @@ -115,6 +120,7 @@ impl Block { base_fee_per_gas: base_fee_per_gas.map(U256::from), total_difficulty, size: Some(U256::from(block_length)), + withdrawals: block.withdrawals, }) } } @@ -140,6 +146,8 @@ pub struct Header { pub transactions_root: H256, /// Transactions receipts root hash pub receipts_root: H256, + /// Withdrawals root hash + pub withdrawals_root: Option, /// Block number pub number: Option, /// Gas Used diff --git a/crates/rpc/rpc-types/src/eth/engine.rs b/crates/rpc/rpc-types/src/eth/engine.rs index 32c73ddca..1215724f1 100644 --- a/crates/rpc/rpc-types/src/eth/engine.rs +++ b/crates/rpc/rpc-types/src/eth/engine.rs @@ -3,7 +3,7 @@ #![allow(missing_docs)] use bytes::BytesMut; -use reth_primitives::{Address, Bloom, Bytes, SealedBlock, H256, H64, U256, U64}; +use reth_primitives::{Address, Bloom, Bytes, SealedBlock, Withdrawal, H256, H64, U256, U64}; use reth_rlp::Encodable; use serde::{Deserialize, Serialize}; @@ -30,7 +30,7 @@ pub struct ExecutionPayload { /// Array of [`Withdrawal`] enabled with V2 /// See #[serde(default, skip_serializing_if = "Option::is_none")] - pub withdrawal: Option, + pub withdrawals: Option>, } impl From for ExecutionPayload { @@ -59,26 +59,11 @@ impl From for ExecutionPayload { base_fee_per_gas: U256::from(value.base_fee_per_gas.unwrap_or_default()), block_hash: value.hash(), transactions, - withdrawal: None, + withdrawals: value.withdrawals, } } } -/// This structure maps onto the validator withdrawal object from the beacon chain spec. -/// -/// See also: -#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Withdrawal { - pub index: U64, - pub validator_index: U64, - pub address: Address, - /// Note: the amount value is represented on the beacon chain as a little-endian value in units - /// of Gwei, whereas the amount in this structure MUST be converted to a big-endian value in - /// units of Wei. - pub amount: U256, -} - /// This structure encapsulates the fork choice state #[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -99,7 +84,7 @@ pub struct PayloadAttributes { /// Array of [`Withdrawal`] enabled with V2 /// See #[serde(default, skip_serializing_if = "Option::is_none")] - pub withdrawal: Option, // TODO: should be a vec + pub withdrawals: Option>, } /// This structure contains the result of processing a payload diff --git a/crates/stages/src/stages/bodies.rs b/crates/stages/src/stages/bodies.rs index 5d601d809..39a86cb39 100644 --- a/crates/stages/src/stages/bodies.rs +++ b/crates/stages/src/stages/bodies.rs @@ -6,7 +6,7 @@ use futures_util::TryStreamExt; use reth_db::{ cursor::{DbCursorRO, DbCursorRW}, database::Database, - models::{StoredBlockBody, StoredBlockOmmers}, + models::{StoredBlockBody, StoredBlockOmmers, StoredBlockWithdrawals}, tables, transaction::{DbTx, DbTxMut}, }; @@ -28,6 +28,7 @@ pub const BODIES: StageId = StageId("Bodies"); /// The body stage downloads block bodies for all block headers stored locally in the database. /// /// # Empty blocks + /// /// Blocks with an ommers hash corresponding to no ommers *and* a transaction root corresponding to /// no transactions will not have a block body downloaded for them, since it would be meaningless to @@ -88,8 +89,9 @@ impl Stage for BodyStage { // Cursors used to write bodies, ommers and transactions let mut body_cursor = tx.cursor_write::()?; - let mut ommers_cursor = tx.cursor_write::()?; let mut tx_cursor = tx.cursor_write::()?; + let mut ommers_cursor = tx.cursor_write::()?; + let mut withdrawals_cursor = tx.cursor_write::()?; // Cursors used to write state transition mapping let mut block_transition_cursor = tx.cursor_write::()?; @@ -112,6 +114,7 @@ impl Stage for BodyStage { let block_number = response.block_number(); let difficulty = response.difficulty(); + let mut has_withdrawals = false; match response { BlockResponse::Full(block) => { body_cursor.append( @@ -133,6 +136,7 @@ impl Stage for BodyStage { transition_id += 1; } + // Write ommers if any if !block.ommers.is_empty() { ommers_cursor.append( block_number, @@ -145,6 +149,15 @@ impl Stage for BodyStage { }, )?; } + + // Write withdrawals if any + if let Some(withdrawals) = block.withdrawals { + if !withdrawals.is_empty() { + has_withdrawals = true; + withdrawals_cursor + .append(block_number, StoredBlockWithdrawals { withdrawals })?; + } + } } BlockResponse::Empty(_) => { body_cursor.append( @@ -163,7 +176,8 @@ impl Stage for BodyStage { .ok_or(ProviderError::TotalDifficulty { number: block_number })? .1; let has_reward = self.consensus.has_block_reward(td.into(), difficulty); - if has_reward { + let has_post_block_transition = has_reward || has_withdrawals; + if has_post_block_transition { transition_id += 1; } block_transition_cursor.append(block_number, transition_id)?; @@ -188,8 +202,9 @@ impl Stage for BodyStage { info!(target: "sync::stages::bodies", to_block = input.unwind_to, "Unwinding"); // Cursors to unwind bodies, ommers let mut body_cursor = tx.cursor_write::()?; - let mut ommers_cursor = tx.cursor_write::()?; let mut transaction_cursor = tx.cursor_write::()?; + let mut ommers_cursor = tx.cursor_write::()?; + let mut withdrawals_cursor = tx.cursor_write::()?; // Cursors to unwind transitions let mut block_transition_cursor = tx.cursor_write::()?; let mut tx_transition_cursor = tx.cursor_write::()?; @@ -200,11 +215,16 @@ impl Stage for BodyStage { break } - // Delete the ommers value if any + // Delete the ommers entry if any if ommers_cursor.seek_exact(number)?.is_some() { ommers_cursor.delete_current()?; } + // Delete the withdrawals entry if any + if withdrawals_cursor.seek_exact(number)?.is_some() { + withdrawals_cursor.delete_current()?; + } + // Delete the block transition if any if block_transition_cursor.seek_exact(number)?.is_some() { block_transition_cursor.delete_current()?; @@ -453,6 +473,7 @@ mod tests { BlockBody { transactions: block.body.clone(), ommers: block.ommers.iter().cloned().map(|ommer| ommer.unseal()).collect(), + withdrawals: block.withdrawals.clone(), }, ) } @@ -772,6 +793,7 @@ mod tests { header, body: body.transactions, ommers: body.ommers.into_iter().map(|h| h.seal()).collect(), + withdrawals: body.withdrawals, })); } diff --git a/crates/stages/src/stages/execution.rs b/crates/stages/src/stages/execution.rs index aca8c6340..1c268cdde 100644 --- a/crates/stages/src/stages/execution.rs +++ b/crates/stages/src/stages/execution.rs @@ -5,7 +5,7 @@ use crate::{ use reth_db::{ cursor::{DbCursorRO, DbCursorRW, DbDupCursorRO}, database::Database, - models::{StoredBlockBody, TransitionIdAddress}, + models::TransitionIdAddress, tables, transaction::{DbTx, DbTxMut}, }; @@ -14,9 +14,7 @@ use reth_executor::{ revm_wrap::{State, SubState}, }; use reth_interfaces::provider::Error as ProviderError; -use reth_primitives::{ - Address, Block, ChainSpec, Hardfork, Header, StorageEntry, H256, MAINNET, U256, -}; +use reth_primitives::{Address, Block, ChainSpec, Hardfork, StorageEntry, H256, MAINNET, U256}; use reth_provider::{LatestStateProviderRef, Transaction}; use std::fmt::Debug; use tracing::*; @@ -85,6 +83,8 @@ impl ExecutionStage { let mut bodies_cursor = tx.cursor_read::()?; // Get ommers with canonical hashes. let mut ommers_cursor = tx.cursor_read::()?; + // Get block withdrawals. + let mut withdrawals_cursor = tx.cursor_read::()?; // Get transaction of the block that we are executing. let mut tx_cursor = tx.cursor_read::()?; // Skip sender recovery and load signer from database. @@ -93,7 +93,7 @@ impl ExecutionStage { // Get block headers and bodies let block_batch = headers_cursor .walk_range(start_block..=end_block)? - .map(|entry| -> Result<(Header, U256, StoredBlockBody, Vec
), StageError> { + .map(|entry| -> Result<_, StageError> { let (number, header) = entry?; let (_, td) = td_cursor .seek_exact(number)? @@ -101,7 +101,9 @@ impl ExecutionStage { let (_, body) = bodies_cursor.seek_exact(number)?.ok_or(ProviderError::BlockBody { number })?; let (_, stored_ommers) = ommers_cursor.seek_exact(number)?.unwrap_or_default(); - Ok((header, td.into(), body, stored_ommers.ommers)) + let withdrawals = + withdrawals_cursor.seek_exact(number)?.map(|(_, w)| w.withdrawals); + Ok((header, td.into(), body, stored_ommers.ommers, withdrawals)) }) .collect::, _>>()?; @@ -110,7 +112,7 @@ impl ExecutionStage { // Fetch transactions, execute them and generate results let mut block_change_patches = Vec::with_capacity(block_batch.len()); - for (header, td, body, ommers) in block_batch.into_iter() { + for (header, td, body, ommers, withdrawals) in block_batch.into_iter() { let block_number = header.number; tracing::trace!(target: "sync::stages::execution", ?block_number, "Execute block."); @@ -144,7 +146,7 @@ impl ExecutionStage { trace!(target: "sync::stages::execution", number = block_number, txs = transactions.len(), "Executing block"); let changeset = reth_executor::executor::execute_and_verify_receipt( - &Block { header, body: transactions, ommers }, + &Block { header, body: transactions, ommers, withdrawals }, td, Some(signers), &self.chain_spec, @@ -163,7 +165,7 @@ impl ExecutionStage { let spurious_dragon_active = self.chain_spec.fork(Hardfork::SpuriousDragon).active_at_block(block_number); // insert state change set - for result in results.changesets.into_iter() { + for result in results.tx_changesets.into_iter() { for (address, account_change_set) in result.changeset.into_iter() { let AccountChangeSet { account, wipe_storage, storage } = account_change_set; // apply account change to db. Updates AccountChangeSet and PlainAccountState @@ -247,20 +249,17 @@ impl ExecutionStage { current_transition_id += 1; } - // If there is block reward we will add account changeset to db - if let Some(block_reward_changeset) = results.block_reward { - // we are sure that block reward index is present. - for (address, changeset) in block_reward_changeset.into_iter() { - trace!(target: "sync::stages::execution", ?address, current_transition_id, "Applying block reward"); - changeset.apply_to_db( - &**tx, - address, - current_transition_id, - spurious_dragon_active, - )?; - } - current_transition_id += 1; + // If there are any post block changes, we will add account changesets to db. + for (address, changeset) in results.block_changesets.into_iter() { + trace!(target: "sync::stages::execution", ?address, current_transition_id, "Applying block reward"); + changeset.apply_to_db( + &**tx, + address, + current_transition_id, + spurious_dragon_active, + )?; } + current_transition_id += 1; } let done = !capped; diff --git a/crates/stages/src/stages/merkle.rs b/crates/stages/src/stages/merkle.rs index f13f91535..07751e204 100644 --- a/crates/stages/src/stages/merkle.rs +++ b/crates/stages/src/stages/merkle.rs @@ -278,11 +278,11 @@ mod tests { let n_accounts = 31; let mut accounts = random_contract_account_range(&mut (0..n_accounts)); - let SealedBlock { header, body, ommers } = + let SealedBlock { header, body, ommers, withdrawals } = random_block(stage_progress, None, Some(0), None); let mut header = header.unseal(); header.state_root = self.generate_initial_trie(&accounts)?; - let sealed_head = SealedBlock { header: header.seal(), body, ommers }; + let sealed_head = SealedBlock { header: header.seal(), body, ommers, withdrawals }; let head_hash = sealed_head.hash(); let mut blocks = vec![sealed_head]; diff --git a/crates/storage/db/src/tables/codecs/compact.rs b/crates/storage/db/src/tables/codecs/compact.rs index 1c9471903..a43fe7d76 100644 --- a/crates/storage/db/src/tables/codecs/compact.rs +++ b/crates/storage/db/src/tables/codecs/compact.rs @@ -42,7 +42,8 @@ impl_compression_for_compact!( StorageEntry, StorageTrieEntry, StoredBlockBody, - StoredBlockOmmers + StoredBlockOmmers, + StoredBlockWithdrawals ); impl_compression_for_compact!(AccountBeforeTx, TransactionSigned); impl_compression_for_compact!(CompactU256); diff --git a/crates/storage/db/src/tables/mod.rs b/crates/storage/db/src/tables/mod.rs index 4b4e3a04b..a88e48a21 100644 --- a/crates/storage/db/src/tables/mod.rs +++ b/crates/storage/db/src/tables/mod.rs @@ -12,7 +12,8 @@ use crate::{ models::{ accounts::{AccountBeforeTx, TransitionIdAddress}, blocks::{HeaderHash, StoredBlockOmmers}, - ShardedKey, + storage_sharded_key::StorageShardedKey, + ShardedKey, StoredBlockBody, StoredBlockWithdrawals, }, }, }; @@ -21,8 +22,6 @@ use reth_primitives::{ StorageTrieEntry, TransactionSigned, TransitionId, TxHash, TxNumber, H256, }; -use self::models::{storage_sharded_key::StorageShardedKey, StoredBlockBody}; - /// Enum for the types of tables present in libmdbx. #[derive(Debug)] pub enum TableType { @@ -33,13 +32,14 @@ pub enum TableType { } /// Default tables that should be present inside database. -pub const TABLES: [(TableType, &str); 26] = [ +pub const TABLES: [(TableType, &str); 27] = [ (TableType::Table, CanonicalHeaders::const_name()), (TableType::Table, HeaderTD::const_name()), (TableType::Table, HeaderNumbers::const_name()), (TableType::Table, Headers::const_name()), (TableType::Table, BlockBodies::const_name()), (TableType::Table, BlockOmmers::const_name()), + (TableType::Table, BlockWithdrawals::const_name()), (TableType::Table, Transactions::const_name()), (TableType::Table, TxHashNumber::const_name()), (TableType::Table, Receipts::const_name()), @@ -141,6 +141,11 @@ table!( ( BlockOmmers ) BlockNumber | StoredBlockOmmers ); +table!( + /// Stores the block withdrawals. + ( BlockWithdrawals ) BlockNumber | StoredBlockWithdrawals +); + table!( /// (Canonical only) Stores the transaction body for canonical transactions. ( Transactions ) TxNumber | TransactionSigned diff --git a/crates/storage/db/src/tables/models/blocks.rs b/crates/storage/db/src/tables/models/blocks.rs index a9f60ca22..48213662c 100644 --- a/crates/storage/db/src/tables/models/blocks.rs +++ b/crates/storage/db/src/tables/models/blocks.rs @@ -9,7 +9,7 @@ use crate::{ }; use bytes::Bytes; use reth_codecs::{main_codec, Compact}; -use reth_primitives::{BlockHash, BlockNumber, Header, TxNumber, H256}; +use reth_primitives::{BlockHash, BlockNumber, Header, TxNumber, Withdrawal, H256}; use serde::{Deserialize, Serialize}; /// Total number of transactions. @@ -51,13 +51,21 @@ impl StoredBlockBody { /// /// It is stored as the headers of the block's uncles. /// tx_amount)`. -#[derive(Debug, Default, Eq, PartialEq, Clone)] #[main_codec] +#[derive(Debug, Default, Eq, PartialEq, Clone)] pub struct StoredBlockOmmers { /// The block headers of this block's uncles. pub ommers: Vec
, } +/// The storage representation of block withdrawals. +#[main_codec] +#[derive(Debug, Default, Eq, PartialEq, Clone)] +pub struct StoredBlockWithdrawals { + /// The block withdrawals. + pub withdrawals: Vec, +} + /// Hash of the block header. Value for [`CanonicalHeaders`][crate::tables::CanonicalHeaders] pub type HeaderHash = H256; diff --git a/crates/storage/provider/src/utils.rs b/crates/storage/provider/src/utils.rs index fd8ca4108..fff1c6f5b 100644 --- a/crates/storage/provider/src/utils.rs +++ b/crates/storage/provider/src/utils.rs @@ -1,5 +1,5 @@ use reth_db::{ - models::{StoredBlockBody, StoredBlockOmmers}, + models::{StoredBlockBody, StoredBlockOmmers, StoredBlockWithdrawals}, tables, transaction::{DbTx, DbTxMut}, }; @@ -72,7 +72,18 @@ pub fn insert_block<'a, TX: DbTxMut<'a> + DbTx<'a>>( current_tx_id += 1; } - if has_block_reward { + let mut has_withdrawals = false; + if let Some(withdrawals) = block.withdrawals.clone() { + if !withdrawals.is_empty() { + has_withdrawals = true; + tx.put::( + block.number, + StoredBlockWithdrawals { withdrawals }, + )?; + } + } + + if has_block_reward || has_withdrawals { transition_id += 1; } tx.put::(block.number, transition_id)?;