diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index 2a21fd812d..0910b7224d 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -3,7 +3,7 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] -use alloy_consensus::{BlockHeader, Transaction, transaction::TxHashRef}; +use alloy_consensus::{BlockHeader, Transaction, TxReceipt, transaction::TxHashRef}; use alloy_evm::block::BlockExecutionResult; use reth_chainspec::EthChainSpec; use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator}; @@ -12,7 +12,7 @@ use reth_consensus_common::validation::{ validate_against_parent_gas_limit, validate_against_parent_hash_number, }; use reth_ethereum_consensus::EthBeaconConsensus; -use reth_primitives_traits::{RecoveredBlock, SealedBlock, SealedHeader}; +use reth_primitives_traits::{GotExpected, RecoveredBlock, SealedBlock, SealedHeader}; use std::sync::Arc; use tempo_chainspec::{ hardfork::TempoHardforks, @@ -190,10 +190,39 @@ impl FullConsensus for TempoConsensus { result: &BlockExecutionResult, receipt_root_bloom: Option, ) -> Result<(), ConsensusError> { + // TIP-1016: block header gas_used tracks execution gas only, while receipt + // cumulative_gas_used tracks total gas (execution + storage creation). The + // standard Ethereum check requires strict equality, but TIP-1016 allows + // header gas_used <= last receipt cumulative_gas_used. + let cumulative_gas_used = result + .receipts + .last() + .map(|r| r.cumulative_gas_used()) + .unwrap_or(0); + if block.header().gas_used() > cumulative_gas_used { + return Err(ConsensusError::BlockGasUsed { + gas: GotExpected { + got: cumulative_gas_used, + expected: block.header().gas_used(), + }, + gas_spent_by_tx: reth_primitives_traits::receipt::gas_spent_by_transactions( + &result.receipts, + ), + }); + } + + // Delegate receipt root, logs bloom, and requests hash validation to the + // inner Ethereum consensus. We construct a temporary result with gas_used + // matching the header so the inner gas check passes, while the actual + // TIP-1016 gas invariant (header <= receipts) is checked above. + let mut patched_result = result.clone(); + if let Some(last) = patched_result.receipts.last_mut() { + last.cumulative_gas_used = block.header().gas_used(); + } FullConsensus::::validate_block_post_execution( &self.inner, block, - result, + &patched_result, receipt_root_bloom, ) } diff --git a/crates/evm/src/block.rs b/crates/evm/src/block.rs index 75d2b611c3..031c86c047 100644 --- a/crates/evm/src/block.rs +++ b/crates/evm/src/block.rs @@ -114,6 +114,13 @@ pub(crate) struct TempoBlockExecutor<'a, DB: Database, I> { non_shared_gas_left: u64, non_payment_gas_left: u64, incentive_gas_used: u64, + + /// Tracks total cumulative gas used (execution + storage creation) for receipts. + /// This differs from block gas limit accounting which only counts execution gas. + cumulative_total_gas_used: u64, + /// Tracks cumulative storage creation gas used (TIP-1016). + /// Used to derive execution-only gas for the block header. + cumulative_storage_creation_gas: u64, } impl<'a, DB, I> TempoBlockExecutor<'a, DB, I> @@ -141,6 +148,8 @@ where section: BlockSection::StartOfBlock, seen_subblocks: Vec::new(), subblock_fee_recipients: ctx.subblock_fee_recipients, + cumulative_total_gas_used: 0, + cumulative_storage_creation_gas: 0, } } @@ -402,7 +411,16 @@ where let TempoTxResult { inner, tx } = output; let next_section = self.validate_tx(&tx, inner.result.result.gas_used())?; - let gas_used = self.inner.commit_transaction(inner)?; + // Extract storage creation gas tracked by the EVM (TIP-1016). + let storage_creation_gas = inner.result.result.gas().state_gas_spent(); + + let total_gas_used = self.inner.commit_transaction(inner)?; + self.cumulative_total_gas_used += total_gas_used; + self.cumulative_storage_creation_gas += storage_creation_gas; + + // Execution gas excludes storage creation gas (TIP-1016). + // Only execution gas counts toward protocol limits (block gas limit). + let gas_used = total_gas_used - storage_creation_gas; // TODO: remove once revm supports emitting logs for reverted transactions // @@ -456,7 +474,7 @@ where } fn finish( - self, + mut self, ) -> Result<(Self::Evm, BlockExecutionResult), BlockExecutionError> { // Check that we ended in the System section with all end-of-block system txs seen if self.section @@ -468,6 +486,12 @@ where BlockValidationError::msg("end-of-block system transactions not seen").into(), ); } + + // The inner executor's gas_used tracks total gas (execution + storage) + // because that's what goes into receipt cumulative_gas_used. For the + // block header, we need execution gas only (TIP-1016). + self.inner.gas_used -= self.cumulative_storage_creation_gas; + self.inner.finish() } @@ -514,6 +538,16 @@ where pub(crate) fn section(&self) -> BlockSection { self.section } + + /// Get the cumulative total gas used (execution + storage) for assertions. + pub(crate) fn cumulative_total_gas_used(&self) -> u64 { + self.cumulative_total_gas_used + } + + /// Get the non-shared gas left for assertions. + pub(crate) fn non_shared_gas_left(&self) -> u64 { + self.non_shared_gas_left + } } #[cfg(test)] @@ -1111,6 +1145,167 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_commit_transaction_tracks_total_cumulative_gas() { + // commit_transaction should track cumulative total gas (for receipts) + let chainspec = test_chainspec(); + let mut db = State::builder().with_bundle_update().build(); + let mut executor = TestExecutorBuilder::default() + .with_general_gas_limit(30_000_000) + .with_parent_beacon_block_root(B256::ZERO) + .build(&mut db, &chainspec); + + executor.apply_pre_execution_changes().unwrap(); + + let tx = create_legacy_tx(); + let output = TempoTxResult { + inner: EthTxResult { + result: ResultAndState { + result: revm::context::result::ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Return, + gas: ResultGas::new(21000, 21000, 0, 0, 0, 0), + logs: vec![], + output: revm::context::result::Output::Call(Bytes::new()), + }, + state: Default::default(), + }, + blob_gas_used: 0, + tx_type: tx.tx_type(), + }, + tx, + }; + + let exec_gas = executor.commit_transaction(output).unwrap(); + + // With zero storage creation gas, execution gas equals total gas + assert_eq!(exec_gas, 21000); + assert_eq!(executor.cumulative_total_gas_used(), 21000); + } + + #[test] + fn test_cumulative_total_gas_accumulates_across_transactions() { + let chainspec = test_chainspec(); + let mut db = State::builder().with_bundle_update().build(); + let mut executor = TestExecutorBuilder::default() + .with_general_gas_limit(30_000_000) + .with_parent_beacon_block_root(B256::ZERO) + .build(&mut db, &chainspec); + + executor.apply_pre_execution_changes().unwrap(); + + // Commit first transaction (21000 gas) + let tx1 = create_legacy_tx(); + let output1 = TempoTxResult { + inner: EthTxResult { + result: ResultAndState { + result: revm::context::result::ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Return, + gas: ResultGas::new(21000, 21000, 0, 0, 0, 0), + logs: vec![], + output: revm::context::result::Output::Call(Bytes::new()), + }, + state: Default::default(), + }, + blob_gas_used: 0, + tx_type: tx1.tx_type(), + }, + tx: tx1, + }; + executor.commit_transaction(output1).unwrap(); + + // Commit second transaction (50000 gas) + let tx2 = create_legacy_tx(); + let output2 = TempoTxResult { + inner: EthTxResult { + result: ResultAndState { + result: revm::context::result::ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Return, + gas: ResultGas::new(50000, 50000, 0, 0, 0, 0), + logs: vec![], + output: revm::context::result::Output::Call(Bytes::new()), + }, + state: Default::default(), + }, + blob_gas_used: 0, + tx_type: tx2.tx_type(), + }, + tx: tx2, + }; + executor.commit_transaction(output2).unwrap(); + + assert_eq!(executor.cumulative_total_gas_used(), 71000); + + // Receipts should have cumulative total gas + let receipts = executor.receipts(); + assert_eq!(receipts[0].cumulative_gas_used, 21000); + assert_eq!(receipts[1].cumulative_gas_used, 71000); + } + + #[test] + fn test_finish_returns_execution_gas_for_block_header() { + // BlockExecutionResult.gas_used (used for block header) should be + // execution gas only, not total gas including storage creation. + // For now these are equal, but the plumbing ensures correctness + // when the EVM starts reporting storage gas separately. + let chainspec = test_chainspec(); + let mut db = State::builder().with_bundle_update().build(); + let mut executor = TestExecutorBuilder::default() + .with_general_gas_limit(30_000_000) + .with_parent_beacon_block_root(B256::ZERO) + .with_section(BlockSection::NonShared) + .build(&mut db, &chainspec); + + executor.apply_pre_execution_changes().unwrap(); + + // Manually set state to simulate a committed transaction + executor.section = BlockSection::System { + seen_subblocks_signatures: true, + }; + executor.inner.gas_used += 21000; + executor.cumulative_total_gas_used += 21000; + + let (_, result) = executor.finish().unwrap(); + // Block header gas_used should be execution gas + assert_eq!(result.gas_used, 21000); + } + + #[test] + fn test_non_shared_gas_uses_execution_gas_only() { + // non_shared_gas_left should be decremented by execution gas, + // which currently equals total gas since storage_creation_gas is 0. + let chainspec = test_chainspec(); + let mut db = State::builder().with_bundle_update().build(); + let mut executor = TestExecutorBuilder::default() + .with_general_gas_limit(30_000_000) + .with_parent_beacon_block_root(B256::ZERO) + .build(&mut db, &chainspec); + + executor.apply_pre_execution_changes().unwrap(); + + let initial_non_shared = executor.non_shared_gas_left(); + + let tx = create_legacy_tx(); + let output = TempoTxResult { + inner: EthTxResult { + result: ResultAndState { + result: revm::context::result::ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Return, + gas: ResultGas::new(50_000, 50_000, 0, 0, 0, 0), + logs: vec![], + output: revm::context::result::Output::Call(Bytes::new()), + }, + state: Default::default(), + }, + blob_gas_used: 0, + tx_type: tx.tx_type(), + }, + tx, + }; + executor.commit_transaction(output).unwrap(); + + assert_eq!(executor.non_shared_gas_left(), initial_non_shared - 50_000); + } + #[test] fn test_finish_system_tx_not_seen() { let chainspec = test_chainspec();