From cb65ee0b8c85150d670f2e886665b054de8a73d5 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:04:24 +0200 Subject: [PATCH 1/5] atomic gas charging Signed-off-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- cmd/evm/internal/t8ntool/execution.go | 2 +- consensus/beacon/consensus.go | 3 +- core/state/access_witness.go | 232 +++++++++++++++++--------- core/state_processor.go | 5 +- core/state_transition.go | 2 +- core/vm/evm.go | 28 +++- core/vm/instructions.go | 91 ++++++---- core/vm/interpreter.go | 10 +- core/vm/operations_verkle.go | 50 +++--- 9 files changed, 277 insertions(+), 146 deletions(-) diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 2fb49139d71..ac5e6c6d3ea 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -343,7 +343,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, // Amount is in gwei, turn into wei amount := new(big.Int).Mul(new(big.Int).SetUint64(w.Amount), big.NewInt(params.GWei)) statedb.AddBalance(w.Address, amount) - statedb.Witness().TouchFullAccount(w.Address[:], true, math.MaxUint64) + statedb.Witness().TouchFullAccount(w.Address[:], true) } if chainConfig.IsVerkle(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp) { if err := overlay.OverlayVerkleTransition(statedb, common.Hash{}, chainConfig.OverlayStride); err != nil { diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index c7bfd1e55b0..7a591052cd1 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -22,7 +22,6 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" @@ -359,7 +358,7 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. state.AddBalance(w.Address, amount) // The returned gas is not charged - state.Witness().TouchFullAccount(w.Address[:], true, math.MaxUint64) + state.Witness().TouchFullAccount(w.Address[:], true) } if chain.Config().IsVerkle(header.Number, header.Time) { diff --git a/core/state/access_witness.go b/core/state/access_witness.go index 940a507b0e4..67b4ee99771 100644 --- a/core/state/access_witness.go +++ b/core/state/access_witness.go @@ -17,6 +17,8 @@ package state import ( + "errors" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/params" @@ -89,83 +91,152 @@ func (aw *AccessWitness) Copy() *AccessWitness { return naw } -func (aw *AccessWitness) TouchFullAccount(addr []byte, isWrite bool, availableGas uint64) uint64 { - var gas uint64 +func (aw *AccessWitness) FullAccountGas(addr []byte, isWrite bool) uint64 { + return aw.calculateWitnessGas(addr, zeroTreeIndex, isWrite, utils.BasicDataLeafKey, utils.CodeHashLeafKey) +} + +func (aw *AccessWitness) TouchFullAccount(addr []byte, isWrite bool) { for i := utils.BasicDataLeafKey; i <= utils.CodeHashLeafKey; i++ { - consumed, wanted := aw.touchAddressAndChargeGas(addr, zeroTreeIndex, byte(i), isWrite, availableGas) - if consumed < wanted { - return wanted + gas - } - availableGas -= consumed - gas += consumed + aw.touchLocation(addr, zeroTreeIndex, byte(i), isWrite) } - return gas } -func (aw *AccessWitness) TouchAndChargeMessageCall(addr []byte, availableGas uint64) uint64 { - _, wanted := aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, false, availableGas) +func (aw *AccessWitness) MessageCallGas(addr []byte) uint64 { + wanted := aw.calculateWitnessGas(addr, zeroTreeIndex, false, utils.BasicDataLeafKey) if wanted == 0 { wanted = params.WarmStorageReadCostEIP2929 } return wanted } -func (aw *AccessWitness) TouchAndChargeValueTransfer(callerAddr, targetAddr []byte, availableGas uint64) uint64 { - _, wanted1 := aw.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BasicDataLeafKey, true, availableGas) - if wanted1 > availableGas { - return wanted1 - } - _, wanted2 := aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BasicDataLeafKey, true, availableGas-wanted1) +func (aw *AccessWitness) TouchMessageCall(addr []byte) { + aw.touchLocation(addr, zeroTreeIndex, utils.BasicDataLeafKey, false) +} + +func (aw *AccessWitness) TouchValueTransfer(callerAddr, targetAddr []byte) { + aw.touchLocation(callerAddr, zeroTreeIndex, utils.BasicDataLeafKey, true) + aw.touchLocation(targetAddr, zeroTreeIndex, utils.BasicDataLeafKey, true) +} + +func (aw *AccessWitness) ValueTransferGas(callerAddr, targetAddr []byte) uint64 { + wanted1 := aw.calculateWitnessGas(callerAddr, zeroTreeIndex, true, utils.BasicDataLeafKey) + wanted2 := aw.calculateWitnessGas(targetAddr, zeroTreeIndex, true, utils.BasicDataLeafKey) if wanted1+wanted2 == 0 { return params.WarmStorageReadCostEIP2929 } return wanted1 + wanted2 } +// ContractCreateCheckGas charges access costs before +// a contract creation is initiated. It is just reads, because the +// address collision is done before the transfer, and so no write +// are guaranteed to happen at this point. +func (aw *AccessWitness) ContractCreateCheckGas(addr []byte) uint64 { + return aw.calculateWitnessGas(addr, zeroTreeIndex, false, utils.BasicDataLeafKey, utils.CodeHashLeafKey) +} + // TouchAndChargeContractCreateCheck charges access costs before // a contract creation is initiated. It is just reads, because the // address collision is done before the transfer, and so no write // are guaranteed to happen at this point. -func (aw *AccessWitness) TouchAndChargeContractCreateCheck(addr []byte, availableGas uint64) uint64 { - gas1, wanted1 := aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, false, availableGas) - _, wanted2 := aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, false, availableGas-gas1) - return wanted1 + wanted2 +func (aw *AccessWitness) TouchContractCreateCheck(addr []byte) { + aw.touchLocation(addr, zeroTreeIndex, utils.BasicDataLeafKey, false) + aw.touchLocation(addr, zeroTreeIndex, utils.CodeHashLeafKey, false) +} + +// ContractCreateInitGas charges access costs to initiate a contract creation. +func (aw *AccessWitness) ContractCreateInitGas(addr []byte) uint64 { + return aw.calculateWitnessGas(addr, zeroTreeIndex, true, utils.BasicDataLeafKey, utils.CodeHashLeafKey) } // TouchAndChargeContractCreateInit charges access costs to initiate // a contract creation. -func (aw *AccessWitness) TouchAndChargeContractCreateInit(addr []byte, availableGas uint64) (uint64, uint64) { - gas1, wanted1 := aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, true, availableGas) - gas2, wanted2 := aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, true, availableGas-gas1) - return gas1 + gas2, wanted1 + wanted2 +func (aw *AccessWitness) TouchContractCreateInit(addr []byte, availableGas uint64) { + aw.touchLocation(addr, zeroTreeIndex, utils.BasicDataLeafKey, true) + aw.touchLocation(addr, zeroTreeIndex, utils.CodeHashLeafKey, true) } func (aw *AccessWitness) TouchTxOriginAndComputeGas(originAddr []byte) { for i := utils.BasicDataLeafKey; i <= utils.CodeHashLeafKey; i++ { - aw.touchAddressAndChargeGas(originAddr, zeroTreeIndex, byte(i), i == utils.BasicDataLeafKey, math.MaxUint64) + aw.touchLocation(originAddr, zeroTreeIndex, byte(i), i == utils.BasicDataLeafKey) } } func (aw *AccessWitness) TouchTxTarget(targetAddr []byte, sendsValue, doesntExist bool) { - aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BasicDataLeafKey, sendsValue, math.MaxUint64) + aw.touchLocation(targetAddr, zeroTreeIndex, utils.BasicDataLeafKey, sendsValue) // Note that we do a write-event in CodeHash without distinguishing if the tx target account // exists or not. Pre-7702, there's no situation in which an existing codeHash can be mutated, thus // doing a write-event shouldn't cause an observable difference in gas usage. // TODO(7702): re-check this in the spec and implementation to be sure is a correct solution after // EIP-7702 is implemented. - aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.CodeHashLeafKey, doesntExist, math.MaxUint64) + aw.touchLocation(targetAddr, zeroTreeIndex, utils.CodeHashLeafKey, doesntExist) } -func (aw *AccessWitness) TouchSlotAndChargeGas(addr []byte, slot common.Hash, isWrite bool, availableGas uint64, warmCostCharging bool) uint64 { +func (aw *AccessWitness) SlotGas(addr []byte, slot common.Hash, isWrite bool) uint64 { treeIndex, subIndex := utils.GetTreeKeyStorageSlotTreeIndexes(slot.Bytes()) - _, wanted := aw.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite, availableGas) - if wanted == 0 && warmCostCharging { + wanted := aw.calculateWitnessGas(addr, *treeIndex, isWrite, subIndex) + if wanted == 0 { wanted = params.WarmStorageReadCostEIP2929 } return wanted } -func (aw *AccessWitness) touchAddressAndChargeGas(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool, availableGas uint64) (uint64, uint64) { +func (aw *AccessWitness) TouchSlot(addr []byte, slot common.Hash, isWrite bool) { + treeIndex, subIndex := utils.GetTreeKeyStorageSlotTreeIndexes(slot.Bytes()) + aw.touchLocation(addr, *treeIndex, subIndex, isWrite) +} + +func (aw *AccessWitness) calculateWitnessGas(addr []byte, treeIndex uint256.Int, isWrite bool, subIndices ...byte) uint64 { + var ( + gas uint64 + branchKey = newBranchAccessKey(addr, treeIndex) + branchRead, branchWrite bool + ) + + // Read access. + if _, hasStem := aw.branches[branchKey]; !hasStem { + branchRead = true + } + + // Write access. + if isWrite { + if (aw.branches[branchKey] & AccessWitnessWriteFlag) == 0 { + branchWrite = true + } + } + + if branchRead { + gas += params.WitnessBranchReadCost + } + if branchWrite { + gas += params.WitnessBranchWriteCost + } + + for _, subIndex := range subIndices { + var chunkRead, chunkWrite, chunkFill bool + chunkKey := newChunkAccessKey(branchKey, subIndex) + if _, hasSelector := aw.chunks[chunkKey]; !hasSelector { + chunkRead = true + + } + if isWrite && (aw.chunks[chunkKey]&AccessWitnessWriteFlag) == 0 { + chunkWrite = true + } + if chunkRead { + gas += params.WitnessChunkReadCost + } + if chunkWrite { + gas += params.WitnessChunkWriteCost + } + if chunkFill { + gas += params.WitnessChunkFillCost + } + } + + return gas +} + +func (aw *AccessWitness) touchLocation(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool) { branchKey := newBranchAccessKey(addr, treeIndex) chunkKey := newChunkAccessKey(branchKey, subIndex) @@ -191,28 +262,6 @@ func (aw *AccessWitness) touchAddressAndChargeGas(addr []byte, treeIndex uint256 } } - var gas uint64 - if branchRead { - gas += params.WitnessBranchReadCost - } - if chunkRead { - gas += params.WitnessChunkReadCost - } - if branchWrite { - gas += params.WitnessBranchWriteCost - } - if chunkWrite { - gas += params.WitnessChunkWriteCost - } - if chunkFill { - gas += params.WitnessChunkFillCost - } - - if availableGas < gas { - // consumed != wanted - return availableGas, gas - } - if branchRead { aw.branches[branchKey] = AccessWitnessReadFlag } @@ -224,10 +273,11 @@ func (aw *AccessWitness) touchAddressAndChargeGas(addr []byte, treeIndex uint256 } if chunkWrite { aw.chunks[chunkKey] |= AccessWitnessWriteFlag - } - // consumed == wanted - return gas, gas + if chunkFill { + // TODO when FILL_COST is implemented + } + } } type branchAccessKey struct { @@ -254,8 +304,8 @@ func newChunkAccessKey(branchKey branchAccessKey, leafKey byte) chunkAccessKey { return lk } -// touchCodeChunksRangeOnReadAndChargeGas is a helper function to touch every chunk in a code range and charge witness gas costs -func (aw *AccessWitness) TouchCodeChunksRangeAndChargeGas(contractAddr []byte, startPC, size uint64, codeLen uint64, isWrite bool, availableGas uint64) (uint64, uint64) { +// CodeChunksRangeGas is a helper function to touch every chunk in a code range and charge witness gas costs +func (aw *AccessWitness) CodeChunksRangeGas(contractAddr []byte, startPC, size uint64, codeLen uint64, isWrite bool) (uint64, error) { // note that in the case where the copied code is outside the range of the // contract code but touches the last leaf with contract code in it, // we don't include the last leaf of code in the AccessWitness. The @@ -263,7 +313,7 @@ func (aw *AccessWitness) TouchCodeChunksRangeAndChargeGas(contractAddr []byte, s // is already in the AccessWitness so a stateless verifier can see that // the code from the last leaf is not needed. if size == 0 || startPC >= codeLen { - return 0, 0 + return 0, nil } endPC := startPC + size @@ -278,39 +328,63 @@ func (aw *AccessWitness) TouchCodeChunksRangeAndChargeGas(contractAddr []byte, s for chunkNumber := startPC / 31; chunkNumber <= endPC/31; chunkNumber++ { treeIndex := *uint256.NewInt((chunkNumber + 128) / 256) subIndex := byte((chunkNumber + 128) % 256) - consumed, wanted := aw.touchAddressAndChargeGas(contractAddr, treeIndex, subIndex, isWrite, availableGas) - // did we OOG ? - if wanted > consumed { - return statelessGasCharged + consumed, statelessGasCharged + wanted - } + wanted := aw.calculateWitnessGas(contractAddr, treeIndex, isWrite, subIndex) var overflow bool - statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, consumed) + statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, wanted) if overflow { - panic("overflow when adding gas") + return 0, errors.New("gas uint overflow") } - availableGas -= consumed } - return statelessGasCharged, statelessGasCharged + return statelessGasCharged, nil } -func (aw *AccessWitness) TouchBasicData(addr []byte, isWrite bool, availableGas uint64, warmCostCharging bool) uint64 { - _, wanted := aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, isWrite, availableGas) +// TouchCodeChunksRange is a helper function to touch every chunk in a code range and charge witness gas costs +func (aw *AccessWitness) TouchCodeChunksRange(contractAddr []byte, startPC, size uint64, codeLen uint64, isWrite bool) { + // note that in the case where the copied code is outside the range of the + // contract code but touches the last leaf with contract code in it, + // we don't include the last leaf of code in the AccessWitness. The + // reason that we do not need the last leaf is the account's code size + // is already in the AccessWitness so a stateless verifier can see that + // the code from the last leaf is not needed. + if size == 0 || startPC >= codeLen { + return + } + + endPC := startPC + size + if endPC > codeLen { + endPC = codeLen + } + if endPC > 0 { + endPC -= 1 // endPC is the last bytecode that will be touched. + } + + for chunkNumber := startPC / 31; chunkNumber <= endPC/31; chunkNumber++ { + treeIndex := *uint256.NewInt((chunkNumber + 128) / 256) + subIndex := byte((chunkNumber + 128) % 256) + aw.touchLocation(contractAddr, treeIndex, subIndex, isWrite) + } +} + +func (aw *AccessWitness) TouchBasicData(addr []byte, isWrite bool) { + aw.touchLocation(addr, zeroTreeIndex, utils.BasicDataLeafKey, isWrite) +} + +func (aw *AccessWitness) BasicDataGas(addr []byte, isWrite bool, warmCostCharging bool) uint64 { + wanted := aw.calculateWitnessGas(addr, zeroTreeIndex, isWrite, utils.BasicDataLeafKey) if wanted == 0 && warmCostCharging { - if availableGas < params.WarmStorageReadCostEIP2929 { - return availableGas - } wanted = params.WarmStorageReadCostEIP2929 } return wanted } -func (aw *AccessWitness) TouchCodeHash(addr []byte, isWrite bool, availableGas uint64, chargeWarmCosts bool) uint64 { - _, wanted := aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, isWrite, availableGas) +func (aw *AccessWitness) TouchCodeHash(addr []byte, isWrite bool) { + aw.touchLocation(addr, zeroTreeIndex, utils.CodeHashLeafKey, isWrite) +} + +func (aw *AccessWitness) CodeHashGas(addr []byte, isWrite bool, chargeWarmCosts bool) uint64 { + wanted := aw.calculateWitnessGas(addr, zeroTreeIndex, isWrite, utils.CodeHashLeafKey) if wanted == 0 && chargeWarmCosts { - if availableGas < params.WarmStorageReadCostEIP2929 { - return availableGas - } wanted = params.WarmStorageReadCostEIP2929 } return wanted diff --git a/core/state_processor.go b/core/state_processor.go index 9ad1a1a518a..835d7db726c 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -23,7 +23,6 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/core/state" @@ -178,7 +177,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo func InsertBlockHashHistoryAtEip2935Fork(statedb *state.StateDB, prevNumber uint64, prevHash common.Hash, chain consensus.ChainHeaderReader) { // Make sure that the historical contract is added to the witness - statedb.Witness().TouchFullAccount(params.HistoryStorageAddress[:], true, math.MaxUint64) + statedb.Witness().TouchFullAccount(params.HistoryStorageAddress[:], true) ancestor := chain.GetHeader(prevHash, prevNumber) for i := prevNumber; i > 0 && i >= prevNumber-params.Eip2935BlockHashHistorySize; i-- { @@ -192,5 +191,5 @@ func ProcessParentBlockHash(statedb *state.StateDB, prevNumber uint64, prevHash var key common.Hash binary.BigEndian.PutUint64(key[24:], ringIndex) statedb.SetState(params.HistoryStorageAddress, key, prevHash) - statedb.Witness().TouchSlotAndChargeGas(params.HistoryStorageAddress[:], key, true, math.MaxUint64, false) + statedb.Witness().TouchSlot(params.HistoryStorageAddress[:], key, true) } diff --git a/core/state_transition.go b/core/state_transition.go index 98e520d4498..3a9ba069605 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -453,7 +453,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { // add the coinbase to the witness iff the fee is greater than 0 if rules.IsEIP4762 && fee.Sign() != 0 { - st.evm.Accesses.TouchFullAccount(st.evm.Context.Coinbase[:], true, math.MaxUint64) + st.evm.Accesses.TouchFullAccount(st.evm.Context.Coinbase[:], true) } } diff --git a/core/vm/evm.go b/core/vm/evm.go index 4371c0c4140..c3a9a544d39 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -207,11 +207,12 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas // list in write mode. If there is enough gas paying for the addition of the code // hash leaf to the access list, then account creation will proceed unimpaired. // Thus, only pay for the creation of the code hash leaf here. - wgas := evm.Accesses.TouchCodeHash(addr.Bytes(), true, gas, false) + wgas := evm.Accesses.CodeHashGas(addr.Bytes(), true, false) if gas < wgas { evm.StateDB.RevertToSnapshot(snapshot) return nil, 0, ErrOutOfGas } + evm.Accesses.TouchCodeHash(addr.Bytes(), true) gas -= wgas } @@ -462,11 +463,12 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // Charge the contract creation init gas in verkle mode if evm.chainRules.IsEIP4762 { - statelessGas := evm.Accesses.TouchAndChargeContractCreateCheck(address.Bytes(), gas) + statelessGas := evm.Accesses.ContractCreateCheckGas(address.Bytes()) if statelessGas > gas { return nil, common.Address{}, 0, ErrOutOfGas } gas -= statelessGas + evm.Accesses.TouchContractCreateCheck(address.Bytes()) } // We add this to the access list _before_ taking a snapshot. Even if the creation fails, // the access-list change should not be rolled back @@ -481,11 +483,12 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // Charge the contract creation init gas in verkle mode if evm.chainRules.IsEIP4762 { - consumed, wanted := evm.Accesses.TouchAndChargeContractCreateInit(address.Bytes(), gas) - if consumed < wanted { + wanted := evm.Accesses.ContractCreateInitGas(address.Bytes()) + if gas < wanted { return nil, common.Address{}, 0, ErrOutOfGas } - gas -= consumed + gas -= wanted + evm.Accesses.TouchContractCreateInit(address.Bytes(), gas) } // Create a new account on the state @@ -534,10 +537,17 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, err = ErrCodeStoreOutOfGas } } else { - consumed, wanted := evm.Accesses.TouchCodeChunksRangeAndChargeGas(address.Bytes(), 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas) - contract.UseGas(consumed) // consumed <= contract.Gas, so no return value check is needed - if len(ret) > 0 && (consumed < wanted) { - err = ErrCodeStoreOutOfGas + var wanted uint64 + wanted, err = evm.Accesses.CodeChunksRangeGas(address.Bytes(), 0, uint64(len(ret)), uint64(len(ret)), true) + if err == nil { + ok := contract.UseGas(wanted) + // TODO check that the len(ret) is necessary here. If it's 0, then + // we should not get any gas charged, so ok would always be true. + if len(ret) > 0 && !ok { + err = ErrCodeStoreOutOfGas + } else { + evm.Accesses.TouchCodeChunksRange(address.Bytes(), 0, uint64(len(ret)), uint64(len(ret)), true) + } } } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 6feb77ae8ba..5b9233d2216 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -20,7 +20,6 @@ import ( "encoding/binary" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" @@ -262,6 +261,7 @@ func opAddress(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([] func opBalance(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { slot := scope.Stack.peek() address := common.Address(slot.Bytes20()) + interpreter.evm.Accesses.TouchBasicData(address[:], false) slot.SetFromBig(interpreter.evm.StateDB.GetBalance(address)) return nil, nil } @@ -270,6 +270,7 @@ func opOrigin(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b scope.Stack.push(new(uint256.Int).SetBytes(interpreter.evm.Origin.Bytes())) return nil, nil } + func opCaller(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.push(new(uint256.Int).SetBytes(scope.Contract.Caller().Bytes())) return nil, nil @@ -345,6 +346,9 @@ func opReturnDataCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeConte func opExtCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { slot := scope.Stack.peek() address := slot.Bytes20() + if interpreter.evm.chainRules.IsEIP4762 { + interpreter.evm.Accesses.TouchBasicData(address[:], false) + } cs := uint64(interpreter.evm.StateDB.GetCodeSize(address)) slot.SetUint64(cs) return nil, nil @@ -371,11 +375,15 @@ func opCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([ codeAddr := scope.Contract.CodeAddr paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(scope.Contract.Code, uint64CodeOffset, length.Uint64()) if interpreter.evm.chainRules.IsEIP4762 && !scope.Contract.IsDeployment { - statelessGas, wanted := interpreter.evm.Accesses.TouchCodeChunksRangeAndChargeGas(codeAddr[:], copyOffset, nonPaddedCopyLength, uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) - scope.Contract.UseGas(statelessGas) - if statelessGas < wanted { + statelessGas, err := interpreter.evm.Accesses.CodeChunksRangeGas(codeAddr[:], copyOffset, nonPaddedCopyLength, uint64(len(scope.Contract.Code)), false) + if err != nil { + return nil, err + } + ok := scope.Contract.UseGas(statelessGas) + if !ok { return nil, ErrOutOfGas } + interpreter.evm.Accesses.TouchCodeChunksRange(codeAddr[:], copyOffset, nonPaddedCopyLength, uint64(len(scope.Contract.Code)), false) } scope.Memory.Set(memOffset.Uint64(), uint64(len(paddedCodeCopy)), paddedCodeCopy) return nil, nil @@ -401,11 +409,15 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) self: AccountRef(addr), } paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64()) - statelessGas, wanted := interpreter.evm.Accesses.TouchCodeChunksRangeAndChargeGas(addr[:], copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false, scope.Contract.Gas) - scope.Contract.UseGas(statelessGas) // statelessGas <= contract.Gas, so no need to check the return value - if statelessGas < wanted { + statelessGas, err := interpreter.evm.Accesses.CodeChunksRangeGas(addr[:], copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false) + if err != nil { + return nil, err + } + ok := scope.Contract.UseGas(statelessGas) // statelessGas <= contract.Gas, so no need to check the return value + if !ok { return nil, ErrOutOfGas } + interpreter.evm.Accesses.TouchCodeChunksRange(addr[:], copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false) scope.Memory.Set(memOffset.Uint64(), length.Uint64(), paddedCodeCopy) } else { codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64()) @@ -444,6 +456,9 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) func opExtCodeHash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { slot := scope.Stack.peek() address := common.Address(slot.Bytes20()) + if interpreter.evm.chainRules.IsEIP4762 { + interpreter.evm.Accesses.TouchCodeHash(address[:], false) + } if interpreter.evm.StateDB.Empty(address) { slot.Clear() } else { @@ -458,14 +473,6 @@ func opGasprice(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([ return nil, nil } -func getBlockHashFromContract(number uint64, statedb StateDB, witness *state.AccessWitness, availableGas uint64) (common.Hash, uint64) { - ringIndex := number % params.Eip2935BlockHashHistorySize - var pnum common.Hash - binary.BigEndian.PutUint64(pnum[24:], ringIndex) - statelessGas := witness.TouchSlotAndChargeGas(params.HistoryStorageAddress[:], pnum, false, availableGas, true) - return statedb.GetState(params.HistoryStorageAddress, pnum), statelessGas -} - func opBlockhash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { num := scope.Stack.peek() num64, overflow := num.Uint64WithOverflow() @@ -486,12 +493,17 @@ func opBlockhash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ( if num64 >= lower && num64 < upper { // if Verkle is active, read it from the history contract (EIP 2935). if evm.chainRules.IsVerkle { - blockHash, statelessGas := getBlockHashFromContract(num64, evm.StateDB, evm.Accesses, scope.Contract.Gas) + ringIndex := num64 % params.Eip2935BlockHashHistorySize + var pnum common.Hash if interpreter.evm.chainRules.IsEIP4762 { + binary.BigEndian.PutUint64(pnum[24:], ringIndex) + statelessGas := evm.Accesses.SlotGas(params.HistoryStorageAddress[:], pnum, false) if !scope.Contract.UseGas(statelessGas) { return nil, ErrOutOfGas } + evm.Accesses.TouchSlot(params.HistoryStorageAddress[:], pnum, false) } + blockHash := evm.StateDB.GetState(params.HistoryStorageAddress, pnum) num.SetBytes(blockHash.Bytes()) } else { num.SetBytes(interpreter.evm.Context.GetHash(num64).Bytes()) @@ -565,6 +577,9 @@ func opSload(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by hash := common.Hash(loc.Bytes32()) val := interpreter.evm.StateDB.GetState(scope.Contract.Address(), hash) + if interpreter.evm.chainRules.IsEIP4762 { + interpreter.evm.Accesses.TouchSlot(scope.Contract.Address().Bytes(), loc.Bytes32(), false) + } loc.SetBytes(val.Bytes()) return nil, nil } @@ -575,6 +590,10 @@ func opSstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b } loc := scope.Stack.pop() val := scope.Stack.pop() + + if interpreter.evm.chainRules.IsEIP4762 { + interpreter.evm.Accesses.TouchSlot(scope.Contract.Address().Bytes(), loc.Bytes32(), true) + } interpreter.evm.StateDB.SetState(scope.Contract.Address(), loc.Bytes32(), val.Bytes32()) return nil, nil } @@ -585,11 +604,15 @@ func opJump(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt } pos := scope.Stack.pop() if interpreter.evm.chainRules.IsEIP4762 && !scope.Contract.IsDeployment { - statelessGas, wanted := interpreter.evm.TxContext.Accesses.TouchCodeChunksRangeAndChargeGas(scope.Contract.CodeAddr[:], pos.Uint64(), 1, uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) - scope.Contract.UseGas(statelessGas) - if statelessGas < wanted { + statelessGas, err := interpreter.evm.TxContext.Accesses.CodeChunksRangeGas(scope.Contract.CodeAddr[:], pos.Uint64(), 1, uint64(len(scope.Contract.Code)), false) + if err != nil { + return nil, err + } + ok := scope.Contract.UseGas(statelessGas) + if !ok { return nil, ErrOutOfGas } + interpreter.evm.TxContext.Accesses.TouchCodeChunksRange(scope.Contract.CodeAddr[:], pos.Uint64(), 1, uint64(len(scope.Contract.Code)), false) } if !scope.Contract.validJumpdest(&pos) { return nil, ErrInvalidJump @@ -605,11 +628,15 @@ func opJumpi(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by pos, cond := scope.Stack.pop(), scope.Stack.pop() if !cond.IsZero() { if interpreter.evm.chainRules.IsEIP4762 && !scope.Contract.IsDeployment { - statelessGas, wanted := interpreter.evm.TxContext.Accesses.TouchCodeChunksRangeAndChargeGas(scope.Contract.CodeAddr[:], pos.Uint64(), 1, uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) - scope.Contract.UseGas(statelessGas) - if statelessGas < wanted { + statelessGas, err := interpreter.evm.TxContext.Accesses.CodeChunksRangeGas(scope.Contract.CodeAddr[:], pos.Uint64(), 1, uint64(len(scope.Contract.Code)), false) + if err != nil { + return nil, err + } + ok := scope.Contract.UseGas(statelessGas) + if !ok { return nil, ErrOutOfGas } + interpreter.evm.TxContext.Accesses.TouchCodeChunksRange(scope.Contract.CodeAddr[:], pos.Uint64(), 1, uint64(len(scope.Contract.Code)), false) } if !scope.Contract.validJumpdest(&pos) { return nil, ErrInvalidJump @@ -958,11 +985,15 @@ func opPush1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by // touch next chunk if PUSH1 is at the boundary. if so, *pc has // advanced past this boundary. codeAddr := scope.Contract.CodeAddr - statelessGas, wanted := interpreter.evm.Accesses.TouchCodeChunksRangeAndChargeGas(codeAddr[:], *pc, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) - scope.Contract.UseGas(statelessGas) - if statelessGas < wanted { + statelessGas, err := interpreter.evm.Accesses.CodeChunksRangeGas(codeAddr[:], *pc, uint64(1), uint64(len(scope.Contract.Code)), false) + if err != nil { + return nil, err + } + ok := scope.Contract.UseGas(statelessGas) + if !ok { return nil, ErrOutOfGas } + interpreter.evm.Accesses.TouchCodeChunksRange(codeAddr[:], *pc, uint64(1), uint64(len(scope.Contract.Code)), false) } } else { scope.Stack.push(integer.Clear()) @@ -987,11 +1018,15 @@ func makePush(size uint64, pushByteSize int) executionFunc { if !scope.Contract.IsDeployment && interpreter.evm.chainRules.IsVerkle { codeAddr := scope.Contract.CodeAddr - statelessGas, wanted := interpreter.evm.Accesses.TouchCodeChunksRangeAndChargeGas(codeAddr[:], uint64(startMin), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) - scope.Contract.UseGas(statelessGas) - if statelessGas < wanted { + statelessGas, err := interpreter.evm.Accesses.CodeChunksRangeGas(codeAddr[:], uint64(startMin), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false) + if err != nil { + return nil, err + } + ok := scope.Contract.UseGas(statelessGas) + if !ok { return nil, ErrOutOfGas } + interpreter.evm.Accesses.TouchCodeChunksRange(codeAddr[:], uint64(startMin), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false) } integer := new(uint256.Int) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 0a29133a998..724c80e82f9 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -183,11 +183,15 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( // if the PC ends up in a new "chunk" of verkleized code, charge the // associated costs. codeAddr := contract.CodeAddr - consumed, wanted := in.evm.TxContext.Accesses.TouchCodeChunksRangeAndChargeGas(codeAddr[:], pc, 1, uint64(len(contract.Code)), false, contract.Gas) - contract.UseGas(consumed) - if consumed < wanted { + wanted, err := in.evm.TxContext.Accesses.CodeChunksRangeGas(codeAddr[:], pc, 1, uint64(len(contract.Code)), false) + if err != nil { + return nil, err + } + ok := contract.UseGas(wanted) + if !ok { return nil, ErrOutOfGas } + in.evm.TxContext.Accesses.TouchCodeChunksRange(codeAddr[:], pc, 1, uint64(len(contract.Code)), false) } // Get the operation from the jump table and validate the stack to ensure there are diff --git a/core/vm/operations_verkle.go b/core/vm/operations_verkle.go index 1d96c9578f5..02682d9dfac 100644 --- a/core/vm/operations_verkle.go +++ b/core/vm/operations_verkle.go @@ -23,16 +23,16 @@ import ( ) func gasSStore4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - return evm.Accesses.TouchSlotAndChargeGas(contract.Address().Bytes(), common.Hash(stack.peek().Bytes32()), true, contract.Gas, true), nil + return evm.Accesses.SlotGas(contract.Address().Bytes(), common.Hash(stack.peek().Bytes32()), true), nil } func gasSLoad4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - return evm.Accesses.TouchSlotAndChargeGas(contract.Address().Bytes(), common.Hash(stack.peek().Bytes32()), false, contract.Gas, true), nil + return evm.Accesses.SlotGas(contract.Address().Bytes(), common.Hash(stack.peek().Bytes32()), false), nil } func gasBalance4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { address := stack.peek().Bytes20() - return evm.Accesses.TouchBasicData(address[:], false, contract.Gas, true), nil + return evm.Accesses.BasicDataGas(address[:], false, true), nil } func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { @@ -42,7 +42,7 @@ func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if isPrecompile || isSystemContract { return params.WarmStorageReadCostEIP2929, nil } - return evm.Accesses.TouchBasicData(address[:], false, contract.Gas, true), nil + return evm.Accesses.BasicDataGas(address[:], false, true), nil } func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { @@ -50,7 +50,7 @@ func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if _, isPrecompile := evm.precompile(address); isPrecompile || evm.isSystemContract(address) { return params.WarmStorageReadCostEIP2929, nil } - return evm.Accesses.TouchCodeHash(address[:], false, contract.Gas, true), nil + return evm.Accesses.CodeHashGas(address[:], false, true), nil } func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) gasFunc { @@ -65,10 +65,11 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) ga // If value is transferred, it is charged before 1/64th // is subtracted from the available gas pool. if withTransferCosts && !stack.Back(2).IsZero() { - wantedValueTransferWitnessGas := evm.Accesses.TouchAndChargeValueTransfer(contract.Address().Bytes()[:], target[:], contract.Gas) + wantedValueTransferWitnessGas := evm.Accesses.ValueTransferGas(contract.Address().Bytes(), target[:]) if wantedValueTransferWitnessGas > contract.Gas { - return wantedValueTransferWitnessGas, nil + return 0, ErrOutOfGas } + evm.Accesses.TouchValueTransfer(contract.Address().Bytes(), target[:]) witnessGas = wantedValueTransferWitnessGas } else if isPrecompile || isSystemContract { witnessGas = params.WarmStorageReadCostEIP2929 @@ -78,14 +79,15 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) ga // (so before we get to this point) // But the message call is part of the subcall, for which only 63/64th // of the gas should be available. - wantedMessageCallWitnessGas := evm.Accesses.TouchAndChargeMessageCall(target.Bytes(), contract.Gas-witnessGas) + wantedMessageCallWitnessGas := evm.Accesses.MessageCallGas(target.Bytes()) var overflow bool if witnessGas, overflow = math.SafeAdd(witnessGas, wantedMessageCallWitnessGas); overflow { return 0, ErrGasUintOverflow } if witnessGas > contract.Gas { - return witnessGas, nil + return 0, ErrOutOfGas } + evm.Accesses.TouchMessageCall(target.Bytes()) } contract.Gas -= witnessGas @@ -111,11 +113,11 @@ func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Mem beneficiaryAddr := common.Address(stack.peek().Bytes20()) contractAddr := contract.Address() - wanted := evm.Accesses.TouchBasicData(contractAddr[:], false, contract.Gas, false) - if wanted > contract.Gas { - return wanted, nil + statelessGas := evm.Accesses.BasicDataGas(contractAddr[:], false, false) + if statelessGas > contract.Gas { + return 0, ErrOutOfGas } - statelessGas := wanted + evm.Accesses.TouchBasicData(contractAddr[:], false) balanceIsZero := evm.StateDB.GetBalance(contractAddr).Sign() == 0 _, isPrecompile := evm.precompile(beneficiaryAddr) @@ -126,30 +128,37 @@ func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Mem } if contractAddr != beneficiaryAddr { - wanted := evm.Accesses.TouchBasicData(beneficiaryAddr[:], false, contract.Gas-statelessGas, false) + wanted := evm.Accesses.BasicDataGas(beneficiaryAddr[:], false, false) if wanted > contract.Gas-statelessGas { return statelessGas + wanted, nil } statelessGas += wanted + evm.Accesses.TouchBasicData(beneficiaryAddr[:], false) } // Charge write costs if it transfers value if !balanceIsZero { - wanted := evm.Accesses.TouchBasicData(contractAddr[:], true, contract.Gas-statelessGas, false) + wanted := evm.Accesses.BasicDataGas(contractAddr[:], true, false) if wanted > contract.Gas-statelessGas { - return statelessGas + wanted, nil + return 0, ErrOutOfGas } statelessGas += wanted + evm.Accesses.TouchBasicData(contractAddr[:], true) if contractAddr != beneficiaryAddr { if evm.StateDB.Exist(beneficiaryAddr) { - wanted = evm.Accesses.TouchBasicData(beneficiaryAddr[:], true, contract.Gas-statelessGas, false) + wanted = evm.Accesses.BasicDataGas(beneficiaryAddr[:], true, false) } else { - wanted = evm.Accesses.TouchFullAccount(beneficiaryAddr[:], true, contract.Gas-statelessGas) + wanted = evm.Accesses.FullAccountGas(beneficiaryAddr[:], true) } if wanted > contract.Gas-statelessGas { - return statelessGas + wanted, nil + return 0, ErrOutOfGas } statelessGas += wanted + if evm.StateDB.Exist(beneficiaryAddr) { + evm.Accesses.TouchBasicData(beneficiaryAddr[:], true) + } else { + evm.Accesses.TouchFullAccount(beneficiaryAddr[:], true) + } } } return statelessGas, nil @@ -172,10 +181,11 @@ func gasExtCodeCopyEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memo } return gas, nil } - wgas := evm.Accesses.TouchBasicData(addr[:], false, contract.Gas-gas, true) + wgas := evm.Accesses.BasicDataGas(addr[:], false, true) var overflow bool if gas, overflow = math.SafeAdd(gas, wgas); overflow { return 0, ErrGasUintOverflow } + evm.Accesses.TouchBasicData(addr[:], false) return gas, nil } From 30b41a7e0c3390118066c1f2f345d1c50f8ca832 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 6 May 2025 21:22:21 +0200 Subject: [PATCH 2/5] fix code range boundary computation + remove variadic parameter --- core/state/access_witness.go | 45 ++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/core/state/access_witness.go b/core/state/access_witness.go index 67b4ee99771..2056bf8cd38 100644 --- a/core/state/access_witness.go +++ b/core/state/access_witness.go @@ -92,7 +92,7 @@ func (aw *AccessWitness) Copy() *AccessWitness { } func (aw *AccessWitness) FullAccountGas(addr []byte, isWrite bool) uint64 { - return aw.calculateWitnessGas(addr, zeroTreeIndex, isWrite, utils.BasicDataLeafKey, utils.CodeHashLeafKey) + return aw.calculateWitnessGasRange(addr, zeroTreeIndex, isWrite, utils.BasicDataLeafKey, utils.CodeHashLeafKey) } func (aw *AccessWitness) TouchFullAccount(addr []byte, isWrite bool) { @@ -102,7 +102,7 @@ func (aw *AccessWitness) TouchFullAccount(addr []byte, isWrite bool) { } func (aw *AccessWitness) MessageCallGas(addr []byte) uint64 { - wanted := aw.calculateWitnessGas(addr, zeroTreeIndex, false, utils.BasicDataLeafKey) + wanted := aw.calculateWitnessGasRange(addr, zeroTreeIndex, false, utils.BasicDataLeafKey, utils.BasicDataLeafKey) if wanted == 0 { wanted = params.WarmStorageReadCostEIP2929 } @@ -119,8 +119,8 @@ func (aw *AccessWitness) TouchValueTransfer(callerAddr, targetAddr []byte) { } func (aw *AccessWitness) ValueTransferGas(callerAddr, targetAddr []byte) uint64 { - wanted1 := aw.calculateWitnessGas(callerAddr, zeroTreeIndex, true, utils.BasicDataLeafKey) - wanted2 := aw.calculateWitnessGas(targetAddr, zeroTreeIndex, true, utils.BasicDataLeafKey) + wanted1 := aw.calculateWitnessGasRange(callerAddr, zeroTreeIndex, true, utils.BasicDataLeafKey, utils.BasicDataLeafKey) + wanted2 := aw.calculateWitnessGasRange(targetAddr, zeroTreeIndex, true, utils.BasicDataLeafKey, utils.BasicDataLeafKey) if wanted1+wanted2 == 0 { return params.WarmStorageReadCostEIP2929 } @@ -132,7 +132,7 @@ func (aw *AccessWitness) ValueTransferGas(callerAddr, targetAddr []byte) uint64 // address collision is done before the transfer, and so no write // are guaranteed to happen at this point. func (aw *AccessWitness) ContractCreateCheckGas(addr []byte) uint64 { - return aw.calculateWitnessGas(addr, zeroTreeIndex, false, utils.BasicDataLeafKey, utils.CodeHashLeafKey) + return aw.calculateWitnessGasRange(addr, zeroTreeIndex, false, utils.BasicDataLeafKey, utils.CodeHashLeafKey) } // TouchAndChargeContractCreateCheck charges access costs before @@ -146,7 +146,7 @@ func (aw *AccessWitness) TouchContractCreateCheck(addr []byte) { // ContractCreateInitGas charges access costs to initiate a contract creation. func (aw *AccessWitness) ContractCreateInitGas(addr []byte) uint64 { - return aw.calculateWitnessGas(addr, zeroTreeIndex, true, utils.BasicDataLeafKey, utils.CodeHashLeafKey) + return aw.calculateWitnessGasRange(addr, zeroTreeIndex, true, utils.BasicDataLeafKey, utils.CodeHashLeafKey) } // TouchAndChargeContractCreateInit charges access costs to initiate @@ -174,7 +174,7 @@ func (aw *AccessWitness) TouchTxTarget(targetAddr []byte, sendsValue, doesntExis func (aw *AccessWitness) SlotGas(addr []byte, slot common.Hash, isWrite bool) uint64 { treeIndex, subIndex := utils.GetTreeKeyStorageSlotTreeIndexes(slot.Bytes()) - wanted := aw.calculateWitnessGas(addr, *treeIndex, isWrite, subIndex) + wanted := aw.calculateWitnessGasRange(addr, *treeIndex, isWrite, uint64(subIndex), uint64(subIndex)) if wanted == 0 { wanted = params.WarmStorageReadCostEIP2929 } @@ -186,7 +186,7 @@ func (aw *AccessWitness) TouchSlot(addr []byte, slot common.Hash, isWrite bool) aw.touchLocation(addr, *treeIndex, subIndex, isWrite) } -func (aw *AccessWitness) calculateWitnessGas(addr []byte, treeIndex uint256.Int, isWrite bool, subIndices ...byte) uint64 { +func (aw *AccessWitness) calculateWitnessGasRange(addr []byte, treeIndex uint256.Int, isWrite bool, from, to uint64) uint64 { var ( gas uint64 branchKey = newBranchAccessKey(addr, treeIndex) @@ -212,9 +212,9 @@ func (aw *AccessWitness) calculateWitnessGas(addr []byte, treeIndex uint256.Int, gas += params.WitnessBranchWriteCost } - for _, subIndex := range subIndices { + for subIndex := from; subIndex <= to; subIndex++ { var chunkRead, chunkWrite, chunkFill bool - chunkKey := newChunkAccessKey(branchKey, subIndex) + chunkKey := newChunkAccessKey(branchKey, byte(subIndex)) if _, hasSelector := aw.chunks[chunkKey]; !hasSelector { chunkRead = true @@ -325,15 +325,30 @@ func (aw *AccessWitness) CodeChunksRangeGas(contractAddr []byte, startPC, size u } var statelessGasCharged uint64 - for chunkNumber := startPC / 31; chunkNumber <= endPC/31; chunkNumber++ { + for chunkNumber := startPC / 31; chunkNumber <= endPC/31; { + startSubIndex := (chunkNumber + 128) % 256 + var endSubIndex uint64 + if chunkNumber < 128 { + // special case of finding the upper boundary for the header group + endSubIndex = min(endPC/31-chunkNumber, 128) + 128 + } else { + endSubIndex = min(endPC/31-chunkNumber, 255) + } + treeIndex := *uint256.NewInt((chunkNumber + 128) / 256) - subIndex := byte((chunkNumber + 128) % 256) - wanted := aw.calculateWitnessGas(contractAddr, treeIndex, isWrite, subIndex) + wanted := aw.calculateWitnessGasRange(contractAddr, treeIndex, isWrite, startSubIndex, endSubIndex) var overflow bool statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, wanted) if overflow { return 0, errors.New("gas uint overflow") } + + // Find the next group boundary, taking the 128 offset into account. + if chunkNumber == 0 { + chunkNumber = 128 + } else { + chunkNumber += 256 + } } return statelessGasCharged, nil @@ -371,7 +386,7 @@ func (aw *AccessWitness) TouchBasicData(addr []byte, isWrite bool) { } func (aw *AccessWitness) BasicDataGas(addr []byte, isWrite bool, warmCostCharging bool) uint64 { - wanted := aw.calculateWitnessGas(addr, zeroTreeIndex, isWrite, utils.BasicDataLeafKey) + wanted := aw.calculateWitnessGasRange(addr, zeroTreeIndex, isWrite, utils.BasicDataLeafKey, utils.BasicDataLeafKey) if wanted == 0 && warmCostCharging { wanted = params.WarmStorageReadCostEIP2929 } @@ -383,7 +398,7 @@ func (aw *AccessWitness) TouchCodeHash(addr []byte, isWrite bool) { } func (aw *AccessWitness) CodeHashGas(addr []byte, isWrite bool, chargeWarmCosts bool) uint64 { - wanted := aw.calculateWitnessGas(addr, zeroTreeIndex, isWrite, utils.CodeHashLeafKey) + wanted := aw.calculateWitnessGasRange(addr, zeroTreeIndex, isWrite, utils.CodeHashLeafKey, utils.CodeHashLeafKey) if wanted == 0 && chargeWarmCosts { wanted = params.WarmStorageReadCostEIP2929 } From a68c13c55b9593b019dfd0162832fe72e0a9ef9e Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 7 May 2025 14:30:35 +0200 Subject: [PATCH 3/5] fix: end group index computation --- core/state/access_witness.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/state/access_witness.go b/core/state/access_witness.go index 2056bf8cd38..7f3caaa2863 100644 --- a/core/state/access_witness.go +++ b/core/state/access_witness.go @@ -330,7 +330,7 @@ func (aw *AccessWitness) CodeChunksRangeGas(contractAddr []byte, startPC, size u var endSubIndex uint64 if chunkNumber < 128 { // special case of finding the upper boundary for the header group - endSubIndex = min(endPC/31-chunkNumber, 128) + 128 + endSubIndex = min(endPC/31, 128) + 128 } else { endSubIndex = min(endPC/31-chunkNumber, 255) } From 5d26bf146b9a58f3a33acb96ddb3dc82c37f6446 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 7 May 2025 22:23:57 +0200 Subject: [PATCH 4/5] more test fixes --- core/state/access_witness.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/state/access_witness.go b/core/state/access_witness.go index 7f3caaa2863..3ae301d1e31 100644 --- a/core/state/access_witness.go +++ b/core/state/access_witness.go @@ -330,9 +330,9 @@ func (aw *AccessWitness) CodeChunksRangeGas(contractAddr []byte, startPC, size u var endSubIndex uint64 if chunkNumber < 128 { // special case of finding the upper boundary for the header group - endSubIndex = min(endPC/31, 128) + 128 + endSubIndex = min(endPC/31+128, 255) } else { - endSubIndex = min(endPC/31-chunkNumber, 255) + endSubIndex = min(endPC/31-chunkNumber+(chunkNumber+128)%256, 255) } treeIndex := *uint256.NewInt((chunkNumber + 128) / 256) @@ -344,7 +344,7 @@ func (aw *AccessWitness) CodeChunksRangeGas(contractAddr []byte, startPC, size u } // Find the next group boundary, taking the 128 offset into account. - if chunkNumber == 0 { + if chunkNumber < 128 { chunkNumber = 128 } else { chunkNumber += 256 From f17c0d456796fc78a8b856ab0f8f2c4819015f6c Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 8 May 2025 21:57:35 +0200 Subject: [PATCH 5/5] fix: only touch ext code hash if target isn't a precompile or system contract Signed-off-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- core/vm/instructions.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 5b9233d2216..02c3cb2fb6b 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -457,7 +457,12 @@ func opExtCodeHash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) slot := scope.Stack.peek() address := common.Address(slot.Bytes20()) if interpreter.evm.chainRules.IsEIP4762 { - interpreter.evm.Accesses.TouchCodeHash(address[:], false) + _, isPrecompile := interpreter.evm.precompile(address) + isSystemContract := interpreter.evm.isSystemContract(address) + + if !isPrecompile && !isSystemContract { + interpreter.evm.Accesses.TouchCodeHash(address[:], false) + } } if interpreter.evm.StateDB.Empty(address) { slot.Clear()