From 5f9e1a1e1102bc45deb8fa2b31c3c62df74cea03 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:17:59 +0200 Subject: [PATCH] implement FILL_COST --- core/state/access_events.go | 93 ++++++++++++++++++++---------------- core/state/statedb.go | 12 +++++ core/vm/interface.go | 2 + core/vm/operations_verkle.go | 20 ++++---- ethdb/memorydb/memorydb.go | 6 +-- 5 files changed, 81 insertions(+), 52 deletions(-) diff --git a/core/state/access_events.go b/core/state/access_events.go index 4b6c7c7e69..a8946fe931 100644 --- a/core/state/access_events.go +++ b/core/state/access_events.go @@ -44,14 +44,16 @@ var zeroTreeIndex uint256.Int type AccessEvents struct { branches map[branchAccessKey]mode chunks map[chunkAccessKey]mode + fills map[chunkAccessKey]struct{} pointCache *utils.PointCache } -func NewAccessEvents(pointCache *utils.PointCache) *AccessEvents { +func NewAccessEvents(pointCache *utils.PointCache, fills map[chunkAccessKey]struct{}) *AccessEvents { return &AccessEvents{ branches: make(map[branchAccessKey]mode), chunks: make(map[chunkAccessKey]mode), + fills: fills, pointCache: pointCache, } } @@ -66,6 +68,9 @@ func (ae *AccessEvents) Merge(other *AccessEvents) { for k, chunk := range other.chunks { ae.chunks[k] |= chunk } + for k := range other.fills { + ae.fills[k] = struct{}{} + } } // Keys returns, predictably, the list of keys that were touched during the @@ -85,6 +90,7 @@ func (ae *AccessEvents) Copy() *AccessEvents { cpy := &AccessEvents{ branches: maps.Clone(ae.branches), chunks: maps.Clone(ae.chunks), + fills: maps.Clone(ae.fills), pointCache: ae.pointCache, } return cpy @@ -92,13 +98,13 @@ 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 { +func (ae *AccessEvents) AddAccount(addr common.Address, isWrite, isFill bool) uint64 { var gas uint64 - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite) - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, isWrite) - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite) - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeKeccakLeafKey, isWrite) - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite, isFill) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, isWrite, isFill) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite, isFill) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeKeccakLeafKey, isWrite, isFill) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite, isFill) return gas } @@ -107,28 +113,28 @@ func (ae *AccessEvents) AddAccount(addr common.Address, isWrite bool) uint64 { // call to that account. func (ae *AccessEvents) MessageCallGas(destination common.Address) uint64 { var gas uint64 - gas += ae.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.VersionLeafKey, false) - gas += ae.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.CodeSizeLeafKey, false) + gas += ae.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.VersionLeafKey, false, false) + gas += ae.touchAddressAndChargeGas(destination, zeroTreeIndex, utils.CodeSizeLeafKey, false, 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 (ae *AccessEvents) ValueTransferGas(callerAddr, targetAddr common.Address) uint64 { +func (ae *AccessEvents) ValueTransferGas(callerAddr, targetAddr common.Address, isFill bool) uint64 { var gas uint64 - gas += ae.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BalanceLeafKey, true) - gas += ae.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, true) + gas += ae.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BalanceLeafKey, true, false) + gas += ae.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, true, isFill) return gas } // ContractCreateInitGas returns the access gas costs for the initialization of // a contract creation. -func (ae *AccessEvents) ContractCreateInitGas(addr common.Address, createSendsValue bool) uint64 { +func (ae *AccessEvents) ContractCreateInitGas(addr common.Address, createSendsValue, isFill bool) uint64 { var gas uint64 - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, true) - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, true) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, true, true) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, true, true) if createSendsValue { - gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, true) + gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, true, isFill) } return gas } @@ -136,33 +142,33 @@ func (ae *AccessEvents) ContractCreateInitGas(addr common.Address, createSendsVa // 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.VersionLeafKey, false) - ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.BalanceLeafKey, true) - ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.NonceLeafKey, true) - ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.CodeKeccakLeafKey, false) - ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.CodeSizeLeafKey, false) + ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.VersionLeafKey, false, false) + ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.BalanceLeafKey, true, false) + ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.NonceLeafKey, true, false) + ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.CodeKeccakLeafKey, false, false) + ae.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.CodeSizeLeafKey, false, false) } // 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.VersionLeafKey, false) - ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, sendsValue) - ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, false) - ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeKeccakLeafKey, false) - ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, false) +func (ae *AccessEvents) AddTxDestination(addr common.Address, sendsValue, isFill bool) { + ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, false, false) + ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, sendsValue, isFill) + ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, false, false) + ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeKeccakLeafKey, false, false) + ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, false, false) } // 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, isFill bool) uint64 { treeIndex, subIndex := utils.StorageIndex(slot.Bytes()) - return ae.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite) + return ae.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite, isFill) } // 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) +func (ae *AccessEvents) touchAddressAndChargeGas(addr common.Address, treeIndex uint256.Int, subIndex byte, isWrite, isFill bool) uint64 { + stemRead, selectorRead, stemWrite, selectorWrite, selectorFill := ae.touchAddress(addr, treeIndex, subIndex, isWrite, isFill) var gas uint64 if stemRead { @@ -184,7 +190,7 @@ func (ae *AccessEvents) touchAddressAndChargeGas(addr common.Address, treeIndex } // 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) touchAddress(addr common.Address, treeIndex uint256.Int, subIndex byte, isWrite, isFill bool) (bool, bool, bool, bool, bool) { branchKey := newBranchAccessKey(addr, treeIndex) chunkKey := newChunkAccessKey(branchKey, subIndex) @@ -212,7 +218,12 @@ func (ae *AccessEvents) touchAddress(addr common.Address, treeIndex uint256.Int, chunkWrite = true ae.chunks[chunkKey] |= AccessWitnessWriteFlag } - // TODO: charge chunk filling costs if the leaf was previously empty in the state + _, ok := ae.fills[chunkKey] + if isFill && !ok { + chunkFill = true + ae.fills[chunkKey] = struct{}{} + } + } return branchRead, chunkRead, branchWrite, chunkWrite, chunkFill } @@ -242,7 +253,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, isFill 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 @@ -265,7 +276,7 @@ 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) + gas := ae.touchAddressAndChargeGas(contractAddr, treeIndex, subIndex, isWrite, isFill) var overflow bool statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, gas) if overflow { @@ -280,7 +291,7 @@ func (ae *AccessEvents) CodeChunksRangeGas(contractAddr common.Address, startPC, // 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 common.Address, isWrite bool) uint64 { - return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite) + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite, false) } // BalanceGas adds the account's balance to the accessed data, and returns the @@ -288,8 +299,8 @@ func (ae *AccessEvents) VersionGas(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) BalanceGas(addr common.Address, isWrite bool) uint64 { - return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, isWrite) +func (ae *AccessEvents) BalanceGas(addr common.Address, isWrite, isFill bool) uint64 { + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, isWrite, isFill) } // NonceGas adds the account's nonce to the accessed data, and returns the @@ -298,7 +309,7 @@ func (ae *AccessEvents) BalanceGas(addr common.Address, isWrite bool) uint64 { // 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 common.Address, isWrite bool) uint64 { - return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite) + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite, false) } // CodeSizeGas adds the account's code size to the accessed data, and returns the @@ -307,7 +318,7 @@ func (ae *AccessEvents) NonceGas(addr common.Address, isWrite bool) uint64 { // 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 common.Address, isWrite bool) uint64 { - return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite) + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite, false) } // CodeHashGas adds the account's code hash to the accessed data, and returns the @@ -316,5 +327,5 @@ func (ae *AccessEvents) CodeSizeGas(addr common.Address, isWrite bool) uint64 { // 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.CodeKeccakLeafKey, isWrite) + return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeKeccakLeafKey, isWrite, false) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 641775b0bd..4594147f9b 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -28,6 +28,7 @@ import ( "sync/atomic" "time" + "github.com/cockroachdb/pebble" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/snapshot" @@ -35,6 +36,7 @@ import ( "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" @@ -1484,3 +1486,13 @@ func (s *StateDB) PointCache() *utils.PointCache { func (s *StateDB) Witness() *stateless.Witness { return s.witness } + +func (s *StateDB) IsSlotFilled(addr common.Address, slot common.Hash) bool { + // The snapshot can not be used, because it uses the old encoding where + // no difference is made between 0 and no data. + _, err := s.db.DiskDB().Get(utils.StorageSlotKeyWithEvaluatedAddress(s.accessList.pointCache.GetTreeKeyHeader(addr[:]), slot[:])) + // The error needs to be checked because we want to be future-proof + // and not rely on the length of the encoding, in case 0-values are + // somehow compressed later. + return errors.Is(pebble.ErrNotFound, err) || errors.Is(memorydb.ErrMemorydbNotFound, err) +} diff --git a/core/vm/interface.go b/core/vm/interface.go index 5f42643565..d280a3f919 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -90,6 +90,8 @@ type StateDB interface { AddPreimage(common.Hash, []byte) Witness() *stateless.Witness + + IsSlotFilled(common.Address, common.Hash) bool } // CallContext provides a basic interface for the EVM calling conventions. The EVM diff --git a/core/vm/operations_verkle.go b/core/vm/operations_verkle.go index 73eb05974d..5a0ef73a71 100644 --- a/core/vm/operations_verkle.go +++ b/core/vm/operations_verkle.go @@ -23,7 +23,11 @@ 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) + var ( + addr = contract.Address() + slot = common.Hash(stack.peek().Bytes32()) + ) + gas := evm.AccessEvents.SlotGas(addr, slot, true, evm.StateDB.IsSlotFilled(addr, slot)) if gas == 0 { gas = params.WarmStorageReadCostEIP2929 } @@ -31,7 +35,7 @@ func gasSStore4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo } func gasSLoad4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - gas := evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), false) + gas := evm.AccessEvents.SlotGas(contract.Address(), stack.peek().Bytes32(), false, false) if gas == 0 { gas = params.WarmStorageReadCostEIP2929 } @@ -40,7 +44,7 @@ func gasSLoad4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor func gasBalance4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { address := stack.peek().Bytes20() - gas := evm.AccessEvents.BalanceGas(address, false) + gas := evm.AccessEvents.BalanceGas(address, false, false) if gas == 0 { gas = params.WarmStorageReadCostEIP2929 } @@ -104,15 +108,15 @@ func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Mem contractAddr := contract.Address() statelessGas := evm.AccessEvents.VersionGas(contractAddr, false) statelessGas += evm.AccessEvents.CodeSizeGas(contractAddr, false) - statelessGas += evm.AccessEvents.BalanceGas(contractAddr, false) + statelessGas += evm.AccessEvents.BalanceGas(contractAddr, false, false) if contractAddr != beneficiaryAddr { - statelessGas += evm.AccessEvents.BalanceGas(beneficiaryAddr, false) + statelessGas += evm.AccessEvents.BalanceGas(beneficiaryAddr, false, false) } // Charge write costs if it transfers value if evm.StateDB.GetBalance(contractAddr).Sign() != 0 { - statelessGas += evm.AccessEvents.BalanceGas(contractAddr, true) + statelessGas += evm.AccessEvents.BalanceGas(contractAddr, true, false) if contractAddr != beneficiaryAddr { - statelessGas += evm.AccessEvents.BalanceGas(beneficiaryAddr, true) + statelessGas += evm.AccessEvents.BalanceGas(beneficiaryAddr, true, !evm.StateDB.Exist(beneficiaryAddr)) } } return statelessGas, nil @@ -133,7 +137,7 @@ func gasCodeCopyEip4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, } _, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(contract.Code, uint64CodeOffset, length.Uint64()) if !contract.IsDeployment { - gas += evm.AccessEvents.CodeChunksRangeGas(contract.Address(), copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false) + gas += evm.AccessEvents.CodeChunksRangeGas(contract.Address(), copyOffset, nonPaddedCopyLength, uint64(len(contract.Code)), false, false) } return gas, nil } diff --git a/ethdb/memorydb/memorydb.go b/ethdb/memorydb/memorydb.go index 532e0dfe3f..0fbe6545f2 100644 --- a/ethdb/memorydb/memorydb.go +++ b/ethdb/memorydb/memorydb.go @@ -32,9 +32,9 @@ var ( // invocation of a data access operation. errMemorydbClosed = errors.New("database closed") - // errMemorydbNotFound is returned if a key is requested that is not found in + // ErrMemorydbNotFound is returned if a key is requested that is not found in // the provided memory database. - errMemorydbNotFound = errors.New("not found") + ErrMemorydbNotFound = errors.New("not found") ) // Database is an ephemeral key-value store. Apart from basic data storage @@ -94,7 +94,7 @@ func (db *Database) Get(key []byte) ([]byte, error) { if entry, ok := db.db[string(key)]; ok { return common.CopyBytes(entry), nil } - return nil, errMemorydbNotFound + return nil, ErrMemorydbNotFound } // Put inserts the given value into the key-value store.