From c3dfeeaac33571f19c76a5d7c1ed2d2bf443ecad Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:33:30 +0200 Subject: [PATCH 1/4] FILL_COST take 2 --- core/state/access_witness.go | 14 ++++++++++++-- core/state/statedb.go | 12 ++++++++++++ core/state_processor.go | 2 +- core/vm/instructions.go | 2 +- core/vm/interface.go | 2 ++ core/vm/operations_verkle.go | 8 ++++++-- 6 files changed, 34 insertions(+), 6 deletions(-) diff --git a/core/state/access_witness.go b/core/state/access_witness.go index 4c9d3218ad96..7ad97f3ec16c 100644 --- a/core/state/access_witness.go +++ b/core/state/access_witness.go @@ -42,6 +42,7 @@ var zeroTreeIndex uint256.Int type AccessWitness struct { branches map[branchAccessKey]mode chunks map[chunkAccessKey]mode + fills map[chunkAccessKey]struct{} pointCache *utils.PointCache } @@ -50,6 +51,7 @@ func NewAccessWitness(pointCache *utils.PointCache) *AccessWitness { return &AccessWitness{ branches: make(map[branchAccessKey]mode), chunks: make(map[chunkAccessKey]mode), + fills: make(map[chunkAccessKey]struct{}), pointCache: pointCache, } } @@ -64,6 +66,9 @@ func (aw *AccessWitness) Merge(other *AccessWitness) { for k, chunk := range other.chunks { aw.chunks[k] |= chunk } + for k := range other.fills { + aw.fills[k] = struct{}{} + } } // Key returns, predictably, the list of keys that were touched during the @@ -83,6 +88,7 @@ func (aw *AccessWitness) Copy() *AccessWitness { naw := &AccessWitness{ branches: make(map[branchAccessKey]mode), chunks: make(map[chunkAccessKey]mode), + fills: make(map[chunkAccessKey]struct{}), pointCache: aw.pointCache, } naw.Merge(aw) @@ -153,9 +159,13 @@ func (aw *AccessWitness) TouchTxExistingAndComputeGas(targetAddr []byte, sendsVa return 0 } -func (aw *AccessWitness) TouchSlotAndChargeGas(addr []byte, slot common.Hash, isWrite bool) uint64 { +func (aw *AccessWitness) TouchSlotAndChargeGas(addr []byte, slot common.Hash, isWrite, isFill bool) uint64 { treeIndex, subIndex := utils.GetTreeKeyStorageSlotTreeIndexes(slot.Bytes()) - return aw.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite) + gas := aw.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite) + if isFill { + gas += params.WitnessChunkFillCost + } + return gas } func (aw *AccessWitness) touchAddressAndChargeGas(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool) uint64 { diff --git a/core/state/statedb.go b/core/state/statedb.go index 4f64d19f4a3e..2144c15d9875 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -24,6 +24,7 @@ import ( "sort" "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" @@ -36,6 +37,7 @@ import ( "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" + "github.com/ethereum/go-ethereum/trie/utils" ) type revision struct { @@ -1467,3 +1469,13 @@ func copy2DSet[k comparable](set map[k]map[common.Hash][]byte) map[k]map[common. } return copied } + +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.GetTreeKeyStorageSlotWithEvaluatedAddress(s.witness.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) +} diff --git a/core/state_processor.go b/core/state_processor.go index d6a01673c6ab..29ab788f14ba 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -196,5 +196,5 @@ func ProcessParentBlockHash(statedb *state.StateDB, prevNumber uint64, prevHash var key common.Hash binary.BigEndian.PutUint64(key[24:], ringIndex) statedb.SetState(params.HistoryStorageAddress, key, prevHash) - statedb.Witness().TouchSlotAndChargeGas(params.HistoryStorageAddress[:], key, true) + statedb.Witness().TouchSlotAndChargeGas(params.HistoryStorageAddress[:], key, true, true /* noop */) } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 7a6db2e8eaaa..7d07a135bfca 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -462,7 +462,7 @@ func getBlockHashFromContract(number uint64, statedb StateDB, witness *state.Acc ringIndex := number % params.Eip2935BlockHashHistorySize var pnum common.Hash binary.BigEndian.PutUint64(pnum[24:], ringIndex) - statelessGas := witness.TouchSlotAndChargeGas(params.HistoryStorageAddress[:], pnum, false) + statelessGas := witness.TouchSlotAndChargeGas(params.HistoryStorageAddress[:], pnum, false, false) return statedb.GetState(params.HistoryStorageAddress, pnum), statelessGas } diff --git a/core/vm/interface.go b/core/vm/interface.go index 4241b1d45a77..5a87da5e0f3f 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -84,6 +84,8 @@ type StateDB interface { Witness() *state.AccessWitness SetWitness(*state.AccessWitness) + + 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 8b55b83bb21f..b1ac112bd81a 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.Accesses.TouchSlotAndChargeGas(contract.Address().Bytes(), common.Hash(stack.peek().Bytes32()), true) + var ( + addr = contract.Address() + slot = common.Hash(stack.peek().Bytes32()) + ) + gas := evm.Accesses.TouchSlotAndChargeGas(addr.Bytes(), 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.Accesses.TouchSlotAndChargeGas(contract.Address().Bytes(), common.Hash(stack.peek().Bytes32()), false) + gas := evm.Accesses.TouchSlotAndChargeGas(contract.Address().Bytes(), common.Hash(stack.peek().Bytes32()), false, false) if gas == 0 { gas = params.WarmStorageReadCostEIP2929 } From 8cd1e51b7d99709bdac3d8aa35aea72b42a70869 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 18 Jul 2024 18:24:19 +0200 Subject: [PATCH 2/4] add fills for contract creation --- consensus/beacon/consensus.go | 2 +- core/state/access_witness.go | 63 ++++++++++++++++++----------------- core/state_processor.go | 2 +- core/state_transition.go | 2 +- core/vm/evm.go | 4 +-- core/vm/operations_verkle.go | 2 +- 6 files changed, 38 insertions(+), 37 deletions(-) diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index e40c180aa421..1a318e5c64a9 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -358,7 +358,7 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. state.AddBalance(w.Address, amount) // The returned gas is not charged - state.Witness().TouchFullAccount(w.Address[:], true) + state.Witness().TouchFullAccount(w.Address[:], true, true /* noop */) } if chain.Config().IsPrague(header.Number, header.Time) { diff --git a/core/state/access_witness.go b/core/state/access_witness.go index 7ad97f3ec16c..8273f0182ee6 100644 --- a/core/state/access_witness.go +++ b/core/state/access_witness.go @@ -95,25 +95,25 @@ func (aw *AccessWitness) Copy() *AccessWitness { return naw } -func (aw *AccessWitness) TouchFullAccount(addr []byte, isWrite bool) uint64 { +func (aw *AccessWitness) TouchFullAccount(addr []byte, isWrite, isFill bool) uint64 { var gas uint64 for i := utils.VersionLeafKey; i <= utils.CodeSizeLeafKey; i++ { - gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, byte(i), isWrite) + gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, byte(i), isWrite, isFill) } return gas } func (aw *AccessWitness) TouchAndChargeMessageCall(addr []byte) uint64 { var gas uint64 - gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, false) - gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, false) + gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, false, false) + gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, false, false) return gas } func (aw *AccessWitness) TouchAndChargeValueTransfer(callerAddr, targetAddr []byte) uint64 { var gas uint64 - gas += aw.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BalanceLeafKey, true) - gas += aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, true) + gas += aw.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BalanceLeafKey, true, false) + gas += aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, true, false) return gas } @@ -121,17 +121,17 @@ func (aw *AccessWitness) TouchAndChargeValueTransfer(callerAddr, targetAddr []by // a contract creation func (aw *AccessWitness) TouchAndChargeContractCreateInit(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 += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, true, false) + gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, true, false) if createSendsValue { - gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, true) + gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, true, false) } return gas } func (aw *AccessWitness) TouchTxOriginAndComputeGas(originAddr []byte) uint64 { for i := utils.VersionLeafKey; i <= utils.CodeSizeLeafKey; i++ { - aw.touchAddressAndChargeGas(originAddr, zeroTreeIndex, byte(i), i == utils.BalanceLeafKey || i == utils.NonceLeafKey) + aw.touchAddressAndChargeGas(originAddr, zeroTreeIndex, byte(i), i == utils.BalanceLeafKey || i == utils.NonceLeafKey, false) } // Kaustinen note: we're currently experimenting with stop chargin gas for the origin address @@ -142,14 +142,14 @@ func (aw *AccessWitness) TouchTxOriginAndComputeGas(originAddr []byte) uint64 { } func (aw *AccessWitness) TouchTxExistingAndComputeGas(targetAddr []byte, sendsValue bool) uint64 { - aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.VersionLeafKey, false) - aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.CodeSizeLeafKey, false) - aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.CodeHashLeafKey, false) - aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.NonceLeafKey, false) + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.VersionLeafKey, false, false) + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.CodeSizeLeafKey, false, false) + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.CodeHashLeafKey, false, false) + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.NonceLeafKey, false, false) if sendsValue { - aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, true) + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, true, false) } else { - aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, false) + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, false, false) } // Kaustinen note: we're currently experimenting with stop chargin gas for the origin address @@ -161,15 +161,11 @@ func (aw *AccessWitness) TouchTxExistingAndComputeGas(targetAddr []byte, sendsVa func (aw *AccessWitness) TouchSlotAndChargeGas(addr []byte, slot common.Hash, isWrite, isFill bool) uint64 { treeIndex, subIndex := utils.GetTreeKeyStorageSlotTreeIndexes(slot.Bytes()) - gas := aw.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite) - if isFill { - gas += params.WitnessChunkFillCost - } - return gas + return aw.touchAddressAndChargeGas(addr, *treeIndex, subIndex, isWrite, isFill) } -func (aw *AccessWitness) touchAddressAndChargeGas(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool) uint64 { - stemRead, selectorRead, stemWrite, selectorWrite, selectorFill := aw.touchAddress(addr, treeIndex, subIndex, isWrite) +func (aw *AccessWitness) touchAddressAndChargeGas(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite, isFill bool) uint64 { + stemRead, selectorRead, stemWrite, selectorWrite, selectorFill := aw.touchAddress(addr, treeIndex, subIndex, isWrite, isFill) var gas uint64 if stemRead { @@ -192,7 +188,7 @@ func (aw *AccessWitness) touchAddressAndChargeGas(addr []byte, treeIndex uint256 } // touchAddress adds any missing access event to the witness. -func (aw *AccessWitness) touchAddress(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite bool) (bool, bool, bool, bool, bool) { +func (aw *AccessWitness) touchAddress(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite, isFill bool) (bool, bool, bool, bool, bool) { branchKey := newBranchAccessKey(addr, treeIndex) chunkKey := newChunkAccessKey(branchKey, subIndex) @@ -221,7 +217,11 @@ func (aw *AccessWitness) touchAddress(addr []byte, treeIndex uint256.Int, subInd aw.chunks[chunkKey] |= AccessWitnessWriteFlag } - // TODO: charge chunk filling costs if the leaf was previously empty in the state + _, ok := aw.fills[chunkKey] + if isFill && !ok { + chunkFill = true + aw.fills[chunkKey] = struct{}{} + } } return branchRead, chunkRead, branchWrite, chunkWrite, chunkFill @@ -275,7 +275,8 @@ func (aw *AccessWitness) TouchCodeChunksRangeAndChargeGas(contractAddr []byte, s for chunkNumber := startPC / 31; chunkNumber <= endPC/31; chunkNumber++ { treeIndex := *uint256.NewInt((chunkNumber + 128) / 256) subIndex := byte((chunkNumber + 128) % 256) - gas := aw.touchAddressAndChargeGas(contractAddr, treeIndex, subIndex, isWrite) + // if the contract is written, then it's also a fill + gas := aw.touchAddressAndChargeGas(contractAddr, treeIndex, subIndex, isWrite, isWrite) var overflow bool statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, gas) if overflow { @@ -287,21 +288,21 @@ func (aw *AccessWitness) TouchCodeChunksRangeAndChargeGas(contractAddr []byte, s } func (aw *AccessWitness) TouchVersion(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite) + return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, isWrite, false) } func (aw *AccessWitness) TouchBalance(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, isWrite) + return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, isWrite, false) } func (aw *AccessWitness) TouchNonce(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite) + return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, isWrite, false) } func (aw *AccessWitness) TouchCodeSize(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite) + return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeSizeLeafKey, isWrite, false) } func (aw *AccessWitness) TouchCodeHash(addr []byte, isWrite bool) uint64 { - return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, isWrite) + return aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.CodeHashLeafKey, isWrite, false) } diff --git a/core/state_processor.go b/core/state_processor.go index 29ab788f14ba..2c7b5a66377f 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -182,7 +182,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo func InsertBlockHashHistoryAtEip2935Fork(statedb *state.StateDB, prevNumber uint64, prevHash common.Hash, chain consensus.ChainHeaderReader) { // Make sure that the historical contract is added to the witness - statedb.Witness().TouchFullAccount(params.HistoryStorageAddress[:], true) + statedb.Witness().TouchFullAccount(params.HistoryStorageAddress[:], true, true /* noop */) ancestor := chain.GetHeader(prevHash, prevNumber) for i := prevNumber; i > 0 && i >= prevNumber-params.Eip2935BlockHashHistorySize; i-- { diff --git a/core/state_transition.go b/core/state_transition.go index 0aa4a369018b..b3d93dfeb1f3 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -472,7 +472,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { // add the coinbase to the witness iff the fee is greater than 0 if rules.IsEIP4762 && fee.Sign() != 0 { - st.evm.Accesses.TouchFullAccount(st.evm.Context.Coinbase[:], true) + st.evm.Accesses.TouchFullAccount(st.evm.Context.Coinbase[:], true, true /* noop */) } } diff --git a/core/vm/evm.go b/core/vm/evm.go index e036d2661768..bab373332fd4 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -198,7 +198,7 @@ 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.Accesses.TouchFullAccount(addr.Bytes(), false) + wgas := evm.Accesses.TouchFullAccount(addr.Bytes(), false, false) if gas < wgas { evm.StateDB.RevertToSnapshot(snapshot) return nil, 0, ErrOutOfGas @@ -518,7 +518,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } } else { // Contract creation completed, touch the missing fields in the contract - if !contract.UseGas(evm.Accesses.TouchFullAccount(address.Bytes()[:], true)) { + if !contract.UseGas(evm.Accesses.TouchFullAccount(address.Bytes()[:], true, true)) { err = ErrCodeStoreOutOfGas } diff --git a/core/vm/operations_verkle.go b/core/vm/operations_verkle.go index b1ac112bd81a..f10c8756e1c4 100644 --- a/core/vm/operations_verkle.go +++ b/core/vm/operations_verkle.go @@ -105,7 +105,7 @@ func gasSelfdestructEIP4762(evm *EVM, contract *Contract, stack *Stack, mem *Mem if _, isPrecompile := evm.precompile(beneficiaryAddr); isPrecompile { return 0, nil } - + // SELFDESTRUCT is the only use case for which a FILL might happen contractAddr := contract.Address() statelessGas := evm.Accesses.TouchVersion(contractAddr[:], false) statelessGas += evm.Accesses.TouchCodeSize(contractAddr[:], false) From 639f71d215ac22e58601bcf6df0288119dbce0b5 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:54:24 +0200 Subject: [PATCH 3/4] fix: reuse the fills between txs --- core/chain_makers.go | 1 - core/state/access_witness.go | 4 ++-- core/state/statedb.go | 10 ++++++++-- core/state_processor.go | 2 +- core/vm/evm.go | 5 +---- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/core/chain_makers.go b/core/chain_makers.go index 1c232e6b6d92..afc0aec73ab2 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -443,7 +443,6 @@ func GenerateVerkleChain(config *params.ChainConfig, parent *types.Block, engine if err != nil { panic(fmt.Sprintf("could not find state for block %d: err=%v, parent root=%x", i, err, parent.Root())) } - statedb.NewAccessWitness() block, receipt := genblock(i, parent, statedb) blocks[i] = block receipts[i] = receipt diff --git a/core/state/access_witness.go b/core/state/access_witness.go index 8273f0182ee6..1691018aa733 100644 --- a/core/state/access_witness.go +++ b/core/state/access_witness.go @@ -47,11 +47,11 @@ type AccessWitness struct { pointCache *utils.PointCache } -func NewAccessWitness(pointCache *utils.PointCache) *AccessWitness { +func NewAccessWitness(pointCache *utils.PointCache, fills map[chunkAccessKey]struct{}) *AccessWitness { return &AccessWitness{ branches: make(map[branchAccessKey]mode), chunks: make(map[chunkAccessKey]mode), - fills: make(map[chunkAccessKey]struct{}), + fills: fills, pointCache: pointCache, } } diff --git a/core/state/statedb.go b/core/state/statedb.go index 2144c15d9875..23390364f07e 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -189,12 +189,18 @@ func (s *StateDB) Snaps() *snapshot.Tree { } func (s *StateDB) NewAccessWitness() *AccessWitness { - return NewAccessWitness(s.db.(*cachingDB).addrToPoint) + return NewAccessWitness(s.db.(*cachingDB).addrToPoint, make(map[chunkAccessKey]struct{})) } +// NewAccessWitnessWithFills does the same thing as NewAccessWitness, +// but reusing the state fills map so that the fill costs are not +// charged multiple times in the same block. +func (s *StateDB) NewAccessWitnessWithFills() *AccessWitness { + return NewAccessWitness(s.db.(*cachingDB).addrToPoint, s.NewAccessWitness().fills) +} func (s *StateDB) Witness() *AccessWitness { if s.witness == nil { - s.witness = s.NewAccessWitness() + panic("witness wasn't initialized") } return s.witness } diff --git a/core/state_processor.go b/core/state_processor.go index 2c7b5a66377f..501aa5577870 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -120,7 +120,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, error) { // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) - txContext.Accesses = statedb.NewAccessWitness() + txContext.Accesses = statedb.NewAccessWitnessWithFills() evm.Reset(txContext, statedb) // Apply the transaction to the current state (included in the env). diff --git a/core/vm/evm.go b/core/vm/evm.go index bab373332fd4..d1613c3c1bca 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -137,9 +137,6 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig chainConfig: chainConfig, chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time), } - if txCtx.Accesses == nil && chainConfig.IsPrague(blockCtx.BlockNumber, blockCtx.Time) { - evm.Accesses = evm.StateDB.(*state.StateDB).NewAccessWitness() - } evm.interpreter = NewEVMInterpreter(evm) return evm } @@ -148,7 +145,7 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig // This is not threadsafe and should only be done very cautiously. func (evm *EVM) Reset(txCtx TxContext, statedb StateDB) { if txCtx.Accesses == nil && evm.chainRules.IsPrague { - txCtx.Accesses = evm.StateDB.(*state.StateDB).NewAccessWitness() + txCtx.Accesses = evm.StateDB.(*state.StateDB).NewAccessWitnessWithFills() } evm.TxContext = txCtx evm.StateDB = statedb From 77cbb830258690b13f854806049fe0f2b90478d7 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:08:14 +0200 Subject: [PATCH 4/4] add test and fix some errors --- core/state/statedb.go | 5 +- core/state_processor_test.go | 97 ++++++++++++++++++++++++++++++++++++ ethdb/memorydb/memorydb.go | 8 +-- 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 23390364f07e..4a071643add2 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/core/state/snapshot" "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/metrics" "github.com/ethereum/go-ethereum/params" @@ -196,7 +197,7 @@ func (s *StateDB) NewAccessWitness() *AccessWitness { // but reusing the state fills map so that the fill costs are not // charged multiple times in the same block. func (s *StateDB) NewAccessWitnessWithFills() *AccessWitness { - return NewAccessWitness(s.db.(*cachingDB).addrToPoint, s.NewAccessWitness().fills) + return NewAccessWitness(s.db.(*cachingDB).addrToPoint, s.witness.fills) } func (s *StateDB) Witness() *AccessWitness { if s.witness == nil { @@ -1483,5 +1484,5 @@ func (s *StateDB) IsSlotFilled(addr common.Address, slot common.Hash) bool { // 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) + return errors.Is(pebble.ErrNotFound, err) || errors.Is(memorydb.ErrMemorydbNotFound, err) } diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 991f4e2de68e..c4a1a59e61d7 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -586,6 +586,103 @@ func TestProcessVerkle(t *testing.T) { } } +// TestProcessVerkleFillThenNoFill tests the case where a slot is filled in a first transaction, for which the +// CHUNK_EDIT costs are filled, and then the same slot is written to in a second transaction, for which the CHUNK_EDIT +// costs are not charged, provided the two transactions occur in the same block. +func TestProcessVerkleFillThenNoFill(t *testing.T) { + var ( + config = ¶ms.ChainConfig{ + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + Ethash: new(params.EthashConfig), + ShanghaiTime: u64(0), + PragueTime: u64(0), + TerminalTotalDifficulty: common.Big0, + TerminalTotalDifficultyPassed: true, + ProofInBlocks: true, + } + signer = types.LatestSigner(config) + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain + gendb = rawdb.NewMemoryDatabase() // Database for the block-generation code, they must be separate as they are path-based. + coinbase = common.HexToAddress("0x71562b71999873DB5b286dF957af199Ec94617F7") + gspec = &Genesis{ + Config: config, + Alloc: GenesisAlloc{ + coinbase: GenesisAccount{ + Balance: big.NewInt(1000000000000000000), // 1 ether + }, + common.Address{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}: GenesisAccount{ + // PUSH1 0, CALLDATALOAD, PUSH1 2, SSTORE + Code: []byte{0x60, 0, 0x35, 0x60, 2, 0x55}, + Balance: big.NewInt(1000000000000000000), + }, + }, + } + loggerCfg = &logger.Config{} + ) + + os.MkdirAll("output", 0755) + traceFile, err := os.Create("./output/traces.jsonl") + if err != nil { + t.Fatal(err) + } + + // Verkle trees use the snapshot, which must be enabled before the + // data is saved into the tree+database. + genesis := gspec.MustCommit(bcdb) + blockchain, _ := NewBlockChain(bcdb, nil, gspec, nil, beacon.New(ethash.NewFaker()), vm.Config{Tracer: logger.NewJSONLogger(loggerCfg, traceFile)}, nil, nil) + defer blockchain.Stop() + // Commit the genesis block to the block-generation database as it + // is now independent of the blockchain database. + gspec.MustCommit(gendb) + + blockGasUsagesExpected := params.TxGas + 216 /* intrinsic gas */ + 3*vm.GasFastestStep + params.WitnessChunkReadCost + params.WitnessChunkWriteCost + chain, _, _, _ := GenerateVerkleChain(gspec.Config, genesis, beacon.New(ethash.NewFaker()), gendb, 1, func(i int, gen *BlockGen) { + gen.SetPoS() + + // fill slot + tx, _ := types.SignTx(types.NewTransaction(0, common.Address{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, big.NewInt(999), blockGasUsagesExpected+params.WitnessChunkFillCost, big.NewInt(875000000), []byte{1}), signer, testKey) + gen.AddTx(tx) + // write but no fill + tx, _ = types.SignTx(types.NewTransaction(1, common.Address{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, big.NewInt(999), blockGasUsagesExpected, big.NewInt(875000000), []byte{2}), signer, testKey) + gen.AddTx(tx) + }) + + // check the proof for the last block + // err = trie.DeserializeAndVerifyVerkleProof(proofs[0], genesis.Root().Bytes(), chain[0].Root().Bytes(), keyvals[0]) + // if err != nil { + // t.Fatal(err) + // } + // t.Log("verfied verkle proof") + + endnum, err := blockchain.InsertChain(chain) + if err != nil { + t.Fatalf("block %d imported with error: %v", endnum, err) + } + + b := blockchain.GetBlockByNumber(1) + if b == nil { + t.Fatalf("expected block %d to be present in chain", 1) + } + if b.Hash() != chain[0].Hash() { + t.Fatalf("block #%d not found at expected height", b.NumberU64()) + } + if b.GasUsed() != 2*blockGasUsagesExpected+params.WitnessChunkFillCost { + t.Fatalf("expected block #%d txs to use %d, got %d\n", b.NumberU64(), 2*blockGasUsagesExpected, b.GasUsed()) + } +} + func TestProcessVerkleInvalidContractCreation(t *testing.T) { var ( config = ¶ms.ChainConfig{ diff --git a/ethdb/memorydb/memorydb.go b/ethdb/memorydb/memorydb.go index f9f74322b575..cd37c937bf4b 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") // errSnapshotReleased is returned if callers want to retrieve data from a // released snapshot. @@ -98,7 +98,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. @@ -377,7 +377,7 @@ func (snap *snapshot) Get(key []byte) ([]byte, error) { if entry, ok := snap.db[string(key)]; ok { return common.CopyBytes(entry), nil } - return nil, errMemorydbNotFound + return nil, ErrMemorydbNotFound } // Release releases associated resources. Release should always succeed and can