Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions core/reverted_tx_gas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package core

import (
"github.com/ethereum/go-ethereum/common"
)

// RevertedTxGasUsed maps specific transaction hashes that have been previously reverter to the amount
// of GAS used by that specific transaction alone.
var RevertedTxGasUsed = map[common.Hash]uint64{
common.HexToHash("0x58df300a7f04fe31d41d24672786cbe1c58b4f3d8329d0d74392d814dd9f7e40"): 45606,
}
81 changes: 57 additions & 24 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -681,34 +681,43 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
ret []byte
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
)
if contractCreation {
deployedContract = &common.Address{}
ret, *deployedContract, st.gasRemaining, multiGas, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value)
usedMultiGas = usedMultiGas.SaturatingAdd(multiGas)
} else {
// Increment the nonce for the next transaction.
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)

// Apply EIP-7702 authorizations.
if msg.SetCodeAuthorizations != nil {
for _, auth := range msg.SetCodeAuthorizations {
// Note errors are ignored, we simply skip invalid authorizations here.
st.applyAuthorization(&auth)
// Check against hardcoded transaction hashes that have previously reverted, so instead
// of executing the transaction we just update state nonce and remaining gas to avoid
// state divergence.
usedMultiGas, vmerr = st.handleRevertedTx(msg, usedMultiGas)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this extra level of indirection (function call) okay in this case? Or should we just inline it all? I'm worried about performance, given for every tx execution we introduced 3 checks:

// 1. check if mg.Tx is not nil
if msg.Tx == nil {
// 2. check if tx is inside RevertedTxGasUsed map
if l2GasUsed, ok := RevertedTxGasUsed[txHash]; ok {
// 3. make sure we continue on critical path if vmerr still nil
if vmerr == nil {

Copy link
Author

@bragaigor bragaigor Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I found a way to reduce those checks to just one for the critical path. Instead of doing the above, we make 2 changes:

  • Instead of having a map of TxHash -> L2GasUsed we would have a map BlockNumber -> (TxHash, L2GasUsed)
  • Instead of doing the check before if contractCreation { we guard st.evm.Call only

That's only possible because we can get the block number from st.evm.Context.BlockNumber without a nil check. The problem with this approach is that not every transaction inside the target block number are considered reverted transactions, so we need to call st.evm.Call for those other transactions; and I think that's okay since that would just happen for the entries found inside RevertedTxGasUsed. On the other hand, we introduce just one map lookup to all other transactions from the other blocks instead of adding a function call and a couple if checks.

The resulting code would look something like the following:

  • map gets updated to:
var RevertedTxGasUsed = map[uint64]RevertedTxEntry{
	204060366: {
		TxHash:    common.HexToHash("0x58df300a7f04fe31d41d24672786cbe1c58b4f3d8329d0d74392d814dd9f7e40"),
		L2GasUsed: 45606,
	},
}
  • and the only logic in state_transition that gets updated would be around st.evm.Call:
if revTxEntry, ok := RevertedTxGasUsed[st.evm.Context.BlockNumber.Uint64()]; ok {
    ret, st.gasRemaining, multiGas, vmerr = st.handleRevertedTx(revTxEntry, msg, usedMultiGas, value)
} else {
    ret, st.gasRemaining, multiGas, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value)
}

where handleRevertedTx would look like:

func (st *stateTransition) handleRevertedTx(revTxEntry RevertedTxEntry, msg *Message, multiGas multigas.MultiGas, value *uint256.Int) (ret []byte, leftOverGas uint64, retUsedMultiGas multigas.MultiGas, err error) {
	if msg.Tx == nil {
		return []byte{}, st.gasRemaining, multiGas, nil
	}

	txHash := msg.Tx.Hash()
	if revTxEntry.TxHash == txHash {
		adjustedGas := revTxEntry.L2GasUsed - params.TxGas
		gasRemaining := st.gasRemaining - adjustedGas

		return []byte{}, gasRemaining, multiGas, vm.ErrExecutionReverted
	} else {
		// A block might contain more than one transaction, therefore we need to make sure we execute such
		// transactions in case they're not considered reverted.
		return st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value)
	}
}

let me know if you prefer this new approach or the original one @tsahee


// vmerr is only not nil when we find a previous reverted transaction
if vmerr == nil {
if contractCreation {
deployedContract = &common.Address{}
ret, *deployedContract, st.gasRemaining, multiGas, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value)
usedMultiGas = usedMultiGas.SaturatingAdd(multiGas)
} else {
// Increment the nonce for the next transaction.
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)

// Apply EIP-7702 authorizations.
if msg.SetCodeAuthorizations != nil {
for _, auth := range msg.SetCodeAuthorizations {
// Note errors are ignored, we simply skip invalid authorizations here.
st.applyAuthorization(&auth)
}
}
}

// Perform convenience warming of sender's delegation target. Although the
// sender is already warmed in Prepare(..), it's possible a delegation to
// the account was deployed during this transaction. To handle correctly,
// simply wait until the final state of delegations is determined before
// performing the resolution and warming.
if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok {
st.state.AddAddressToAccessList(addr)
}
// Perform convenience warming of sender's delegation target. Although the
// sender is already warmed in Prepare(..), it's possible a delegation to
// the account was deployed during this transaction. To handle correctly,
// simply wait until the final state of delegations is determined before
// performing the resolution and warming.
if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok {
st.state.AddAddressToAccessList(addr)
}

// Execute the transaction's call.
ret, st.gasRemaining, multiGas, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value)
usedMultiGas = usedMultiGas.SaturatingAdd(multiGas)
// Execute the transaction's call.
ret, st.gasRemaining, multiGas, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value)
usedMultiGas = usedMultiGas.SaturatingAdd(multiGas)
}
}

// Refund the gas that was held to limit the amount of computation done.
Expand Down Expand Up @@ -787,6 +796,30 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}, nil
}

// handleRevertedTx attempts to process a reverted transaction. It returns
// ErrExecutionReverted with the updated multiGas if a matching reverted
// tx is found; otherwise, it returns nil error with unchangedmultiGas
func (st *stateTransition) handleRevertedTx(msg *Message, usedMultiGas multigas.MultiGas) (multigas.MultiGas, error) {
if msg.Tx == nil {
return usedMultiGas, nil
}

txHash := msg.Tx.Hash()
if l2GasUsed, ok := RevertedTxGasUsed[txHash]; ok {
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)

// Calculate adjusted gas since l2GasUsed contains params.TxGas
adjustedGas := l2GasUsed - params.TxGas
st.gasRemaining -= adjustedGas

// Update multigas and return ErrExecutionReverted error
usedMultiGas = usedMultiGas.SaturatingAdd(multigas.ComputationGas(adjustedGas))
return usedMultiGas, vm.ErrExecutionReverted
}

return usedMultiGas, nil
}

// validateAuthorization validates an EIP-7702 authorization against the state.
func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) {
// Verify chain ID is null or equal to current chain ID.
Expand Down