From 50db8297dae2ea330e5a8bade98fc380b37e24e1 Mon Sep 17 00:00:00 2001 From: Federico Gimenez Date: Tue, 17 Feb 2026 10:45:12 +0000 Subject: [PATCH 1/4] feat(evm): split gas tracking into execution vs storage creation gas (TIP-1016) --- crates/evm/src/block.rs | 200 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 2 deletions(-) diff --git a/crates/evm/src/block.rs b/crates/evm/src/block.rs index 75d2b611c3..18e2154a33 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 full cumulative gas used (execution + storage creation) for receipts. + /// This differs from block gas limit accounting which only counts execution gas. + cumulative_full_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_full_gas_used: 0, + cumulative_storage_creation_gas: 0, } } @@ -402,7 +411,17 @@ 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)?; + // TODO(TIP-1016): extract storage creation gas from EVM context once + // the EVM-level tracking is implemented. + let storage_creation_gas: u64 = 0; + + let full_gas_used = self.inner.commit_transaction(inner)?; + self.cumulative_full_gas_used += full_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 = full_gas_used - storage_creation_gas; // TODO: remove once revm supports emitting logs for reverted transactions // @@ -456,7 +475,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 +487,12 @@ where BlockValidationError::msg("end-of-block system transactions not seen").into(), ); } + + // The inner executor's gas_used tracks full 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 +539,16 @@ where pub(crate) fn section(&self) -> BlockSection { self.section } + + /// Get the cumulative full gas used (execution + storage) for assertions. + pub(crate) fn cumulative_full_gas_used(&self) -> u64 { + self.cumulative_full_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 +1146,167 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_commit_transaction_tracks_full_cumulative_gas() { + // commit_transaction should track cumulative full 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_full_gas_used(), 21000); + } + + #[test] + fn test_cumulative_full_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_full_gas_used(), 71000); + + // Receipts should have cumulative full 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 full 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_full_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 full 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(); From 9e64dcefca45b14e398e021872d6a543ef11660c Mon Sep 17 00:00:00 2001 From: Federico Gimenez Date: Tue, 17 Feb 2026 18:15:12 +0000 Subject: [PATCH 2/4] full_gas -> total_gas --- crates/evm/src/block.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/evm/src/block.rs b/crates/evm/src/block.rs index 18e2154a33..2a6ec4b581 100644 --- a/crates/evm/src/block.rs +++ b/crates/evm/src/block.rs @@ -115,9 +115,9 @@ pub(crate) struct TempoBlockExecutor<'a, DB: Database, I> { non_payment_gas_left: u64, incentive_gas_used: u64, - /// Tracks full cumulative gas used (execution + storage creation) for receipts. + /// Tracks total cumulative gas used (execution + storage creation) for receipts. /// This differs from block gas limit accounting which only counts execution gas. - cumulative_full_gas_used: u64, + 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, @@ -148,7 +148,7 @@ where section: BlockSection::StartOfBlock, seen_subblocks: Vec::new(), subblock_fee_recipients: ctx.subblock_fee_recipients, - cumulative_full_gas_used: 0, + cumulative_total_gas_used: 0, cumulative_storage_creation_gas: 0, } } @@ -415,13 +415,13 @@ where // the EVM-level tracking is implemented. let storage_creation_gas: u64 = 0; - let full_gas_used = self.inner.commit_transaction(inner)?; - self.cumulative_full_gas_used += full_gas_used; + 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 = full_gas_used - storage_creation_gas; + let gas_used = total_gas_used - storage_creation_gas; // TODO: remove once revm supports emitting logs for reverted transactions // @@ -488,7 +488,7 @@ where ); } - // The inner executor's gas_used tracks full gas (execution + storage) + // 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; @@ -540,9 +540,9 @@ where self.section } - /// Get the cumulative full gas used (execution + storage) for assertions. - pub(crate) fn cumulative_full_gas_used(&self) -> u64 { - self.cumulative_full_gas_used + /// 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. @@ -1147,8 +1147,8 @@ mod tests { } #[test] - fn test_commit_transaction_tracks_full_cumulative_gas() { - // commit_transaction should track cumulative full gas (for receipts) + 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() @@ -1180,11 +1180,11 @@ mod tests { // With zero storage creation gas, execution gas equals total gas assert_eq!(exec_gas, 21000); - assert_eq!(executor.cumulative_full_gas_used(), 21000); + assert_eq!(executor.cumulative_total_gas_used(), 21000); } #[test] - fn test_cumulative_full_gas_accumulates_across_transactions() { + 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() @@ -1234,9 +1234,9 @@ mod tests { }; executor.commit_transaction(output2).unwrap(); - assert_eq!(executor.cumulative_full_gas_used(), 71000); + assert_eq!(executor.cumulative_total_gas_used(), 71000); - // Receipts should have cumulative full gas + // 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); @@ -1245,7 +1245,7 @@ mod tests { #[test] fn test_finish_returns_execution_gas_for_block_header() { // BlockExecutionResult.gas_used (used for block header) should be - // execution gas only, not full gas including storage creation. + // 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(); @@ -1263,7 +1263,7 @@ mod tests { seen_subblocks_signatures: true, }; executor.inner.gas_used += 21000; - executor.cumulative_full_gas_used += 21000; + executor.cumulative_total_gas_used += 21000; let (_, result) = executor.finish().unwrap(); // Block header gas_used should be execution gas @@ -1273,7 +1273,7 @@ mod tests { #[test] fn test_non_shared_gas_uses_execution_gas_only() { // non_shared_gas_left should be decremented by execution gas, - // which currently equals full gas since storage_creation_gas is 0. + // 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() From 5f00eb434d6e46af0acf990dd03a10cd308243a6 Mon Sep 17 00:00:00 2001 From: Federico Gimenez Date: Wed, 18 Feb 2026 20:33:28 +0000 Subject: [PATCH 3/4] extract storage creation gas tracked by the evm --- crates/evm/src/block.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/evm/src/block.rs b/crates/evm/src/block.rs index 2a6ec4b581..031c86c047 100644 --- a/crates/evm/src/block.rs +++ b/crates/evm/src/block.rs @@ -411,9 +411,8 @@ where let TempoTxResult { inner, tx } = output; let next_section = self.validate_tx(&tx, inner.result.result.gas_used())?; - // TODO(TIP-1016): extract storage creation gas from EVM context once - // the EVM-level tracking is implemented. - let storage_creation_gas: u64 = 0; + // 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; From 33151aeb245ce223325cb6dc6d3faef0fbd5b084 Mon Sep 17 00:00:00 2001 From: Federico Gimenez Date: Thu, 19 Feb 2026 15:22:31 +0000 Subject: [PATCH 4/4] fix(consensus): allow header gas_used <= receipt cumulative_gas_used for TIP-1016 storage creation gas exemption --- crates/consensus/src/lib.rs | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) 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, ) }