diff --git a/core/state/access_events.go b/core/state/access_events.go index bc6064e96d..025237b339 100644 --- a/core/state/access_events.go +++ b/core/state/access_events.go @@ -57,44 +57,44 @@ func NewAccessEvents(pointCache *utils.PointCache) *AccessEvents { // Merge is used to merge the access events that were generated during the // execution of a tx, with the accumulation of all access events that were // generated during the execution of all txs preceding this one in a block. -func (aw *AccessEvents) Merge(other *AccessEvents) { +func (ae *AccessEvents) Merge(other *AccessEvents) { for k := range other.branches { - aw.branches[k] |= other.branches[k] + ae.branches[k] |= other.branches[k] } for k, chunk := range other.chunks { - aw.chunks[k] |= chunk + ae.chunks[k] |= chunk } } // Key returns, predictably, the list of keys that were touched during the // buildup of the access witness. -func (aw *AccessEvents) Keys() [][]byte { - // TODO: consider if parallelizing this is worth it, probably depending on len(aw.chunks). - keys := make([][]byte, 0, len(aw.chunks)) - for chunk := range aw.chunks { - basePoint := aw.pointCache.Get(chunk.addr[:]) +func (ae *AccessEvents) Keys() [][]byte { + // TODO: consider if parallelizing this is worth it, probably depending on len(ae.chunks). + keys := make([][]byte, 0, len(ae.chunks)) + for chunk := range ae.chunks { + basePoint := ae.pointCache.Get(chunk.addr[:]) key := utils.GetTreeKeyWithEvaluatedAddress(basePoint, &chunk.treeIndex, chunk.leafKey) keys = append(keys, key) } return keys } -func (aw *AccessEvents) Copy() *AccessEvents { - naw := &AccessEvents{ +func (ae *AccessEvents) Copy() *AccessEvents { + cpy := &AccessEvents{ branches: make(map[branchAccessKey]mode), chunks: make(map[chunkAccessKey]mode), - pointCache: aw.pointCache, + pointCache: ae.pointCache, } - naw.Merge(aw) - return naw + cpy.Merge(ae) + return cpy } // AddAccount returns the gas to be charged for each of the currently cold // member fields of an account. -func (aw *AccessEvents) AddAccount(addr []byte, isWrite bool) uint64 { +func (ae *AccessEvents) AddAccount(addr []byte, isWrite bool) uint64 { var gas uint64 for i := utils.VersionLeafKey; i <= utils.CodeSizeLeafKey; i++ { - gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, byte(i), isWrite) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, byte(i), isWrite) } return gas } @@ -102,60 +102,60 @@ func (aw *AccessEvents) AddAccount(addr []byte, isWrite bool) uint64 { // 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 (aw *AccessEvents) MessageCallGas(destination []byte) uint64 { +func (ae *AccessEvents) MessageCallGas(destination []byte) uint64 { var gas uint64 - gas += aw.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.VersionLeafKey, false) - gas += aw.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.CodeSizeLeafKey, false) + gas += ae.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.VersionLeafKey, false) + gas += ae.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.CodeSizeLeafKey, false) return gas } // ValueTransferGas returns the gas to be charged for each of the currently // cold balance member fields of the caller and the callee accounts. -func (aw *AccessEvents) ValueTransferGas(callerAddr, targetAddr []byte) uint64 { +func (ae *AccessEvents) ValueTransferGas(callerAddr, targetAddr []byte) uint64 { var gas uint64 - gas += aw.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BalanceLeafKey, true) - gas += aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, true) + gas += ae.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BalanceLeafKey, true) + gas += ae.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, true) return gas } // ContractCreateInitGas returns the access gas costs for the initialization of // a contract creation. -func (aw *AccessEvents) ContractCreateInitGas(addr []byte, createSendsValue bool) uint64 { +func (ae *AccessEvents) ContractCreateInitGas(addr []byte, createSendsValue bool) uint64 { var gas uint64 - gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, true) - gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, true) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, true) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, true) if createSendsValue { - gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, true) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, true) } return gas } // AddTxOrigin adds the member fields of the sender account to the witness, // so that cold accesses are not charged, since they are covered by the 21000 gas. -func (aw *AccessEvents) AddTxOrigin(originAddr []byte) { +func (ae *AccessEvents) AddTxOrigin(originAddr []byte) { for i := utils.VersionLeafKey; i <= utils.CodeSizeLeafKey; i++ { - aw.touchAddressAndChargeGas(originAddr, zeroTreeIndex, byte(i), i == utils.BalanceLeafKey || i == utils.NonceLeafKey) + ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, byte(i), i == utils.BalanceLeafKey || i == utils.NonceLeafKey) } } // AddTxDestination adds the member fields of the sender account to the witness, // so that cold accesses are not charged, since they are covered by the 21000 gas. -func (aw *AccessEvents) AddTxDestination(targetAddr []byte, sendsValue bool) { +func (ae *AccessEvents) AddTxDestination(targetAddr []byte, sendsValue bool) { for i := utils.VersionLeafKey; i <= utils.CodeSizeLeafKey; i++ { - aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, byte(i), i == utils.VersionLeafKey && sendsValue) + ae.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, byte(i), i == utils.VersionLeafKey && sendsValue) } } // SlotGas returns the amount of gas to be charged for a cold storage access. -func (aw *AccessEvents) SlotGas(addr []byte, slot common.Hash, isWrite bool) uint64 { +func (ae *AccessEvents) SlotGas(addr []byte, slot common.Hash, isWrite bool) uint64 { treeIndex, subIndex := utils.StorageIndex(slot.Bytes()) - return aw.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite) + return ae.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite) } // touchAddressAndChargeGas adds any missing access event to the witness, and returns the cold // access cost to be charged, if need be. -func (aw *AccessEvents) touchAddressAndChargeGas(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool) uint64 { - stemRead, selectorRead, stemWrite, selectorWrite, selectorFill := aw.touchAddress(addr, treeIndex, subIndex, isWrite) +func (ae *AccessEvents) touchAddressAndChargeGas(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool) uint64 { + stemRead, selectorRead, stemWrite, selectorWrite, selectorFill := ae.touchAddress(addr, treeIndex, subIndex, isWrite) var gas uint64 if stemRead { @@ -178,33 +178,33 @@ func (aw *AccessEvents) touchAddressAndChargeGas(addr []byte, treeIndex uint256. } // touchAddress adds any missing access event to the witness. -func (aw *AccessEvents) touchAddress(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool) (bool, bool, bool, bool, bool) { +func (ae *AccessEvents) touchAddress(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool) (bool, bool, bool, bool, bool) { branchKey := newBranchAccessKey(addr, treeIndex) chunkKey := newChunkAccessKey(branchKey, subIndex) // Read access. var branchRead, chunkRead bool - if _, hasStem := aw.branches[branchKey]; !hasStem { + if _, hasStem := ae.branches[branchKey]; !hasStem { branchRead = true - aw.branches[branchKey] = AccessWitnessReadFlag + ae.branches[branchKey] = AccessWitnessReadFlag } - if _, hasSelector := aw.chunks[chunkKey]; !hasSelector { + if _, hasSelector := ae.chunks[chunkKey]; !hasSelector { chunkRead = true - aw.chunks[chunkKey] = AccessWitnessReadFlag + ae.chunks[chunkKey] = AccessWitnessReadFlag } // Write access. var branchWrite, chunkWrite, chunkFill bool if isWrite { - if (aw.branches[branchKey] & AccessWitnessWriteFlag) == 0 { + if (ae.branches[branchKey] & AccessWitnessWriteFlag) == 0 { branchWrite = true - aw.branches[branchKey] |= AccessWitnessWriteFlag + ae.branches[branchKey] |= AccessWitnessWriteFlag } - chunkValue := aw.chunks[chunkKey] + chunkValue := ae.chunks[chunkKey] if (chunkValue & AccessWitnessWriteFlag) == 0 { chunkWrite = true - aw.chunks[chunkKey] |= AccessWitnessWriteFlag + ae.chunks[chunkKey] |= AccessWitnessWriteFlag } // TODO: charge chunk filling costs if the leaf was previously empty in the state @@ -238,7 +238,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 (aw *AccessEvents) CodeChunksRangeGas(contractAddr []byte, startPC, size uint64, codeLen uint64, isWrite bool) uint64 { +func (ae *AccessEvents) CodeChunksRangeGas(contractAddr []byte, startPC, size uint64, codeLen uint64, isWrite bool) 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 @@ -261,7 +261,7 @@ func (aw *AccessEvents) CodeChunksRangeGas(contractAddr []byte, startPC, size ui for chunkNumber := startPC / 31; chunkNumber <= endPC/31; chunkNumber++ { treeIndex := *uint256.NewInt((chunkNumber + 128) / 256) subIndex := byte((chunkNumber + 128) % 256) - gas := aw.touchAddressAndChargeGas(contractAddr, treeIndex, subIndex, isWrite) + gas := ae.touchAddressAndChargeGas(contractAddr, treeIndex, subIndex, isWrite) var overflow bool statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, gas) if overflow { @@ -272,22 +272,47 @@ func (aw *AccessEvents) CodeChunksRangeGas(contractAddr []byte, startPC, size ui return statelessGasCharged } -func (aw *AccessEvents) VersionGas(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite) +// VersionGas returns the amount of gas to be charged if the account's version hasn't been +// touched in the proper mode. If `isWrite` is `true` then the charged gas is for an access +// 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) VersionGas(addr []byte, isWrite bool) uint64 { + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite) } -func (aw *AccessEvents) BalanceGas(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, isWrite) +// BalanceGas returns the amount of gas to be charged if the account's balance hasn't been +// touched in the proper mode. If `isWrite` is `true` then the charged gas is for an access +// 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) BalanceGas(addr []byte, isWrite bool) uint64 { + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, isWrite) } -func (aw *AccessEvents) NonceGas(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite) +// NonceGas returns the amount of gas to be charged if the account's nonce hasn't been +// touched in the proper mode. If `isWrite` is `true` then the charged gas is for an access +// 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) NonceGas(addr []byte, isWrite bool) uint64 { + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite) } -func (aw *AccessEvents) CodeSizeGas(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite) +// CodeSizeGas returns the amount of gas to be charged if the account's code size hasn't been +// touched in the proper mode. If `isWrite` is `true` then the charged gas is for an access +// 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) CodeSizeGas(addr []byte, isWrite bool) uint64 { + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite) } -func (aw *AccessEvents) CodeHashGas(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeKeccakLeafKey, isWrite) +// CodeHashGas returns the amount of gas to be charged if the account's code hash hasn't been +// touched in the proper mode. If `isWrite` is `true` then the charged gas is for an access +// 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 []byte, isWrite bool) uint64 { + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeKeccakLeafKey, isWrite) } diff --git a/core/state/access_events_test.go b/core/state/access_events_test.go new file mode 100644 index 0000000000..e8cba9c0b9 --- /dev/null +++ b/core/state/access_events_test.go @@ -0,0 +1,154 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie/utils" +) + +var ( + testAddr [20]byte + testAddr2 [20]byte +) + +func init() { + for i := byte(0); i < 20; i++ { + testAddr[i] = i + testAddr[2] = 2 * i + } +} + +func TestAccountHeaderGas(t *testing.T) { + ae := NewAccessEvents(utils.NewPointCache(1024)) + + // Check cold read cost + gas := ae.VersionGas(testAddr[:], false) + if gas != params.WitnessBranchReadCost+params.WitnessChunkReadCost { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessBranchReadCost+params.WitnessChunkReadCost) + } + + // Check warm read cost + gas = ae.VersionGas(testAddr[:], 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.BalanceGas(testAddr[:], false) + if gas != params.WitnessChunkReadCost { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessChunkReadCost) + } + gas = ae.NonceGas(testAddr[:], false) + if gas != params.WitnessChunkReadCost { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessChunkReadCost) + } + gas = ae.CodeSizeGas(testAddr[:], false) + if gas != params.WitnessChunkReadCost { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessChunkReadCost) + } + gas = ae.CodeHashGas(testAddr[:], false) + if gas != params.WitnessChunkReadCost { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessChunkReadCost) + } + + // Check cold write cost + gas = ae.VersionGas(testAddr[:], true) + if gas != params.WitnessBranchWriteCost+params.WitnessChunkWriteCost { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessBranchReadCost+params.WitnessBranchWriteCost) + } + + // Check warm write cost + gas = ae.VersionGas(testAddr[:], true) + 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.BalanceGas(testAddr2[:], true) + if gas != params.WitnessBranchReadCost+params.WitnessBranchWriteCost+params.WitnessChunkWriteCost+params.WitnessChunkReadCost { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessBranchReadCost+params.WitnessBranchWriteCost+params.WitnessChunkWriteCost+params.WitnessChunkReadCost) + + } + + // Check that a write followed by a read charges nothing + gas = ae.BalanceGas(testAddr2[:], 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) + if gas != params.WitnessChunkReadCost { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessChunkReadCost) + } +} + +// TestContractCreateInitGas checks that the gas cost of contract creation is correctly +// calculated. +func TestContractCreateInitGas(t *testing.T) { + ae := NewAccessEvents(utils.NewPointCache(1024)) + + var testAddr [20]byte + for i := byte(0); i < 20; i++ { + testAddr[i] = i + } + + // Check cold read cost, without a value + gas := ae.ContractCreateInitGas(testAddr[:], false) + if gas != params.WitnessBranchWriteCost+params.WitnessBranchReadCost+params.WitnessChunkWriteCost*2+params.WitnessChunkReadCost*2 { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessBranchWriteCost+params.WitnessBranchReadCost+params.WitnessChunkWriteCost*3) + } + + // Check warm read cost + gas = ae.ContractCreateInitGas(testAddr[:], false) + if gas != 0 { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0) + } +} + +// TestMessageCallGas checks that the gas cost of message calls is correctly +// calculated. +func TestMessageCallGas(t *testing.T) { + ae := NewAccessEvents(utils.NewPointCache(1024)) + + // Check cold read cost, without a value + gas := ae.MessageCallGas(testAddr[:]) + if gas != params.WitnessBranchReadCost+params.WitnessChunkReadCost*2 { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessBranchReadCost+params.WitnessChunkReadCost*2) + } + + // Check that reading the version and code size of the same account does not incur the branch read cost + gas = ae.VersionGas(testAddr[:], false) + if gas != 0 { + t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0) + } + gas = ae.CodeSizeGas(testAddr[:], false) + if gas != 0 { + 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) + } +} diff --git a/core/vm/operations_verkle.go b/core/vm/operations_verkle.go index 2ded65950b..37932522f2 100644 --- a/core/vm/operations_verkle.go +++ b/core/vm/operations_verkle.go @@ -52,12 +52,12 @@ func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if _, isPrecompile := evm.precompile(address); isPrecompile { return 0, nil } - wgas := evm.AccessEvents.VersionGas(address[:], false) - wgas += evm.AccessEvents.CodeSizeGas(address[:], false) - if wgas == 0 { - wgas = params.WarmStorageReadCostEIP2929 + gas := evm.AccessEvents.VersionGas(address[:], false) + gas += evm.AccessEvents.CodeSizeGas(address[:], false) + if gas == 0 { + gas = params.WarmStorageReadCostEIP2929 } - return wgas, nil + return gas, nil } func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { @@ -65,11 +65,11 @@ func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if _, isPrecompile := evm.precompile(address); isPrecompile { return 0, nil } - codehashgas := evm.AccessEvents.CodeHashGas(address[:], false) - if codehashgas == 0 { - codehashgas = params.WarmStorageReadCostEIP2929 + gas := evm.AccessEvents.CodeHashGas(address[:], false) + if gas == 0 { + gas = params.WarmStorageReadCostEIP2929 } - return codehashgas, nil + return gas, nil } func makeCallVariantGasEIP4762(oldCalculator gasFunc) gasFunc { @@ -81,11 +81,11 @@ func makeCallVariantGasEIP4762(oldCalculator gasFunc) gasFunc { if _, isPrecompile := evm.precompile(contract.Address()); isPrecompile { return gas, nil } - wgas := evm.AccessEvents.MessageCallGas(contract.Address().Bytes()) - if wgas == 0 { - wgas = params.WarmStorageReadCostEIP2929 + witnessGas := evm.AccessEvents.MessageCallGas(contract.Address().Bytes()) + if witnessGas == 0 { + witnessGas = params.WarmStorageReadCostEIP2929 } - return wgas + gas, nil + return witnessGas + gas, nil } }