diff --git a/core/state/access_events.go b/core/state/access_events.go index b745c383b1..e9aa4910d2 100644 --- a/core/state/access_events.go +++ b/core/state/access_events.go @@ -18,6 +18,7 @@ package state import ( "maps" + gomath "math" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -92,97 +93,94 @@ func (ae *AccessEvents) Copy() *AccessEvents { // AddAccount returns the gas to be charged for each of the currently cold // member fields of an account. -func (ae *AccessEvents) AddAccount(addr common.Address, isWrite bool) uint64 { - var gas uint64 - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, isWrite) - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, isWrite) +func (ae *AccessEvents) AddAccount(addr common.Address, isWrite bool, availableGas uint64) uint64 { + var gas uint64 // accumulate the consumed gas + consumed, wanted := ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, isWrite, availableGas) + if consumed < wanted { + return wanted + } + gas += consumed + consumed, wanted = ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, isWrite, availableGas-consumed) + if consumed < wanted { + return wanted + gas + } + gas += wanted return gas } // MessageCallGas returns the gas to be charged for each of the currently // cold member fields of an account, that need to be touched when making a message // call to that account. -func (ae *AccessEvents) MessageCallGas(destination common.Address) uint64 { - var gas uint64 - gas += ae.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.BasicDataLeafKey, false) - return gas +func (ae *AccessEvents) MessageCallGas(destination common.Address, availableGas uint64) uint64 { + _, wanted := ae.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.BasicDataLeafKey, false, availableGas) + if wanted == 0 { + wanted = params.WarmStorageReadCostEIP2929 + } + return wanted } // ValueTransferGas returns the gas to be charged for each of the currently // cold balance member fields of the caller and the callee accounts. -func (ae *AccessEvents) ValueTransferGas(callerAddr, targetAddr common.Address) uint64 { - var gas uint64 - gas += ae.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BasicDataLeafKey, true) - gas += ae.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BasicDataLeafKey, true) - return gas +func (ae *AccessEvents) ValueTransferGas(callerAddr, targetAddr common.Address, availableGas uint64) uint64 { + _, wanted1 := ae.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BasicDataLeafKey, true, availableGas) + if wanted1 > availableGas { + return wanted1 + } + _, wanted2 := ae.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BasicDataLeafKey, true, availableGas-wanted1) + if wanted1+wanted2 > availableGas { + return params.WarmStorageReadCostEIP2929 + } + return wanted1 + wanted2 } // ContractCreatePreCheckGas 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 (ae *AccessEvents) ContractCreatePreCheckGas(addr common.Address) uint64 { - var gas uint64 - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, false) - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, false) - return gas +func (ae *AccessEvents) ContractCreatePreCheckGas(addr common.Address, availableGas uint64) uint64 { + consumed, wanted1 := ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, false, availableGas) + _, wanted2 := ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, false, availableGas-consumed) + return wanted1 + wanted2 } // ContractCreateInitGas returns the access gas costs for the initialization of // a contract creation. -func (ae *AccessEvents) ContractCreateInitGas(addr common.Address) uint64 { +func (ae *AccessEvents) ContractCreateInitGas(addr common.Address, availableGas uint64) (uint64, uint64) { var gas uint64 - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, true) - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, true) - return gas + consumed, wanted1 := ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, true, availableGas) + gas += consumed + consumed, wanted2 := ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, true, availableGas-consumed) + gas += consumed + return gas, wanted1 + wanted2 } // AddTxOrigin adds the member fields of the sender account to the access event list, // so that cold accesses are not charged, since they are covered by the 21000 gas. func (ae *AccessEvents) AddTxOrigin(originAddr common.Address) { - ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.BasicDataLeafKey, true) - ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.CodeHashLeafKey, false) + ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.BasicDataLeafKey, true, gomath.MaxUint64) + ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.CodeHashLeafKey, false, gomath.MaxUint64) } // AddTxDestination adds the member fields of the sender account to the access event list, // so that cold accesses are not charged, since they are covered by the 21000 gas. -func (ae *AccessEvents) AddTxDestination(addr common.Address, sendsValue bool) { - ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, sendsValue) - ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, false) +func (ae *AccessEvents) AddTxDestination(addr common.Address, sendsValue, doesntExist bool) { + ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, sendsValue, gomath.MaxUint64) + ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, doesntExist, gomath.MaxUint64) } // SlotGas returns the amount of gas to be charged for a cold storage access. -func (ae *AccessEvents) SlotGas(addr common.Address, slot common.Hash, isWrite bool) uint64 { +func (ae *AccessEvents) SlotGas(addr common.Address, slot common.Hash, isWrite bool, availableGas uint64, chargeWarmCosts bool) uint64 { treeIndex, subIndex := utils.StorageIndex(slot.Bytes()) - return ae.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite) + _, wanted := ae.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite, availableGas) + if wanted == 0 && chargeWarmCosts { + wanted = params.WarmStorageReadCostEIP2929 + } + return wanted } // touchAddressAndChargeGas adds any missing access event to the access event list, and returns the cold // access cost to be charged, if need be. -func (ae *AccessEvents) touchAddressAndChargeGas(addr common.Address, treeIndex uint256.Int, subIndex byte, isWrite bool) uint64 { - stemRead, selectorRead, stemWrite, selectorWrite, selectorFill := ae.touchAddress(addr, treeIndex, subIndex, isWrite) - - var gas uint64 - if stemRead { - gas += params.WitnessBranchReadCost - } - if selectorRead { - gas += params.WitnessChunkReadCost - } - if stemWrite { - gas += params.WitnessBranchWriteCost - } - if selectorWrite { - gas += params.WitnessChunkWriteCost - } - if selectorFill { - gas += params.WitnessChunkFillCost - } - return gas -} - -// touchAddress adds any missing access event to the access event list. -func (ae *AccessEvents) touchAddress(addr common.Address, treeIndex uint256.Int, subIndex byte, isWrite bool) (bool, bool, bool, bool, bool) { +func (ae *AccessEvents) touchAddressAndChargeGas(addr common.Address, treeIndex uint256.Int, subIndex byte, isWrite bool, availableGas uint64) (uint64, uint64) { branchKey := newBranchAccessKey(addr, treeIndex) chunkKey := newChunkAccessKey(branchKey, subIndex) @@ -190,11 +188,9 @@ func (ae *AccessEvents) touchAddress(addr common.Address, treeIndex uint256.Int, var branchRead, chunkRead bool if _, hasStem := ae.branches[branchKey]; !hasStem { branchRead = true - ae.branches[branchKey] = AccessWitnessReadFlag } if _, hasSelector := ae.chunks[chunkKey]; !hasSelector { chunkRead = true - ae.chunks[chunkKey] = AccessWitnessReadFlag } // Write access. @@ -202,17 +198,51 @@ func (ae *AccessEvents) touchAddress(addr common.Address, treeIndex uint256.Int, if isWrite { if (ae.branches[branchKey] & AccessWitnessWriteFlag) == 0 { branchWrite = true - ae.branches[branchKey] |= AccessWitnessWriteFlag } chunkValue := ae.chunks[chunkKey] if (chunkValue & AccessWitnessWriteFlag) == 0 { chunkWrite = true - ae.chunks[chunkKey] |= AccessWitnessWriteFlag } - // TODO: charge chunk filling costs if the leaf was previously empty in the state } - return branchRead, chunkRead, branchWrite, chunkWrite, chunkFill + + 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 { + ae.branches[branchKey] = AccessWitnessReadFlag + } + if branchWrite { + ae.branches[branchKey] |= AccessWitnessWriteFlag + } + if chunkRead { + ae.chunks[chunkKey] = AccessWitnessReadFlag + } + if chunkWrite { + ae.chunks[chunkKey] |= AccessWitnessWriteFlag + } + + // consumed == wanted + return gas, gas } type branchAccessKey struct { @@ -240,7 +270,7 @@ func newChunkAccessKey(branchKey branchAccessKey, leafKey byte) chunkAccessKey { } // CodeChunksRangeGas is a helper function to touch every chunk in a code range and charge witness gas costs -func (ae *AccessEvents) CodeChunksRangeGas(contractAddr common.Address, startPC, size uint64, codeLen uint64, isWrite bool) uint64 { +func (ae *AccessEvents) CodeChunksRangeGas(contractAddr common.Address, startPC, size uint64, codeLen uint64, isWrite bool, availableGas uint64) (uint64, uint64) { // 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 @@ -248,7 +278,7 @@ func (ae *AccessEvents) CodeChunksRangeGas(contractAddr common.Address, startPC, // is already in the AccessWitness so a stateless verifier can see that // the code from the last leaf is not needed. if (codeLen == 0 && size == 0) || startPC > codeLen { - return 0 + return 0, 0 } endPC := startPC + size @@ -263,22 +293,34 @@ func (ae *AccessEvents) CodeChunksRangeGas(contractAddr common.Address, startPC, for chunkNumber := startPC / 31; chunkNumber <= endPC/31; chunkNumber++ { treeIndex := *uint256.NewInt((chunkNumber + 128) / 256) subIndex := byte((chunkNumber + 128) % 256) - gas := ae.touchAddressAndChargeGas(contractAddr, treeIndex, subIndex, isWrite) + consumed, wanted := ae.touchAddressAndChargeGas(contractAddr, treeIndex, subIndex, isWrite, availableGas) + // did we OOG ? + if wanted > consumed { + return statelessGasCharged + consumed, statelessGasCharged + wanted + } var overflow bool - statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, gas) + statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, consumed) if overflow { panic("overflow when adding gas") } + availableGas -= consumed } - return statelessGasCharged + return statelessGasCharged, statelessGasCharged } // BasicDataGas adds the account's basic data to the accessed data, and returns the // amount of gas that it costs. // Note that an access in write mode implies an access in read mode, whereas an // access in read mode does not imply an access in write mode. -func (ae *AccessEvents) BasicDataGas(addr common.Address, isWrite bool) uint64 { - return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, isWrite) +func (ae *AccessEvents) BasicDataGas(addr common.Address, isWrite bool, availableGas uint64, chargeWarmCosts bool) uint64 { + _, wanted := ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BasicDataLeafKey, isWrite, availableGas) + if wanted == 0 && chargeWarmCosts { + if availableGas < params.WarmStorageReadCostEIP2929 { + return availableGas + } + wanted = params.WarmStorageReadCostEIP2929 + } + return wanted } // CodeHashGas adds the account's code hash to the accessed data, and returns the @@ -286,6 +328,13 @@ func (ae *AccessEvents) BasicDataGas(addr common.Address, isWrite bool) uint64 { // in write mode. If false, the charged gas corresponds to an access in read mode. // Note that an access in write mode implies an access in read mode, whereas an access in // read mode does not imply an access in write mode. -func (ae *AccessEvents) CodeHashGas(addr common.Address, isWrite bool) uint64 { - return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, isWrite) +func (ae *AccessEvents) CodeHashGas(addr common.Address, isWrite bool, availableGas uint64, chargeWarmCosts bool) uint64 { + _, wanted := ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, isWrite, availableGas) + if wanted == 0 && chargeWarmCosts { + if availableGas < params.WarmStorageReadCostEIP2929 { + return availableGas + } + wanted = params.WarmStorageReadCostEIP2929 + } + return wanted } diff --git a/core/state/access_events_test.go b/core/state/access_events_test.go index 10630b3181..5e1fee767c 100644 --- a/core/state/access_events_test.go +++ b/core/state/access_events_test.go @@ -17,6 +17,7 @@ package state import ( + "math" "testing" "github.com/ethereum/go-ethereum/common" @@ -40,50 +41,50 @@ func TestAccountHeaderGas(t *testing.T) { ae := NewAccessEvents(utils.NewPointCache(1024)) // Check cold read cost - gas := ae.BasicDataGas(testAddr, false) + gas := ae.BasicDataGas(testAddr, false, math.MaxUint64, false) if want := params.WitnessBranchReadCost + params.WitnessChunkReadCost; gas != want { t.Fatalf("incorrect gas computed, got %d, want %d", gas, want) } // Check warm read cost - gas = ae.BasicDataGas(testAddr, false) + gas = ae.BasicDataGas(testAddr, false, math.MaxUint64, false) if gas != 0 { t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0) } // Check cold read costs in the same group no longer incur the branch read cost - gas = ae.CodeHashGas(testAddr, false) + gas = ae.CodeHashGas(testAddr, false, math.MaxUint64, false) if gas != params.WitnessChunkReadCost { t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessChunkReadCost) } // Check cold write cost - gas = ae.BasicDataGas(testAddr, true) + gas = ae.BasicDataGas(testAddr, true, math.MaxUint64, false) if want := params.WitnessBranchWriteCost + params.WitnessChunkWriteCost; gas != want { t.Fatalf("incorrect gas computed, got %d, want %d", gas, want) } // Check warm write cost - gas = ae.BasicDataGas(testAddr, true) + gas = ae.BasicDataGas(testAddr, true, math.MaxUint64, false) if gas != 0 { t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0) } // Check a write without a read charges both read and write costs - gas = ae.BasicDataGas(testAddr2, true) + gas = ae.BasicDataGas(testAddr2, true, math.MaxUint64, false) if want := params.WitnessBranchReadCost + params.WitnessBranchWriteCost + params.WitnessChunkWriteCost + params.WitnessChunkReadCost; gas != want { t.Fatalf("incorrect gas computed, got %d, want %d", gas, want) } // Check that a write followed by a read charges nothing - gas = ae.BasicDataGas(testAddr2, false) + gas = ae.BasicDataGas(testAddr2, false, math.MaxUint64, false) if gas != 0 { t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0) } // Check that reading a slot from the account header only charges the // chunk read cost. - gas = ae.SlotGas(testAddr, common.Hash{}, false) + gas = ae.SlotGas(testAddr, common.Hash{}, false, math.MaxUint64, false) if gas != params.WitnessChunkReadCost { t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessChunkReadCost) } @@ -100,13 +101,13 @@ func TestContractCreateInitGas(t *testing.T) { } // Check cold read cost, without a value - gas := ae.ContractCreateInitGas(testAddr) + gas, _ := ae.ContractCreateInitGas(testAddr, math.MaxUint64) if want := params.WitnessBranchWriteCost + params.WitnessBranchReadCost + 2*params.WitnessChunkWriteCost + 2*params.WitnessChunkReadCost; gas != want { t.Fatalf("incorrect gas computed, got %d, want %d", gas, want) } // Check warm read cost - gas = ae.ContractCreateInitGas(testAddr) + gas, _ = ae.ContractCreateInitGas(testAddr, math.MaxUint64) if gas != 0 { t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0) } @@ -118,24 +119,24 @@ func TestMessageCallGas(t *testing.T) { ae := NewAccessEvents(utils.NewPointCache(1024)) // Check cold read cost, without a value - gas := ae.MessageCallGas(testAddr) + gas := ae.MessageCallGas(testAddr, math.MaxUint64) if want := params.WitnessBranchReadCost + params.WitnessChunkReadCost; gas != want { t.Fatalf("incorrect gas computed, got %d, want %d", gas, want) } // Check that reading the basic data and code hash of the same account does not incur the branch read cost - gas = ae.BasicDataGas(testAddr, false) + gas = ae.BasicDataGas(testAddr, false, math.MaxUint64, false) if gas != 0 { t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0) } - gas = ae.CodeHashGas(testAddr, false) + gas = ae.CodeHashGas(testAddr, false, math.MaxUint64, false) if gas != params.WitnessChunkReadCost { t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0) } // Check warm read cost - gas = ae.MessageCallGas(testAddr) - if gas != 0 { - t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0) + gas = ae.MessageCallGas(testAddr, math.MaxUint64) + if gas != params.WarmStorageReadCostEIP2929 { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WarmStorageReadCostEIP2929) } } diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 26d0217099..4329d968bc 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -157,6 +157,10 @@ func (s *hookedStateDB) Witness() *stateless.Witness { return s.inner.Witness() } +func (s *hookedStateDB) AccessEvents() *AccessEvents { + return s.inner.AccessEvents() +} + func (s *hookedStateDB) SubBalance(addr common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) uint256.Int { prev := s.inner.SubBalance(addr, amount, reason) if s.hooks.OnBalanceChange != nil && !amount.IsZero() { diff --git a/core/state_transition.go b/core/state_transition.go index ea7e3df2ff..7ad81bd92a 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -419,7 +419,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { st.evm.AccessEvents.AddTxOrigin(msg.From) if targetAddr := msg.To; targetAddr != nil { - st.evm.AccessEvents.AddTxDestination(*targetAddr, msg.Value.Sign() != 0) + st.evm.AccessEvents.AddTxDestination(*targetAddr, msg.Value.Sign() != 0, !st.state.Exist(*targetAddr)) } } @@ -482,7 +482,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // add the coinbase to the witness iff the fee is greater than 0 if rules.IsEIP4762 && fee.Sign() != 0 { - st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true) + st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true, math.MaxUint64) } } diff --git a/core/vm/eips.go b/core/vm/eips.go index 71d51f81ef..c6b5ea95f9 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -342,9 +342,9 @@ func opExtCodeCopyEIP4762(pc *uint64, interpreter *EVMInterpreter, scope *ScopeC self: AccountRef(addr), } paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64()) - statelessGas := interpreter.evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false) - if !scope.Contract.UseGas(statelessGas, interpreter.evm.Config.Tracer, tracing.GasChangeUnspecified) { - scope.Contract.Gas = 0 + consumed, wanted := interpreter.evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false, scope.Contract.Gas) + scope.Contract.UseGas(consumed, interpreter.evm.Config.Tracer, tracing.GasChangeUnspecified) + if consumed < wanted { return nil, ErrOutOfGas } scope.Memory.Set(memOffset.Uint64(), length.Uint64(), paddedCodeCopy) @@ -368,9 +368,9 @@ func opPush1EIP4762(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext // touch next chunk if PUSH1 is at the boundary. if so, *pc has // advanced past this boundary. contractAddr := scope.Contract.Address() - statelessGas := interpreter.evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false) - if !scope.Contract.UseGas(statelessGas, interpreter.evm.Config.Tracer, tracing.GasChangeUnspecified) { - scope.Contract.Gas = 0 + consumed, wanted := interpreter.evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) + scope.Contract.UseGas(wanted, interpreter.evm.Config.Tracer, tracing.GasChangeUnspecified) + if consumed < wanted { return nil, ErrOutOfGas } } @@ -396,9 +396,9 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc { if !scope.Contract.IsDeployment { contractAddr := scope.Contract.Address() - statelessGas := interpreter.evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false) - if !scope.Contract.UseGas(statelessGas, interpreter.evm.Config.Tracer, tracing.GasChangeUnspecified) { - scope.Contract.Gas = 0 + consumed, wanted := interpreter.evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) + scope.Contract.UseGas(consumed, interpreter.evm.Config.Tracer, tracing.GasChangeUnspecified) + if consumed < wanted { return nil, ErrOutOfGas } } diff --git a/core/vm/evm.go b/core/vm/evm.go index 07e4a272fa..761b0a7caa 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -45,6 +45,16 @@ func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) { return p, ok } +func (evm *EVM) isSystemContract(addr common.Address) bool { + switch addr { + case params.BeaconRootsAddress, params.HistoryStorageAddress, params.WithdrawalQueueAddress, + params.ConsolidationQueueAddress, params.SystemAddress: + return true + default: + return false + } +} + // BlockContext provides the EVM with auxiliary information. Once provided // it shouldn't be modified. type BlockContext struct { @@ -195,8 +205,14 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP4762 { - // add proof of absence to witness - wgas := evm.AccessEvents.AddAccount(addr, false) + // Add proof of absence to witness + // At this point, the read costs have already been charged, either because this + // is a direct tx call, in which case it's covered by the intrinsic gas, or because + // of a CALL instruction, in which case BASIC_DATA has been added to the access + // 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.AccessEvents.CodeHashGas(addr, true, gas, false) if gas < wgas { evm.StateDB.RevertToSnapshot(snapshot) return nil, 0, ErrOutOfGas @@ -443,7 +459,7 @@ 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.AccessEvents.ContractCreatePreCheckGas(address) + statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas) if statelessGas > gas { return nil, common.Address{}, 0, ErrOutOfGas } @@ -491,14 +507,14 @@ 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.AccessEvents.ContractCreateInitGas(address) - if statelessGas > gas { + consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas) + if consumed < wanted { return nil, common.Address{}, 0, ErrOutOfGas } if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas, gas-statelessGas, tracing.GasChangeWitnessContractInit) + evm.Config.Tracer.OnGasChange(gas, gas-consumed, tracing.GasChangeWitnessContractInit) } - gas = gas - statelessGas + gas = gas - consumed } evm.Context.Transfer(evm.StateDB, caller.Address(), address, value) @@ -542,7 +558,9 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address, valu return ret, ErrCodeStoreOutOfGas } } else { - if len(ret) > 0 && !contract.UseGas(evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true), evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) { + consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas) + contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) + if len(ret) > 0 && (consumed < wanted) { return ret, ErrCodeStoreOutOfGas } } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index f3a9808251..f711aa4a18 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -394,14 +394,7 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { return 0, ErrGasUintOverflow } - if evm.chainRules.IsEIP4762 { - if transfersValue { - gas, overflow = math.SafeAdd(gas, evm.AccessEvents.ValueTransferGas(contract.Address(), address)) - if overflow { - return 0, ErrGasUintOverflow - } - } - } + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) if err != nil { return 0, err @@ -428,16 +421,6 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { return 0, ErrGasUintOverflow } - if evm.chainRules.IsEIP4762 { - address := common.Address(stack.Back(1).Bytes20()) - transfersValue := !stack.Back(2).IsZero() - if transfersValue { - gas, overflow = math.SafeAdd(gas, evm.AccessEvents.ValueTransferGas(contract.Address(), address)) - if overflow { - return 0, ErrGasUintOverflow - } - } - } evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) if err != nil { return 0, err diff --git a/core/vm/interface.go b/core/vm/interface.go index 9229f4d2cd..39f645c067 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -20,6 +20,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" @@ -96,6 +97,8 @@ type StateDB interface { Witness() *stateless.Witness + AccessEvents() *state.AccessEvents + // Finalise must be invoked at the end of a transaction Finalise(bool) } diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index c408994401..8ed55f0181 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -235,7 +235,11 @@ 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. contractAddr := contract.Address() - contract.Gas -= in.evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false) + consumed, wanted := in.evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas) + contract.UseGas(consumed, in.evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) + if consumed < wanted { + return nil, ErrOutOfGas + } } // 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 3492994778..8bdc42eb21 100644 --- a/core/vm/operations_verkle.go +++ b/core/vm/operations_verkle.go @@ -25,28 +25,16 @@ import ( ) func gasSStore4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - gas := evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), true) - if gas == 0 { - gas = params.WarmStorageReadCostEIP2929 - } - return gas, nil + return evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), true, contract.Gas, true), nil } func gasSLoad4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - gas := evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), false) - if gas == 0 { - gas = params.WarmStorageReadCostEIP2929 - } - return gas, nil + return evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), false, contract.Gas, true), nil } func gasBalance4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { address := stack.peek().Bytes20() - gas := evm.AccessEvents.BasicDataGas(address, false) - if gas == 0 { - gas = params.WarmStorageReadCostEIP2929 - } - return gas, nil + return evm.AccessEvents.BasicDataGas(address, false, contract.Gas, true), nil } func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { @@ -54,11 +42,7 @@ func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if _, isPrecompile := evm.precompile(address); isPrecompile { return 0, nil } - gas := evm.AccessEvents.BasicDataGas(address, false) - if gas == 0 { - gas = params.WarmStorageReadCostEIP2929 - } - return gas, nil + return evm.AccessEvents.BasicDataGas(address, false, contract.Gas, true), nil } func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { @@ -66,35 +50,61 @@ func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if _, isPrecompile := evm.precompile(address); isPrecompile { return 0, nil } - gas := evm.AccessEvents.CodeHashGas(address, false) - if gas == 0 { - gas = params.WarmStorageReadCostEIP2929 - } - return gas, nil + return evm.AccessEvents.CodeHashGas(address, false, contract.Gas, true), nil } -func makeCallVariantGasEIP4762(oldCalculator gasFunc) gasFunc { +func makeCallVariantGasEIP4762(oldCalculator gasFunc, withTransferCosts bool) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - gas, err := oldCalculator(evm, contract, stack, mem, memorySize) - if err != nil { - return 0, err - } - if _, isPrecompile := evm.precompile(contract.Address()); isPrecompile { - return gas, nil - } - witnessGas := evm.AccessEvents.MessageCallGas(contract.Address()) - if witnessGas == 0 { + var ( + target = common.Address(stack.Back(1).Bytes20()) + witnessGas uint64 + _, isPrecompile = evm.precompile(target) + isSystemContract = evm.isSystemContract(target) + ) + + // 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.AccessEvents.ValueTransferGas(contract.Address(), target, contract.Gas) + if wantedValueTransferWitnessGas > contract.Gas { + return wantedValueTransferWitnessGas, nil + } + witnessGas = wantedValueTransferWitnessGas + } else if isPrecompile || isSystemContract { witnessGas = params.WarmStorageReadCostEIP2929 + } else { + // The charging for the value transfer is done BEFORE subtracting + // the 1/64th gas, as this is considered part of the CALL instruction. + // (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.AccessEvents.MessageCallGas(target, contract.Gas-witnessGas) + var overflow bool + if witnessGas, overflow = math.SafeAdd(witnessGas, wantedMessageCallWitnessGas); overflow { + return 0, ErrGasUintOverflow + } + if witnessGas > contract.Gas { + return witnessGas, nil + } } - return witnessGas + gas, nil + + contract.Gas -= witnessGas + // if the operation fails, adds witness gas to the gas before returning the error + gas, err := oldCalculator(evm, contract, stack, mem, memorySize) + contract.Gas += witnessGas // restore witness gas so that it can be charged at the callsite + var overflow bool + if gas, overflow = math.SafeAdd(gas, witnessGas); overflow { + return 0, ErrGasUintOverflow + } + return gas, err } } var ( - gasCallEIP4762 = makeCallVariantGasEIP4762(gasCall) - gasCallCodeEIP4762 = makeCallVariantGasEIP4762(gasCallCode) - gasStaticCallEIP4762 = makeCallVariantGasEIP4762(gasStaticCall) - gasDelegateCallEIP4762 = makeCallVariantGasEIP4762(gasDelegateCall) + gasCallEIP4762 = makeCallVariantGasEIP4762(gasCall, true) + gasCallCodeEIP4762 = makeCallVariantGasEIP4762(gasCallCode, false) + gasStaticCallEIP4762 = makeCallVariantGasEIP4762(gasStaticCall, false) + gasDelegateCallEIP4762 = makeCallVariantGasEIP4762(gasDelegateCall, false) ) func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { @@ -103,15 +113,44 @@ func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Mem return 0, nil } contractAddr := contract.Address() - statelessGas := evm.AccessEvents.BasicDataGas(contractAddr, false) + wanted := evm.AccessEvents.BasicDataGas(contractAddr, false, contract.Gas, false) + if wanted > contract.Gas { + return wanted, nil + } + statelessGas := wanted + balanceIsZero := evm.StateDB.GetBalance(contractAddr).Sign() == 0 + _, isPrecompile := evm.precompile(beneficiaryAddr) + isSystemContract := evm.isSystemContract(beneficiaryAddr) + + if (isPrecompile || isSystemContract) && balanceIsZero { + return statelessGas, nil + } + if contractAddr != beneficiaryAddr { - statelessGas += evm.AccessEvents.BasicDataGas(beneficiaryAddr, false) + wanted := evm.AccessEvents.BasicDataGas(beneficiaryAddr, false, contract.Gas-statelessGas, false) + if wanted > contract.Gas-statelessGas { + return statelessGas + wanted, nil + } + statelessGas += wanted } // Charge write costs if it transfers value - if evm.StateDB.GetBalance(contractAddr).Sign() != 0 { - statelessGas += evm.AccessEvents.BasicDataGas(contractAddr, true) + if !balanceIsZero { + wanted := evm.AccessEvents.BasicDataGas(contractAddr, true, contract.Gas-statelessGas, false) + if wanted > contract.Gas-statelessGas { + return statelessGas + wanted, nil + } + statelessGas += wanted + if contractAddr != beneficiaryAddr { - statelessGas += evm.AccessEvents.BasicDataGas(beneficiaryAddr, true) + if evm.StateDB.Exist(beneficiaryAddr) { + wanted = evm.AccessEvents.BasicDataGas(beneficiaryAddr, true, contract.Gas-statelessGas, false) + } else { + wanted = evm.AccessEvents.AddAccount(beneficiaryAddr, true, contract.Gas-statelessGas) + } + if wanted > contract.Gas-statelessGas { + return statelessGas + wanted, nil + } + statelessGas += wanted } } return statelessGas, nil @@ -122,17 +161,19 @@ func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if err != nil { return 0, err } - var ( - codeOffset = stack.Back(1) - length = stack.Back(2) - ) - uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow() - if overflow { - uint64CodeOffset = gomath.MaxUint64 - } - _, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(contract.Code, uint64CodeOffset, length.Uint64()) if !contract.IsDeployment { - gas += evm.AccessEvents.CodeChunksRangeGas(contract.Address(), copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false) + var ( + codeOffset = stack.Back(1) + length = stack.Back(2) + ) + uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow() + if overflow { + uint64CodeOffset = gomath.MaxUint64 + } + + _, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(contract.Code, uint64CodeOffset, length.Uint64()) + _, wanted := evm.AccessEvents.CodeChunksRangeGas(contract.Address(), copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false, contract.Gas-gas) + gas += wanted } return gas, nil } @@ -144,12 +185,17 @@ func gasExtCodeCopyEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Memo return 0, err } addr := common.Address(stack.peek().Bytes20()) - wgas := evm.AccessEvents.BasicDataGas(addr, false) - if wgas == 0 { - wgas = params.WarmStorageReadCostEIP2929 + isSystemContract := evm.isSystemContract(addr) + _, isPrecompile := evm.precompile(addr) + if isPrecompile || isSystemContract { + var overflow bool + if gas, overflow = math.SafeAdd(gas, params.WarmStorageReadCostEIP2929); overflow { + return 0, ErrGasUintOverflow + } + return gas, nil } + wgas := evm.AccessEvents.BasicDataGas(addr, false, contract.Gas-gas, true) var overflow bool - // We charge (cold-warm), since 'warm' is already charged as constantGas if gas, overflow = math.SafeAdd(gas, wgas); overflow { return 0, ErrGasUintOverflow }