-
Notifications
You must be signed in to change notification settings - Fork 20.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cmd, core, params, trie: add verkle access witness gas charging (#29338)
Implements some of the changes required to charge and do gas accounting in verkle testnet.
- Loading branch information
Showing
31 changed files
with
1,082 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,320 @@ | ||
// 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 <http://www.gnu.org/licenses/>. | ||
|
||
package state | ||
|
||
import ( | ||
"maps" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/common/math" | ||
"github.com/ethereum/go-ethereum/params" | ||
"github.com/ethereum/go-ethereum/trie/utils" | ||
"github.com/holiman/uint256" | ||
) | ||
|
||
// mode specifies how a tree location has been accessed | ||
// for the byte value: | ||
// * the first bit is set if the branch has been edited | ||
// * the second bit is set if the branch has been read | ||
type mode byte | ||
|
||
const ( | ||
AccessWitnessReadFlag = mode(1) | ||
AccessWitnessWriteFlag = mode(2) | ||
) | ||
|
||
var zeroTreeIndex uint256.Int | ||
|
||
// AccessEvents lists the locations of the state that are being accessed | ||
// during the production of a block. | ||
type AccessEvents struct { | ||
branches map[branchAccessKey]mode | ||
chunks map[chunkAccessKey]mode | ||
|
||
pointCache *utils.PointCache | ||
} | ||
|
||
func NewAccessEvents(pointCache *utils.PointCache) *AccessEvents { | ||
return &AccessEvents{ | ||
branches: make(map[branchAccessKey]mode), | ||
chunks: make(map[chunkAccessKey]mode), | ||
pointCache: pointCache, | ||
} | ||
} | ||
|
||
// 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 (ae *AccessEvents) Merge(other *AccessEvents) { | ||
for k := range other.branches { | ||
ae.branches[k] |= other.branches[k] | ||
} | ||
for k, chunk := range other.chunks { | ||
ae.chunks[k] |= chunk | ||
} | ||
} | ||
|
||
// Keys returns, predictably, the list of keys that were touched during the | ||
// buildup of the access witness. | ||
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 (ae *AccessEvents) Copy() *AccessEvents { | ||
cpy := &AccessEvents{ | ||
branches: maps.Clone(ae.branches), | ||
chunks: maps.Clone(ae.chunks), | ||
pointCache: ae.pointCache, | ||
} | ||
return cpy | ||
} | ||
|
||
// 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.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) | ||
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.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 (ae *AccessEvents) ValueTransferGas(callerAddr, targetAddr common.Address) uint64 { | ||
var gas uint64 | ||
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 (ae *AccessEvents) ContractCreateInitGas(addr common.Address, createSendsValue bool) uint64 { | ||
var gas uint64 | ||
gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, true) | ||
gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, true) | ||
if createSendsValue { | ||
gas += ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, true) | ||
} | ||
return gas | ||
} | ||
|
||
// 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) | ||
} | ||
|
||
// 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) | ||
} | ||
|
||
// 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 { | ||
treeIndex, subIndex := utils.StorageIndex(slot.Bytes()) | ||
return ae.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite) | ||
} | ||
|
||
// 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) { | ||
branchKey := newBranchAccessKey(addr, treeIndex) | ||
chunkKey := newChunkAccessKey(branchKey, subIndex) | ||
|
||
// Read access. | ||
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. | ||
var branchWrite, chunkWrite, chunkFill bool | ||
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 | ||
} | ||
|
||
type branchAccessKey struct { | ||
addr common.Address | ||
treeIndex uint256.Int | ||
} | ||
|
||
func newBranchAccessKey(addr common.Address, treeIndex uint256.Int) branchAccessKey { | ||
var sk branchAccessKey | ||
sk.addr = addr | ||
sk.treeIndex = treeIndex | ||
return sk | ||
} | ||
|
||
type chunkAccessKey struct { | ||
branchAccessKey | ||
leafKey byte | ||
} | ||
|
||
func newChunkAccessKey(branchKey branchAccessKey, leafKey byte) chunkAccessKey { | ||
var lk chunkAccessKey | ||
lk.branchAccessKey = branchKey | ||
lk.leafKey = leafKey | ||
return lk | ||
} | ||
|
||
// 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 { | ||
// 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 (codeLen == 0 && size == 0) || startPC > codeLen { | ||
return 0 | ||
} | ||
|
||
endPC := startPC + size | ||
if endPC > codeLen { | ||
endPC = codeLen | ||
} | ||
if endPC > 0 { | ||
endPC -= 1 // endPC is the last bytecode that will be touched. | ||
} | ||
|
||
var statelessGasCharged uint64 | ||
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) | ||
var overflow bool | ||
statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, gas) | ||
if overflow { | ||
panic("overflow when adding gas") | ||
} | ||
} | ||
return statelessGasCharged | ||
} | ||
|
||
// VersionGas adds the account's version 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) VersionGas(addr common.Address, isWrite bool) uint64 { | ||
return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite) | ||
} | ||
|
||
// BalanceGas adds the account's balance to the accessed data, and returns the | ||
// amount of gas that it costs. | ||
// 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) | ||
} | ||
|
||
// NonceGas adds the account's nonce to the accessed data, and returns the | ||
// amount of gas that it costs. | ||
// 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 common.Address, isWrite bool) uint64 { | ||
return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite) | ||
} | ||
|
||
// CodeSizeGas adds the account's code size to the accessed data, and returns the | ||
// amount of gas that it costs. | ||
// 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 common.Address, isWrite bool) uint64 { | ||
return ae.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite) | ||
} | ||
|
||
// CodeHashGas adds the account's code hash to the accessed data, and returns the | ||
// amount of gas that it costs. | ||
// 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.CodeKeccakLeafKey, isWrite) | ||
} |
Oops, something went wrong.