diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index a350e383a2..a99959e2ae 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -348,9 +348,9 @@ func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine and processes withdrawals on top. -func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) { +func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, statedb *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) { if !beacon.IsPoSHeader(header) { - beacon.ethone.Finalize(chain, header, state, txs, uncles, nil) + beacon.ethone.Finalize(chain, header, statedb, txs, uncles, nil) return } // Withdrawals processing. @@ -358,7 +358,11 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. // Convert amount from gwei to wei. amount := new(uint256.Int).SetUint64(w.Amount) amount = amount.Mul(amount, uint256.NewInt(params.GWei)) - state.AddBalance(w.Address, amount) + statedb.AddBalance(w.Address, amount) + + if chain.Config().IsVerkle(header.Number, header.Time) { + statedb.Witness().AddAddress(w.Address, state.ALAllItems, state.AccessListWrite) + } } // No block reward which is issued by consensus layer instead. } diff --git a/core/error.go b/core/error.go index 72cacf8c78..e8b7cc4c78 100644 --- a/core/error.go +++ b/core/error.go @@ -67,6 +67,10 @@ var ( // than init code size limit. ErrMaxInitCodeSizeExceeded = errors.New("max initcode size exceeded") + // ErrInsufficientBalanceWitness is returned if the transaction sender does not + // have enough funds to cover the witness access costs. + ErrInsufficientBalanceWitness = errors.New("insufficient funds to cover witness access costs for transaction") + // ErrInsufficientFunds is returned if the total cost of executing a transaction // is higher than the balance of the user's account. ErrInsufficientFunds = errors.New("insufficient funds for gas * price + value") diff --git a/core/state/access_list.go b/core/state/access_list.go index 4194691345..30e32ecfea 100644 --- a/core/state/access_list.go +++ b/core/state/access_list.go @@ -18,22 +18,62 @@ package state import ( "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) -type accessList struct { +type ALAccessMode bool + +var ( + AccessListRead = ALAccessMode(false) + AccessListWrite = ALAccessMode(true) +) + +type ALAccountItem uint64 + +const ( + ALVersion = ALAccountItem(1 << iota) + ALBalance + ALNonce + ALCodeHash + ALCodeSize + ALLastHeaderItem +) + +const ALAllItems = ALVersion | ALBalance | ALNonce | ALCodeSize | ALCodeHash + +type AccessList interface { + ContainsAddress(address common.Address) bool + Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) + Copy() AccessList + AddAddress(address common.Address, items ALAccountItem, isWrite ALAccessMode) uint64 + AddSlot(address common.Address, slot common.Hash, isWrite ALAccessMode) uint64 + DeleteSlot(address common.Address, slot common.Hash) + DeleteAddress(address common.Address) + + TouchAndChargeValueTransfer(callerAddr, targetAddr []byte) uint64 + TouchAndChargeContractCreateInit(addr []byte, createSendsValue bool) uint64 + TouchTxOriginAndComputeGas(originAddr []byte) uint64 + TouchTxExistingAndComputeGas(targetAddr []byte, sendsValue bool) uint64 + TouchAddressOnReadAndComputeGas(addr []byte, index uint256.Int, suffix byte) uint64 + Merge(AccessList) + Keys() [][]byte +} + +type accessList2929 struct { addresses map[common.Address]int slots []map[common.Hash]struct{} } // ContainsAddress returns true if the address is in the access list. -func (al *accessList) ContainsAddress(address common.Address) bool { +func (al *accessList2929) ContainsAddress(address common.Address) bool { _, ok := al.addresses[address] return ok } // Contains checks if a slot within an account is present in the access list, returning // separate flags for the presence of the account and the slot respectively. -func (al *accessList) Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { +func (al *accessList2929) Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { idx, ok := al.addresses[address] if !ok { // no such address (and hence zero slots) @@ -48,15 +88,15 @@ func (al *accessList) Contains(address common.Address, slot common.Hash) (addres } // newAccessList creates a new accessList. -func newAccessList() *accessList { - return &accessList{ +func newAccessList() AccessList { + return &accessList2929{ addresses: make(map[common.Address]int), } } // Copy creates an independent copy of an accessList. -func (a *accessList) Copy() *accessList { - cp := newAccessList() +func (a *accessList2929) Copy() AccessList { + cp := newAccessList().(*accessList2929) for k, v := range a.addresses { cp.addresses[k] = v } @@ -73,12 +113,12 @@ func (a *accessList) Copy() *accessList { // AddAddress adds an address to the access list, and returns 'true' if the operation // caused a change (addr was not previously in the list). -func (al *accessList) AddAddress(address common.Address) bool { +func (al *accessList2929) AddAddress(address common.Address, _ ALAccountItem, _ ALAccessMode) uint64 { if _, present := al.addresses[address]; present { - return false + return params.WarmStorageReadCostEIP2929 } al.addresses[address] = -1 - return true + return params.ColdAccountAccessCostEIP2929 } // AddSlot adds the specified (addr, slot) combo to the access list. @@ -86,31 +126,31 @@ func (al *accessList) AddAddress(address common.Address) bool { // - address added // - slot added // For any 'true' value returned, a corresponding journal entry must be made. -func (al *accessList) AddSlot(address common.Address, slot common.Hash) (addrChange bool, slotChange bool) { +func (al *accessList2929) AddSlot(address common.Address, slot common.Hash, _ ALAccessMode) (gas uint64) { idx, addrPresent := al.addresses[address] if !addrPresent || idx == -1 { // Address not present, or addr present but no slots there al.addresses[address] = len(al.slots) slotmap := map[common.Hash]struct{}{slot: {}} al.slots = append(al.slots, slotmap) - return !addrPresent, true + return params.WarmStorageReadCostEIP2929 } // There is already an (address,slot) mapping slotmap := al.slots[idx] if _, ok := slotmap[slot]; !ok { slotmap[slot] = struct{}{} // Journal add slot change - return false, true + return params.ColdSloadCostEIP2929 } // No changes required - return false, false + return params.WarmStorageReadCostEIP2929 } // DeleteSlot removes an (address, slot)-tuple from the access list. // This operation needs to be performed in the same order as the addition happened. // This method is meant to be used by the journal, which maintains ordering of // operations. -func (al *accessList) DeleteSlot(address common.Address, slot common.Hash) { +func (al *accessList2929) DeleteSlot(address common.Address, slot common.Hash) { idx, addrOk := al.addresses[address] // There are two ways this can fail if !addrOk { @@ -131,6 +171,29 @@ func (al *accessList) DeleteSlot(address common.Address, slot common.Hash) { // needs to be performed in the same order as the addition happened. // This method is meant to be used by the journal, which maintains ordering of // operations. -func (al *accessList) DeleteAddress(address common.Address) { +func (al *accessList2929) DeleteAddress(address common.Address) { delete(al.addresses, address) } + +func (al *accessList2929) TouchAndChargeValueTransfer(callerAddr []byte, targetAddr []byte) uint64 { + return 0 +} + +func (al *accessList2929) TouchAndChargeContractCreateInit(addr []byte, createSendsValue bool) uint64 { + return 0 +} + +func (al *accessList2929) TouchTxOriginAndComputeGas(originAddr []byte) uint64 { + return 0 +} + +func (al *accessList2929) TouchTxExistingAndComputeGas(targetAddr []byte, sendsValue bool) uint64 { + return 0 +} + +func (al *accessList2929) TouchAddressOnReadAndComputeGas(addr []byte, index uint256.Int, subIndex byte) uint64 { + return 0 +} + +func (al *accessList2929) Merge(other AccessList) {} +func (al *accessList2929) Keys() [][]byte { return nil } diff --git a/core/state/access_witness.go b/core/state/access_witness.go new file mode 100644 index 0000000000..07795952b2 --- /dev/null +++ b/core/state/access_witness.go @@ -0,0 +1,260 @@ +// 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 ( + "github.com/ethereum/go-ethereum/common" + "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 + +// AccessWitness lists the locations of the state that are being accessed +// during the production of a block. +type AccessWitness struct { + branches map[branchAccessKey]mode + chunks map[chunkAccessKey]mode + + pointCache *utils.PointCache +} + +func NewAccessWitness() *AccessWitness { + return &AccessWitness{ + branches: make(map[branchAccessKey]mode), + chunks: make(map[chunkAccessKey]mode), + pointCache: utils.NewPointCache(), + } +} + +// Merge is used to merge the witness that got generated during the execution +// of a tx, with the accumulation of witnesses that were generated during the +// execution of all the txs preceding this one in a given block. +func (aw *AccessWitness) Merge(o AccessList) { + other := o.(*AccessWitness) + for k := range other.branches { + aw.branches[k] |= other.branches[k] + } + for k, chunk := range other.chunks { + aw.chunks[k] |= chunk + } +} + +// Key returns, predictably, the list of keys that were touched during the +// buildup of the access witness. +func (aw *AccessWitness) 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.GetTreeKeyHeader(chunk.addr[:]) + key := utils.GetTreeKeyWithEvaluatedAddess(basePoint, &chunk.treeIndex, chunk.leafKey) + keys = append(keys, key) + } + return keys +} + +func (aw *AccessWitness) Copy() AccessList { + naw := &AccessWitness{ + branches: make(map[branchAccessKey]mode), + chunks: make(map[chunkAccessKey]mode), + pointCache: aw.pointCache, + } + naw.Merge(aw) + return naw +} + +func (aw *AccessWitness) TouchAndChargeValueTransfer(callerAddr, targetAddr []byte) uint64 { + var gas uint64 + gas += aw.touchAddressAndChargeGas(callerAddr, zeroTreeIndex, utils.BalanceLeafKey, AccessListWrite) + gas += aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, AccessListWrite) + return gas +} + +// TouchAndChargeContractCreateInit charges access costs to initiate +// a contract creation +func (aw *AccessWitness) TouchAndChargeContractCreateInit(addr []byte, createSendsValue bool) uint64 { + var gas uint64 + gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.VersionLeafKey, AccessListWrite) + gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.NonceLeafKey, AccessListWrite) + // FIXME note that this is incompatible with the spec + if createSendsValue { + gas += aw.touchAddressAndChargeGas(addr, zeroTreeIndex, utils.BalanceLeafKey, AccessListWrite) + } + return gas +} + +func (aw *AccessWitness) TouchTxOriginAndComputeGas(originAddr []byte) uint64 { + aw.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.VersionLeafKey, AccessListRead) + aw.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.CodeSizeLeafKey, AccessListRead) + aw.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.CodeKeccakLeafKey, AccessListRead) + aw.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.NonceLeafKey, AccessListWrite) + aw.touchAddressAndChargeGas(originAddr, zeroTreeIndex, utils.BalanceLeafKey, AccessListWrite) + + // Kaustinen note: we're currently experimenting with stop chargin gas for the origin address + // so simple transfer still take 21000 gas. This is to potentially avoid breaking existing tooling. + // This is the reason why we return 0 instead of `gas`. + // Note that we still have to touch the addresses to make sure the witness is correct. + return 0 +} + +func (aw *AccessWitness) TouchTxExistingAndComputeGas(targetAddr []byte, sendsValue bool) uint64 { + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.VersionLeafKey, AccessListRead) + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.CodeSizeLeafKey, AccessListRead) + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.CodeKeccakLeafKey, AccessListRead) + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.NonceLeafKey, AccessListRead) + if sendsValue { + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, AccessListWrite) + } else { + aw.touchAddressAndChargeGas(targetAddr, zeroTreeIndex, utils.BalanceLeafKey, AccessListRead) + } + + // Kaustinen note: we're currently experimenting with stop chargin gas for the origin address + // so simple transfer still take 21000 gas. This is to potentially avoid breaking existing tooling. + // This is the reason why we return 0 instead of `gas`. + // Note that we still have to touch the addresses to make sure the witness is correct. + return 0 +} + +func (aw *AccessWitness) TouchAddressOnReadAndComputeGas(addr []byte, treeIndex uint256.Int, subIndex byte) uint64 { + return aw.touchAddressAndChargeGas(addr, treeIndex, subIndex, AccessListRead) +} + +func (aw *AccessWitness) touchAddressAndChargeGas(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite ALAccessMode) uint64 { + stemRead, suffixRead, stemWrite, selectorWrite, selectorFill := aw.touchAddress(addr, treeIndex, subIndex, isWrite) + + var gas uint64 + if stemRead { + gas += params.WitnessBranchReadCost + } + if suffixRead { + 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 witness. +func (aw *AccessWitness) touchAddress(addr []byte, treeIndex uint256.Int, subIndex byte, isWrite ALAccessMode) (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 { + branchRead = true + aw.branches[branchKey] = AccessWitnessReadFlag + } + if _, hasSelector := aw.chunks[chunkKey]; !hasSelector { + chunkRead = true + aw.chunks[chunkKey] = AccessWitnessReadFlag + } + + // Write access. + var branchWrite, chunkWrite, chunkFill bool + if isWrite { + if (aw.branches[branchKey] & AccessWitnessWriteFlag) == 0 { + branchWrite = true + aw.branches[branchKey] |= AccessWitnessWriteFlag + } + + chunkValue := aw.chunks[chunkKey] + if (chunkValue & AccessWitnessWriteFlag) == 0 { + chunkWrite = true + aw.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 []byte, treeIndex uint256.Int) branchAccessKey { + var sk branchAccessKey + copy(sk.addr[20-len(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 +} + +func (aw *AccessWitness) AddAddress(addr common.Address, items ALAccountItem, isWrite ALAccessMode) uint64 { + var gas uint64 + for index := utils.VersionLeafKey; index <= utils.CodeSizeLeafKey; index++ { + if (1< 0 { s.journal.append(accessListAddAccountChange{&addr}) } + return gas } // AddSlotToAccessList adds the given (address, slot)-tuple to the access list -func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { - addrMod, slotMod := s.accessList.AddSlot(addr, slot) - if addrMod { - // In practice, this should not happen, since there is no way to enter the - // scope of 'address' without having the 'address' become already added - // to the access list (via call-variant, create, etc). - // Better safe than sorry, though - s.journal.append(accessListAddAccountChange{&addr}) - } - if slotMod { +func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash, isWrite ALAccessMode) uint64 { + gas := s.accessList.AddSlot(addr, slot, isWrite) + + if gas > params.WarmStorageReadCostEIP2929 { s.journal.append(accessListAddSlotChange{ address: &addr, slot: &slot, }) } -} - -// AddressInAccessList returns true if the given address is in the access list. -func (s *StateDB) AddressInAccessList(addr common.Address) bool { - return s.accessList.ContainsAddress(addr) -} - -// SlotInAccessList returns true if the given (address, slot)-tuple is in the access list. -func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { - return s.accessList.Contains(addr, slot) + return gas } // convertAccountSet converts a provided account set from address keyed to hash keyed. @@ -1370,3 +1364,45 @@ func copy2DSet[k comparable](set map[k]map[common.Hash][]byte) map[k]map[common. } return copied } + +func (s *StateDB) TouchTxOriginAndComputeGas(addr common.Address) uint64 { + return s.accessList.TouchTxOriginAndComputeGas(addr.Bytes()) +} + +func (s *StateDB) TouchTxExistingAndComputeGas(addr common.Address, sendsValue bool) uint64 { + return s.accessList.TouchTxExistingAndComputeGas(addr.Bytes(), sendsValue) +} + +func (s *StateDB) TouchAndChargeContractCreateInit(addr common.Address, sendsValue bool) uint64 { + return s.accessList.TouchAndChargeContractCreateInit(addr.Bytes(), sendsValue) +} + +func (s *StateDB) TouchAndChargeValueTransfer(from, to common.Address) uint64 { + return s.accessList.TouchAndChargeValueTransfer(from.Bytes(), to.Bytes()) +} + +func (s *StateDB) TouchAddressOnReadAndComputeGas(from common.Address, index uint64) uint64 { + treeIndex := uint256.NewInt((index + 128) / 256) + subIndex := byte((index + 128) % 256) + return s.accessList.TouchAddressOnReadAndComputeGas(from.Bytes(), *treeIndex, subIndex) +} + +func (s *StateDB) MergeTxAccessListIntoBlockWitness() { + if _, ok := s.accessList.(*AccessWitness); ok { + s.witness.Merge(s.accessList) + } +} + +// addressInAccessList returns true if the given address is in the access list. +func (s *StateDB) addressInAccessList(addr common.Address) bool { + return s.accessList.ContainsAddress(addr) +} + +// slotInAccessList returns true if the given (address, slot)-tuple is in the access list. +func (s *StateDB) slotInAccessList(addr common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { + return s.accessList.Contains(addr, slot) +} + +func (s *StateDB) Witness() AccessList { + return s.witness +} diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 3649b0ac58..58f1eff165 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -345,14 +345,14 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction { { name: "AddAddressToAccessList", fn: func(a testAction, s *StateDB) { - s.AddAddressToAccessList(addr) + s.AddAddressToAccessList(addr, 0, false) }, }, { name: "AddSlotToAccessList", fn: func(a testAction, s *StateDB) { s.AddSlotToAccessList(addr, - common.Hash{byte(a.args[0])}) + common.Hash{byte(a.args[0])}, false) }, args: make([]int64, 1), }, @@ -879,19 +879,19 @@ func TestStateDBAccessList(t *testing.T) { } // Check that the given addresses are in the access list for _, address := range addresses { - if !state.AddressInAccessList(address) { + if !state.addressInAccessList(address) { t.Fatalf("expected %x to be in access list", address) } } // Check that only the expected addresses are present in the access list - for address := range state.accessList.addresses { + for address := range state.accessList.(*accessList2929).addresses { if _, exist := addressMap[address]; !exist { t.Fatalf("extra address %x in access list", address) } } } verifySlots := func(addrString string, slotStrings ...string) { - if !state.AddressInAccessList(addr(addrString)) { + if !state.addressInAccessList(addr(addrString)) { t.Fatalf("scope missing address/slots %v", addrString) } var address = addr(addrString) @@ -905,14 +905,14 @@ func TestStateDBAccessList(t *testing.T) { } // Check that the expected items are in the access list for i, s := range slots { - if _, slotPresent := state.SlotInAccessList(address, s); !slotPresent { + if _, slotPresent := state.slotInAccessList(address, s); !slotPresent { t.Fatalf("input %d: scope missing slot %v (address %v)", i, s, addrString) } } // Check that no extra elements are in the access list - index := state.accessList.addresses[address] + index := state.accessList.(*accessList2929).addresses[address] if index >= 0 { - stateSlots := state.accessList.slots[index] + stateSlots := state.accessList.(*accessList2929).slots[index] for s := range stateSlots { if _, slotPresent := slotMap[s]; !slotPresent { t.Fatalf("scope has extra slot %v (address %v)", s, addrString) @@ -921,9 +921,9 @@ func TestStateDBAccessList(t *testing.T) { } } - state.AddAddressToAccessList(addr("aa")) // 1 - state.AddSlotToAccessList(addr("bb"), slot("01")) // 2,3 - state.AddSlotToAccessList(addr("bb"), slot("02")) // 4 + state.AddAddressToAccessList(addr("aa"), 0, false) // 1 + state.AddSlotToAccessList(addr("bb"), slot("01"), false) // 2,3 + state.AddSlotToAccessList(addr("bb"), slot("02"), false) // 4 verifyAddrs("aa", "bb") verifySlots("bb", "01", "02") @@ -934,17 +934,17 @@ func TestStateDBAccessList(t *testing.T) { } // same again, should cause no journal entries - state.AddSlotToAccessList(addr("bb"), slot("01")) - state.AddSlotToAccessList(addr("bb"), slot("02")) - state.AddAddressToAccessList(addr("aa")) + state.AddSlotToAccessList(addr("bb"), slot("01"), false) + state.AddSlotToAccessList(addr("bb"), slot("02"), false) + state.AddAddressToAccessList(addr("aa"), 0, false) if exp, got := 4, state.journal.length(); exp != got { t.Fatalf("journal length mismatch: have %d, want %d", got, exp) } // some new ones - state.AddSlotToAccessList(addr("bb"), slot("03")) // 5 - state.AddSlotToAccessList(addr("aa"), slot("01")) // 6 - state.AddSlotToAccessList(addr("cc"), slot("01")) // 7,8 - state.AddAddressToAccessList(addr("cc")) + state.AddSlotToAccessList(addr("bb"), slot("03"), false) // 5 + state.AddSlotToAccessList(addr("aa"), slot("01"), false) // 6 + state.AddSlotToAccessList(addr("cc"), slot("01"), false) // 7,8 + state.AddAddressToAccessList(addr("cc"), 0, false) if exp, got := 8, state.journal.length(); exp != got { t.Fatalf("journal length mismatch: have %d, want %d", got, exp) } @@ -956,7 +956,7 @@ func TestStateDBAccessList(t *testing.T) { // now start rolling back changes state.journal.revert(state, 7) - if _, ok := state.SlotInAccessList(addr("cc"), slot("01")); ok { + if _, ok := state.slotInAccessList(addr("cc"), slot("01")); ok { t.Fatalf("slot present, expected missing") } verifyAddrs("aa", "bb", "cc") @@ -964,7 +964,7 @@ func TestStateDBAccessList(t *testing.T) { verifySlots("bb", "01", "02", "03") state.journal.revert(state, 6) - if state.AddressInAccessList(addr("cc")) { + if state.addressInAccessList(addr("cc")) { t.Fatalf("addr present, expected missing") } verifyAddrs("aa", "bb") @@ -972,46 +972,46 @@ func TestStateDBAccessList(t *testing.T) { verifySlots("bb", "01", "02", "03") state.journal.revert(state, 5) - if _, ok := state.SlotInAccessList(addr("aa"), slot("01")); ok { + if _, ok := state.slotInAccessList(addr("aa"), slot("01")); ok { t.Fatalf("slot present, expected missing") } verifyAddrs("aa", "bb") verifySlots("bb", "01", "02", "03") state.journal.revert(state, 4) - if _, ok := state.SlotInAccessList(addr("bb"), slot("03")); ok { + if _, ok := state.slotInAccessList(addr("bb"), slot("03")); ok { t.Fatalf("slot present, expected missing") } verifyAddrs("aa", "bb") verifySlots("bb", "01", "02") state.journal.revert(state, 3) - if _, ok := state.SlotInAccessList(addr("bb"), slot("02")); ok { + if _, ok := state.slotInAccessList(addr("bb"), slot("02")); ok { t.Fatalf("slot present, expected missing") } verifyAddrs("aa", "bb") verifySlots("bb", "01") state.journal.revert(state, 2) - if _, ok := state.SlotInAccessList(addr("bb"), slot("01")); ok { + if _, ok := state.slotInAccessList(addr("bb"), slot("01")); ok { t.Fatalf("slot present, expected missing") } verifyAddrs("aa", "bb") state.journal.revert(state, 1) - if state.AddressInAccessList(addr("bb")) { + if state.addressInAccessList(addr("bb")) { t.Fatalf("addr present, expected missing") } verifyAddrs("aa") state.journal.revert(state, 0) - if state.AddressInAccessList(addr("aa")) { + if state.addressInAccessList(addr("aa")) { t.Fatalf("addr present, expected missing") } - if got, exp := len(state.accessList.addresses), 0; got != exp { + if got, exp := len(state.accessList.(*accessList2929).addresses), 0; got != exp { t.Fatalf("expected empty, got %d", got) } - if got, exp := len(state.accessList.slots), 0; got != exp { + if got, exp := len(state.accessList.(*accessList2929).slots), 0; got != exp { t.Fatalf("expected empty, got %d", got) } // Check the copy @@ -1019,10 +1019,10 @@ func TestStateDBAccessList(t *testing.T) { state = stateCopy1 verifyAddrs("aa", "bb") verifySlots("bb", "01", "02") - if got, exp := len(state.accessList.addresses), 2; got != exp { + if got, exp := len(state.accessList.(*accessList2929).addresses), 2; got != exp { t.Fatalf("expected empty, got %d", got) } - if got, exp := len(state.accessList.slots), 1; got != exp { + if got, exp := len(state.accessList.(*accessList2929).slots), 1; got != exp { t.Fatalf("expected empty, got %d", got) } } diff --git a/core/state_processor.go b/core/state_processor.go index 9e32ab4e56..520490a1f5 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -145,6 +145,8 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce()) } + statedb.MergeTxAccessListIntoBlockWitness() + // Set the receipt logs and create the bloom filter. receipt.Logs = statedb.GetLogs(tx.Hash(), blockNumber.Uint64(), blockHash) receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) @@ -185,7 +187,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, vmenv *vm.EVM, statedb *stat Data: beaconRoot[:], } vmenv.Reset(NewEVMTxContext(msg), statedb) - statedb.AddAddressToAccessList(params.BeaconRootsStorageAddress) + statedb.AddAddressToAccessList(params.BeaconRootsStorageAddress, state.ALAllItems, state.AccessListWrite) _, _, _ = vmenv.Call(vm.AccountRef(msg.From), *msg.To, msg.Data, 30_000_000, common.U2560) statedb.Finalise(true) } diff --git a/core/state_transition.go b/core/state_transition.go index 8fcf4c093d..378382c9c8 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -23,8 +23,10 @@ import ( "github.com/ethereum/go-ethereum/common" cmath "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -354,6 +356,19 @@ func (st *StateTransition) preCheck() error { return st.buyGas() } +// tryConsumeGas tries to subtract gas from gasPool, setting the result in gasPool +// if subtracting more gas than remains in gasPool, set gasPool = 0 and return false +// otherwise, do the subtraction setting the result in gasPool and return true +func tryConsumeGas(gasPool *uint64, gas uint64) bool { + if *gasPool < gas { + *gasPool = 0 + return false + } + + *gasPool -= gas + return true +} + // TransitionDb will transition the state by applying the current message and // returning the evm execution result with following fields. // @@ -404,6 +419,28 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { } st.gasRemaining -= gas + if rules.IsVerkle { + statelessGasOrigin := st.evm.StateDB.TouchTxOriginAndComputeGas(msg.From) + if !tryConsumeGas(&st.gasRemaining, statelessGasOrigin) { + return nil, fmt.Errorf("%w: Insufficient funds to cover witness access costs for transaction: have %d, want %d", ErrInsufficientBalanceWitness, st.gasRemaining, gas) + } + originNonce := st.evm.StateDB.GetNonce(msg.From) + if msg.To != nil { + statelessGasDest := st.evm.StateDB.TouchTxExistingAndComputeGas(*msg.To, msg.Value.Sign() != 0) + if !tryConsumeGas(&st.gasRemaining, statelessGasDest) { + return nil, fmt.Errorf("%w: Insufficient funds to cover witness access costs for transaction: have %d, want %d", ErrInsufficientBalanceWitness, st.gasRemaining, gas) + } + + // ensure the code size ends up in the access witness + st.evm.StateDB.GetCodeSize(*msg.To) + } else { + contractAddr := crypto.CreateAddress(msg.From, originNonce) + if !tryConsumeGas(&st.gasRemaining, st.evm.StateDB.TouchAndChargeContractCreateInit(contractAddr, msg.Value.Sign() != 0)) { + return nil, fmt.Errorf("%w: Insufficient funds to cover witness access costs for transaction: have %d, want %d", ErrInsufficientBalanceWitness, st.gasRemaining, gas) + } + } + } + // Check clause 6 value, overflow := uint256.FromBig(msg.Value) if overflow { @@ -457,6 +494,10 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { fee := new(uint256.Int).SetUint64(st.gasUsed()) fee.Mul(fee, effectiveTipU256) st.state.AddBalance(st.evm.Context.Coinbase, fee) + + if rules.IsVerkle && fee.Sign() != 0 { + st.evm.StateDB.AddAddressToAccessList(st.evm.Context.Coinbase, state.ALAllItems, state.AccessListWrite) + } } return &ExecutionResult{ diff --git a/core/vm/contract.go b/core/vm/contract.go index 16b669ebca..10a2126457 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -56,6 +56,8 @@ type Contract struct { CodeAddr *common.Address Input []byte + IsDeployment bool + Gas uint64 value *uint256.Int } diff --git a/core/vm/eips.go b/core/vm/eips.go index 9f06b2818f..9ccd8a19f5 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -37,6 +37,7 @@ var activators = map[int]func(*JumpTable){ 1884: enable1884, 1344: enable1344, 1153: enable1153, + 4762: enable4762, } // EnableEIP enables the given EIP on the config. @@ -123,28 +124,28 @@ func enable2929(jt *JumpTable) { jt[SLOAD].constantGas = 0 jt[SLOAD].dynamicGas = gasSLoadEIP2929 - jt[EXTCODECOPY].constantGas = params.WarmStorageReadCostEIP2929 + jt[EXTCODECOPY].constantGas = 0 jt[EXTCODECOPY].dynamicGas = gasExtCodeCopyEIP2929 - jt[EXTCODESIZE].constantGas = params.WarmStorageReadCostEIP2929 + jt[EXTCODESIZE].constantGas = 0 jt[EXTCODESIZE].dynamicGas = gasEip2929AccountCheck - jt[EXTCODEHASH].constantGas = params.WarmStorageReadCostEIP2929 + jt[EXTCODEHASH].constantGas = 0 jt[EXTCODEHASH].dynamicGas = gasEip2929AccountCheck - jt[BALANCE].constantGas = params.WarmStorageReadCostEIP2929 + jt[BALANCE].constantGas = 0 jt[BALANCE].dynamicGas = gasEip2929AccountCheck - jt[CALL].constantGas = params.WarmStorageReadCostEIP2929 + jt[CALL].constantGas = 0 jt[CALL].dynamicGas = gasCallEIP2929 - jt[CALLCODE].constantGas = params.WarmStorageReadCostEIP2929 + jt[CALLCODE].constantGas = 0 jt[CALLCODE].dynamicGas = gasCallCodeEIP2929 - jt[STATICCALL].constantGas = params.WarmStorageReadCostEIP2929 + jt[STATICCALL].constantGas = 0 jt[STATICCALL].dynamicGas = gasStaticCallEIP2929 - jt[DELEGATECALL].constantGas = params.WarmStorageReadCostEIP2929 + jt[DELEGATECALL].constantGas = 0 jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP2929 // This was previously part of the dynamic cost, but we're using it as a constantGas @@ -319,3 +320,14 @@ func enable6780(jt *JumpTable) { maxStack: maxStack(1, 0), } } + +func enable4762(jt *JumpTable) { + jt[SSTORE].dynamicGas = gasSStore4762 + jt[BALANCE].dynamicGas = gasBalance4762 + jt[EXTCODESIZE].dynamicGas = gasExtCodeSize4762 + jt[EXTCODEHASH].dynamicGas = gasExtCodeHash4762 + jt[CALL].dynamicGas = nil + jt[CALLCODE].dynamicGas = nil + jt[DELEGATECALL].dynamicGas = nil + jt[STATICCALL].dynamicGas = nil +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 16cc854908..e993405421 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -21,6 +21,7 @@ import ( "sync/atomic" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" @@ -189,6 +190,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas p, isPrecompile := evm.precompile(addr) debug := evm.Config.Tracer != nil + var creation bool if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { // Calling a non existing account, don't do anything, but ping the tracer @@ -201,9 +203,14 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas evm.Config.Tracer.CaptureExit(ret, 0, nil) } } + + if evm.chainRules.IsVerkle { + tryConsumeGas(&gas, evm.StateDB.AddAddressToAccessList(addr, state.ALAllItems, state.AccessListRead)) + } return nil, gas, nil } evm.StateDB.CreateAccount(addr) + creation = true } evm.Context.Transfer(evm.StateDB, caller.Address(), addr, value) @@ -237,6 +244,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas // The depth-check is already done, and precompiles handled above contract := NewContract(caller, AccountRef(addrCopy), value, gas) contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code) + contract.IsDeployment = creation ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -418,6 +426,20 @@ func (c *codeAndHash) Hash() common.Hash { return c.hash } +// tryConsumeGas tries to subtract gas from gasPool, setting the result in gasPool +// if subtracting more gas than remains in gasPool, set gasPool = 0 and return false +// otherwise, do the subtraction setting the result in gasPool and return true +func tryConsumeGas(gasPool *uint64, gas uint64) bool { + // TODO remove this function, it should no longer be needed + if *gasPool < gas { + *gasPool = 0 + return false + } + + *gasPool -= gas + return true +} + // create creates a new contract using code as deployment code. func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { // Depth check execution. Fail if we're trying to execute above the @@ -436,7 +458,10 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // We add this to the access list _before_ taking a snapshot. Even if the creation fails, // the access-list change should not be rolled back if evm.chainRules.IsBerlin { - evm.StateDB.AddAddressToAccessList(address) + evm.StateDB.AddAddressToAccessList(address, state.ALVersion|state.ALNonce, state.AccessListWrite) + if value.Sign() != 0 { + evm.StateDB.AddAddressToAccessList(address, state.ALBalance, state.AccessListWrite) + } } // Ensure there's no existing contract already at the designated address contractHash := evm.StateDB.GetCodeHash(address) @@ -455,6 +480,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // The contract is a scoped environment for this execution context only. contract := NewContract(caller, AccountRef(address), value, gas) contract.SetCodeOptionalHash(&address, codeAndHash) + contract.IsDeployment = true if evm.Config.Tracer != nil { if evm.depth == 0 { @@ -489,6 +515,15 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } } + if err == nil && evm.chainRules.IsVerkle { + if len(ret) > 0 { + touchCodeChunksRangeOnReadAndChargeGas(address, 0, uint64(len(ret)), uint64(len(ret)), evm.StateDB) + } + if !contract.UseGas(evm.StateDB.AddAddressToAccessList(address, state.ALAllItems, state.AccessListWrite)) { + err = ErrOutOfGas + } + } + // When an error was returned by the EVM or when setting the creation code // above we revert to the snapshot and consume any gas remaining. Additionally // when we're in homestead this also counts for code storage gas errors. diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 4b141d8f9a..4a505b43e2 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/params" ) @@ -395,6 +396,20 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { return 0, ErrGasUintOverflow } + if evm.chainRules.IsVerkle { + if _, isPrecompile := evm.precompile(address); !isPrecompile { + gas, overflow = math.SafeAdd(gas, evm.StateDB.AddAddressToAccessList(address, state.ALVersion|state.ALCodeSize, state.AccessListRead)) + if overflow { + return 0, ErrGasUintOverflow + } + if transfersValue { + gas, overflow = math.SafeAdd(gas, evm.StateDB.AddAddressToAccessList(address, state.ALBalance, state.AccessListRead)) + if overflow { + return 0, ErrGasUintOverflow + } + } + } + } return gas, nil } @@ -420,6 +435,17 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { return 0, ErrGasUintOverflow } + if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { + return 0, ErrGasUintOverflow + } + if evm.chainRules.IsVerkle { + if _, isPrecompile := evm.precompile(contract.Address()); !isPrecompile { + gas, overflow = math.SafeAdd(gas, evm.StateDB.AddAddressToAccessList(contract.Address(), state.ALVersion|state.ALCodeSize, state.AccessListRead)) + if overflow { + return 0, ErrGasUintOverflow + } + } + } return gas, nil } @@ -436,6 +462,14 @@ func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { return 0, ErrGasUintOverflow } + if evm.chainRules.IsVerkle { + if _, isPrecompile := evm.precompile(contract.Address()); !isPrecompile { + gas, overflow = math.SafeAdd(gas, evm.StateDB.AddAddressToAccessList(contract.Address(), state.ALVersion|state.ALCodeSize, state.AccessListRead)) + if overflow { + return 0, ErrGasUintOverflow + } + } + } return gas, nil } @@ -452,6 +486,14 @@ func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { return 0, ErrGasUintOverflow } + if evm.chainRules.IsVerkle { + if _, isPrecompile := evm.precompile(contract.Address()); !isPrecompile { + gas, overflow = math.SafeAdd(gas, evm.StateDB.AddAddressToAccessList(contract.Address(), state.ALVersion|state.ALCodeSize, state.AccessListRead)) + if overflow { + return 0, ErrGasUintOverflow + } + } + } return gas, nil } @@ -477,3 +519,40 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me } return gas, nil } + +// touchCodeChunksRangeOnReadAndChargeGas is a helper function to touch every chunk in a code range and charge witness gas costs +func touchCodeChunksRangeOnReadAndChargeGas(contractAddr common.Address, startPC, size uint64, codeLen uint64, sdb StateDB) 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++ { + gas := sdb.TouchAddressOnReadAndComputeGas(contractAddr, chunkNumber) + var overflow bool + statelessGasCharged, overflow = math.SafeAdd(statelessGasCharged, gas) + if overflow { + panic("overflow when adding gas") + + } + } + return statelessGasCharged +} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index ac3ea4bcd6..13b187849c 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -584,6 +584,13 @@ func opCreate(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b input = scope.Memory.GetCopy(int64(offset.Uint64()), int64(size.Uint64())) gas = scope.Contract.Gas ) + if interpreter.evm.chainRules.IsVerkle { + contractAddress := crypto.CreateAddress(scope.Contract.Address(), interpreter.evm.StateDB.GetNonce(scope.Contract.Address())) + statelessGas := interpreter.evm.StateDB.TouchAndChargeContractCreateInit(contractAddress, value.Sign() != 0) + if !tryConsumeGas(&gas, statelessGas) { + return nil, ErrOutOfGas + } + } if interpreter.evm.chainRules.IsEIP150 { gas -= gas / 64 } @@ -626,6 +633,14 @@ func opCreate2(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([] input = scope.Memory.GetCopy(int64(offset.Uint64()), int64(size.Uint64())) gas = scope.Contract.Gas ) + if interpreter.evm.chainRules.IsVerkle { + codeAndHash := &codeAndHash{code: input} + contractAddress := crypto.CreateAddress2(scope.Contract.Address(), salt.Bytes32(), codeAndHash.Hash().Bytes()) + statelessGas := interpreter.evm.StateDB.TouchAndChargeContractCreateInit(contractAddress, endowment.Sign() != 0) + if !tryConsumeGas(&gas, statelessGas) { + return nil, ErrOutOfGas + } + } // Apply EIP150 gas -= gas / 64 scope.Contract.UseGas(gas) @@ -866,6 +881,17 @@ func opPush1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by *pc += 1 if *pc < codeLen { scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc]))) + + if !scope.Contract.IsDeployment && interpreter.evm.chainRules.IsVerkle && *pc%31 == 0 { + // touch next chunk if PUSH1 is at the boundary. if so, *pc has + // advanced past this boundary. + contractAddr := scope.Contract.Address() + statelessGas := touchCodeChunksRangeOnReadAndChargeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), interpreter.evm.StateDB) + if !scope.Contract.UseGas(statelessGas) { + scope.Contract.Gas = 0 + return nil, ErrOutOfGas + } + } } else { scope.Stack.push(integer.Clear()) } @@ -887,6 +913,17 @@ func makePush(size uint64, pushByteSize int) executionFunc { endMin = startMin + pushByteSize } + if !scope.Contract.IsDeployment && interpreter.evm.chainRules.IsVerkle && *pc%31 == 0 { + // touch next chunk if PUSH1 is at the boundary. if so, *pc has + // advanced past this boundary. + contractAddr := scope.Contract.Address() + statelessGas := touchCodeChunksRangeOnReadAndChargeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), interpreter.evm.StateDB) + if !scope.Contract.UseGas(statelessGas) { + scope.Contract.Gas = 0 + return nil, ErrOutOfGas + } + } + integer := new(uint256.Int) scope.Stack.push(integer.SetBytes(common.RightPadBytes( scope.Contract.Code[startMin:endMin], pushByteSize))) diff --git a/core/vm/interface.go b/core/vm/interface.go index 25bfa06720..2dfa610c23 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/types" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -64,14 +65,18 @@ type StateDB interface { // is defined according to EIP161 (balance = nonce = code = 0). Empty(common.Address) bool - AddressInAccessList(addr common.Address) bool - SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) // AddAddressToAccessList adds the given address to the access list. This operation is safe to perform // even if the feature/fork is not active yet - AddAddressToAccessList(addr common.Address) + AddAddressToAccessList(addr common.Address, item state.ALAccountItem, isWrite state.ALAccessMode) uint64 // AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform // even if the feature/fork is not active yet - AddSlotToAccessList(addr common.Address, slot common.Hash) + AddSlotToAccessList(addr common.Address, slot common.Hash, write state.ALAccessMode) uint64 + TouchTxOriginAndComputeGas(addr common.Address) uint64 + TouchTxExistingAndComputeGas(addr common.Address, sendsValue bool) uint64 + TouchAndChargeContractCreateInit(addr common.Address, sendsValue bool) uint64 + TouchAndChargeValueTransfer(from, to common.Address) uint64 + TouchAddressOnReadAndComputeGas(from common.Address, chunk uint64) uint64 + Prepare(rules params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) RevertToSnapshot(int) @@ -79,6 +84,8 @@ type StateDB interface { AddLog(*types.Log) AddPreimage(common.Hash, []byte) + + Witness() state.AccessList } // CallContext provides a basic interface for the EVM calling conventions. The EVM diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 1968289f4e..c3a2327d18 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -56,6 +56,11 @@ func NewEVMInterpreter(evm *EVM) *EVMInterpreter { // If jump table was not initialised we set the default one. var table *JumpTable switch { + case evm.chainRules.IsVerkle: + table = &cancunInstructionSet + if err := EnableEIP(4762, table); err != nil { + log.Error("EIP activation failed", "eip", 4762, "error", err) + } case evm.chainRules.IsCancun: table = &cancunInstructionSet case evm.chainRules.IsShanghai: @@ -174,6 +179,12 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( // Capture pre-execution values for tracing. logged, pcCopy, gasCopy = false, pc, contract.Gas } + if in.evm.chainRules.IsVerkle && !contract.IsDeployment { + // if the PC ends up in a new "chunk" of verkleized code, charge the + // associated costs. + contractAddr := contract.Address() + contract.Gas -= touchCodeChunksRangeOnReadAndChargeGas(contractAddr, pc, 1, uint64(len(contract.Code)), in.evm.StateDB) + } // Get the operation from the jump table and validate the stack to ensure there are // enough stack items available to perform the operation. op = contract.GetOp(pc) diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 65716f9442..b357f4c5f8 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -80,6 +80,12 @@ func validate(jt JumpTable) JumpTable { return jt } +func newVerkleInstructionSet() JumpTable { + instructionSet := newCancunInstructionSet() + EnableEIP(4762, &instructionSet) + return validate(instructionSet) +} + func newCancunInstructionSet() JumpTable { instructionSet := newShanghaiInstructionSet() enable4844(&instructionSet) // EIP-4844 (BLOBHASH opcode) diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index f420a24105..451fe4dbc3 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/params" ) @@ -35,26 +36,15 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { y, x = stack.Back(1), stack.peek() slot = common.Hash(x.Bytes32()) current = evm.StateDB.GetState(contract.Address(), slot) - cost = uint64(0) + cost = evm.StateDB.AddSlotToAccessList(contract.Address(), slot, state.AccessListWrite) ) // Check slot presence in the access list - if addrPresent, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { - cost = params.ColdSloadCostEIP2929 - // If the caller cannot afford the cost, this change will be rolled back - evm.StateDB.AddSlotToAccessList(contract.Address(), slot) - if !addrPresent { - // Once we're done with YOLOv2 and schedule this for mainnet, might - // be good to remove this panic here, which is just really a - // canary to have during testing - panic("impossible case: address was not present in access list during sstore op") - } - } value := common.Hash(y.Bytes32()) if current == value { // noop (1) // EIP 2200 original clause: // return params.SloadGasEIP2200, nil - return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS + return cost, nil // SLOAD_GAS } original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32()) if original == current { @@ -91,10 +81,27 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { } // EIP-2200 original clause: //return params.SloadGasEIP2200, nil // dirty update (2.2) - return cost + params.WarmStorageReadCostEIP2929, nil // dirty update (2.2) + return cost, nil // dirty update (2.2) } } +func gasSStore4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + gas := evm.StateDB.AddSlotToAccessList(contract.Address(), common.Hash(stack.peek().Bytes32()), state.AccessListWrite) + return gas, nil +} + +func gasBalance4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return evm.StateDB.AddAddressToAccessList(contract.Address(), state.ALBalance, state.AccessListRead), nil +} + +func gasExtCodeSize4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return evm.StateDB.AddAddressToAccessList(contract.Address(), state.ALCodeSize, state.AccessListRead), nil +} + +func gasExtCodeHash4762(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return evm.StateDB.AddAddressToAccessList(contract.Address(), state.ALCodeHash, state.AccessListRead), nil +} + // gasSLoadEIP2929 calculates dynamic gas for SLOAD according to EIP-2929 // For SLOAD, if the (address, storage_key) pair (where address is the address of the contract // whose storage is being read) is not yet in accessed_storage_keys, @@ -104,13 +111,7 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me loc := stack.peek() slot := common.Hash(loc.Bytes32()) // Check slot presence in the access list - if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { - // If the caller cannot afford the cost, this change will be rolled back - // If he does afford it, we can skip checking the same thing later on, during execution - evm.StateDB.AddSlotToAccessList(contract.Address(), slot) - return params.ColdSloadCostEIP2929, nil - } - return params.WarmStorageReadCostEIP2929, nil + return evm.StateDB.AddSlotToAccessList(contract.Address(), slot, false), nil } // gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929 @@ -126,15 +127,13 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo } addr := common.Address(stack.peek().Bytes20()) // Check slot presence in the access list - if !evm.StateDB.AddressInAccessList(addr) { - evm.StateDB.AddAddressToAccessList(addr) - var overflow bool - // We charge (cold-warm), since 'warm' is already charged as constantGas - if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow { - return 0, ErrGasUintOverflow - } - return gas, nil + algas := evm.StateDB.AddAddressToAccessList(addr, state.ALCodeSize, state.AccessListRead) + var overflow bool + // We charge (cold-warm), since 'warm' is already charged as constantGas + if gas, overflow = math.SafeAdd(gas, algas); overflow { + return 0, ErrGasUintOverflow } + return gas, nil } @@ -148,28 +147,22 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { addr := common.Address(stack.peek().Bytes20()) // Check slot presence in the access list - if !evm.StateDB.AddressInAccessList(addr) { - // If the caller cannot afford the cost, this change will be rolled back - evm.StateDB.AddAddressToAccessList(addr) - // The warm storage read cost is already charged as constantGas - return params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, nil - } - return 0, nil + + // If the caller cannot afford the cost, this change will be rolled back + return evm.StateDB.AddAddressToAccessList(addr, state.ALCodeSize|state.ALCodeHash|state.ALBalance, state.AccessListRead), nil } func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { addr := common.Address(stack.Back(1).Bytes20()) // Check slot presence in the access list - warmAccess := evm.StateDB.AddressInAccessList(addr) + cost := evm.StateDB.AddAddressToAccessList(addr, 0, state.AccessListRead) // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so // the cost to charge for cold access, if any, is Cold - Warm - coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 - if !warmAccess { - evm.StateDB.AddAddressToAccessList(addr) + if cost == params.WarmStorageReadCostEIP2929 { // Charge the remaining difference here already, to correctly calculate available // gas for call - if !contract.UseGas(coldCost) { + if !contract.UseGas(cost) { return 0, ErrOutOfGas } } @@ -179,17 +172,17 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc { // - memory expansion // - 63/64ths rule gas, err := oldCalculator(evm, contract, stack, mem, memorySize) - if warmAccess || err != nil { + if cost == params.WarmStorageReadCostEIP2929 || err != nil { return gas, err } // In case of a cold access, we temporarily add the cold charge back, and also // add it to the returned gas. By adding it to the return, it will be charged // outside of this function, as part of the dynamic gas, and that will make it // also become correctly reported to tracers. - contract.Gas += coldCost + contract.Gas += cost var overflow bool - if gas, overflow = math.SafeAdd(gas, coldCost); overflow { + if gas, overflow = math.SafeAdd(gas, cost); overflow { return 0, ErrGasUintOverflow } return gas, nil @@ -231,11 +224,7 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { gas uint64 address = common.Address(stack.peek().Bytes20()) ) - if !evm.StateDB.AddressInAccessList(address) { - // If the caller cannot afford the cost, this change will be rolled back - evm.StateDB.AddAddressToAccessList(address) - gas = params.ColdAccountAccessCostEIP2929 - } + gas = evm.StateDB.AddAddressToAccessList(address, 0, state.AccessListWrite) // if empty and transfers value if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { gas += params.CreateBySelfdestructGas diff --git a/params/verkle_params.go b/params/verkle_params.go new file mode 100644 index 0000000000..93d4f7cd64 --- /dev/null +++ b/params/verkle_params.go @@ -0,0 +1,36 @@ +// Copyright 2023 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 params + +// Verkle tree EIP: costs associated to witness accesses +var ( + WitnessBranchReadCost uint64 = 1900 + WitnessChunkReadCost uint64 = 200 + WitnessBranchWriteCost uint64 = 3000 + WitnessChunkWriteCost uint64 = 500 + WitnessChunkFillCost uint64 = 6200 +) + +// ClearVerkleWitnessCosts sets all witness costs to 0, which is necessary +// for historical block replay simulations. +func ClearVerkleWitnessCosts() { + WitnessBranchReadCost = 0 + WitnessChunkReadCost = 0 + WitnessBranchWriteCost = 0 + WitnessChunkWriteCost = 0 + WitnessChunkFillCost = 0 +} diff --git a/trie/utils/verkle.go b/trie/utils/verkle.go index ce059edc64..d836166d9b 100644 --- a/trie/utils/verkle.go +++ b/trie/utils/verkle.go @@ -18,7 +18,6 @@ package utils import ( "encoding/binary" - "sync" "github.com/crate-crypto/go-ipa/bandersnatch/fr" "github.com/ethereum/go-ethereum/common/lru" @@ -35,6 +34,8 @@ const ( NonceLeafKey = 2 CodeKeccakLeafKey = 3 CodeSizeLeafKey = 4 + + maxPointCacheByteSize = 100 << 20 ) var ( @@ -46,7 +47,7 @@ var ( verkleNodeWidth = uint256.NewInt(256) codeStorageDelta = uint256.NewInt(0).Sub(codeOffset, headerStorageOffset) - index0Point *verkle.Point // pre-computed commitment of polynomial [2+256*64] + getTreePolyIndex0Point *verkle.Point // pre-computed commitment of polynomial [2+256*64] // cacheHitGauge is the metric to track how many cache hit occurred. cacheHitGauge = metrics.NewRegisteredGauge("trie/verkle/cache/hit", nil) @@ -57,15 +58,11 @@ var ( func init() { // The byte array is the Marshalled output of the point computed as such: - // - // var ( - // config = verkle.GetConfig() - // fr verkle.Fr - // ) - // verkle.FromLEBytes(&fr, []byte{2, 64}) - // point := config.CommitToPoly([]verkle.Fr{fr}, 1) - index0Point = new(verkle.Point) - err := index0Point.SetBytes([]byte{34, 25, 109, 242, 193, 5, 144, 224, 76, 52, 189, 92, 197, 126, 9, 145, 27, 152, 199, 130, 165, 3, 210, 27, 193, 131, 142, 28, 110, 26, 16, 191}) + //cfg, _ := verkle.GetConfig() + //verkle.FromLEBytes(&getTreePolyIndex0Fr[0], []byte{2, 64}) + //= cfg.CommitToPoly(getTreePolyIndex0Fr[:], 1) + getTreePolyIndex0Point = new(verkle.Point) + err := getTreePolyIndex0Point.SetBytes([]byte{34, 25, 109, 242, 193, 5, 144, 224, 76, 52, 189, 92, 197, 126, 9, 145, 27, 152, 199, 130, 165, 3, 210, 27, 193, 131, 142, 28, 110, 26, 16, 191}) if err != nil { panic(err) } @@ -73,39 +70,36 @@ func init() { // PointCache is the LRU cache for storing evaluated address commitment. type PointCache struct { - lru lru.BasicLRU[string, *verkle.Point] - lock sync.RWMutex + cache *lru.Cache[string, *verkle.Point] } // NewPointCache returns the cache with specified size. -func NewPointCache(maxItems int) *PointCache { +func NewPointCache() *PointCache { + // Each verkle.Point is 96 bytes. + verklePointSize := 96 + capacity := maxPointCacheByteSize / verklePointSize return &PointCache{ - lru: lru.NewBasicLRU[string, *verkle.Point](maxItems), + cache: lru.NewCache[string, *verkle.Point](capacity), } } -// Get returns the cached commitment for the specified address, or computing -// it on the flight. -func (c *PointCache) Get(addr []byte) *verkle.Point { - c.lock.Lock() - defer c.lock.Unlock() - - p, ok := c.lru.Get(string(addr)) +// GetTreeKeyHeader returns the cached commitment for the specified address, +// or computing it on the fly. +func (pc *PointCache) GetTreeKeyHeader(addr []byte) *verkle.Point { + point, ok := pc.cache.Get(string(addr)) if ok { - cacheHitGauge.Inc(1) - return p + return point } - cacheMissGauge.Inc(1) - p = evaluateAddressPoint(addr) - c.lru.Add(string(addr), p) - return p + + point = EvaluateAddressPoint(addr) + pc.cache.Add(string(addr), point) + return point } -// GetStem returns the first 31 bytes of the tree key as the tree stem. It only -// works for the account metadata whose treeIndex is 0. -func (c *PointCache) GetStem(addr []byte) []byte { - p := c.Get(addr) - return pointToHash(p, 0)[:31] +func (pc *PointCache) GetTreeKeyVersionCached(addr []byte) []byte { + p := pc.GetTreeKeyHeader(addr) + v := PointToHash(p, VersionLeafKey) + return v[:] } // GetTreeKey performs both the work of the spec's get_tree_key function, and that @@ -143,9 +137,9 @@ func GetTreeKey(address []byte, treeIndex *uint256.Int, subIndex byte) []byte { ret := cfg.CommitToPoly(poly[:], 0) // add a constant point corresponding to poly[0]=[2+256*64]. - ret.Add(ret, index0Point) + ret.Add(ret, getTreePolyIndex0Point) - return pointToHash(ret, subIndex) + return PointToHash(ret, subIndex) } // GetTreeKeyWithEvaluatedAddress is basically identical to GetTreeKey, the only @@ -174,7 +168,7 @@ func GetTreeKeyWithEvaluatedAddress(evaluated *verkle.Point, treeIndex *uint256. // add the pre-evaluated address ret.Add(ret, evaluated) - return pointToHash(ret, subIndex) + return PointToHash(ret, subIndex) } // VersionKey returns the verkle tree key of the version field for the specified account. @@ -305,7 +299,7 @@ func StorageSlotKeyWithEvaluatedAddress(evaluated *verkle.Point, storageKey []by return GetTreeKeyWithEvaluatedAddress(evaluated, treeIndex, subIndex) } -func pointToHash(evaluated *verkle.Point, suffix byte) []byte { +func PointToHash(evaluated *verkle.Point, suffix byte) []byte { // The output of Byte() is big endian for banderwagon. This // introduces an imbalance in the tree, because hashes are // elements of a 253-bit field. This means more than half the @@ -319,7 +313,31 @@ func pointToHash(evaluated *verkle.Point, suffix byte) []byte { return bytes[:] } -func evaluateAddressPoint(address []byte) *verkle.Point { +func GetTreeKeyWithEvaluatedAddess(evaluated *verkle.Point, treeIndex *uint256.Int, subIndex byte) []byte { + var poly [5]fr.Element + + poly[0].SetZero() + poly[1].SetZero() + poly[2].SetZero() + + // little-endian, 32-byte aligned treeIndex + var index [32]byte + for i := 0; i < len(treeIndex); i++ { + binary.LittleEndian.PutUint64(index[i*8:(i+1)*8], treeIndex[i]) + } + verkle.FromLEBytes(&poly[3], index[:16]) + verkle.FromLEBytes(&poly[4], index[16:]) + + cfg := verkle.GetConfig() + ret := cfg.CommitToPoly(poly[:], 0) + + // add the pre-evaluated address + ret.Add(ret, evaluated) + + return PointToHash(ret, subIndex) +} + +func EvaluateAddressPoint(address []byte) *verkle.Point { if len(address) < 32 { var aligned [32]byte address = append(aligned[:32-len(address)], address...) @@ -337,6 +355,43 @@ func evaluateAddressPoint(address []byte) *verkle.Point { ret := cfg.CommitToPoly(poly[:], 0) // add a constant point - ret.Add(ret, index0Point) + ret.Add(ret, getTreePolyIndex0Point) + return ret } + +func GetTreeKeyStorageSlotTreeIndexes(storageKey []byte) (*uint256.Int, byte) { + var pos uint256.Int + pos.SetBytes(storageKey) + + // If the storage slot is in the header, we need to add the header offset. + if pos.Cmp(codeStorageDelta) < 0 { + // This addition is always safe; it can't ever overflow since pos