From 7b8d4d1b8e00c0515ead0abb3f556e2b5a0617a7 Mon Sep 17 00:00:00 2001 From: Qi Zhou Date: Thu, 21 Apr 2022 11:35:27 -0700 Subject: [PATCH] unlimit code size with cold/warm storage --- core/rawdb/accessors_state.go | 18 +++++++ core/rawdb/schema.go | 6 +++ core/state/access_list.go | 32 +++++++++++- core/state/database.go | 6 +++ core/state/journal.go | 11 ++++ core/state/statedb.go | 23 ++++++-- core/vm/eips.go | 20 +++++++ core/vm/evm.go | 8 +-- core/vm/interface.go | 2 + core/vm/operations_acl.go | 98 +++++++++++++++++++++++++++++++++++ params/protocol_params.go | 10 ++-- 11 files changed, 221 insertions(+), 13 deletions(-) diff --git a/core/rawdb/accessors_state.go b/core/rawdb/accessors_state.go index 41e21b6ca..ad7fc150d 100644 --- a/core/rawdb/accessors_state.go +++ b/core/rawdb/accessors_state.go @@ -17,6 +17,8 @@ package rawdb import ( + "encoding/binary" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" @@ -48,6 +50,16 @@ func ReadCodeWithPrefix(db ethdb.KeyValueReader, hash common.Hash) []byte { return data } +// ReadCodeSize retrieves the contract code size of the provided code hash. +// Return 0 if not found +func ReadCodeSize(db ethdb.KeyValueReader, hash common.Hash) int { + data, _ := db.Get(codeSizeKey(hash)) + if len(data) != 4 { + return 0 + } + return int(binary.BigEndian.Uint32(data)) +} + // ReadTrieNode retrieves the trie node of the provided hash. func ReadTrieNode(db ethdb.KeyValueReader, hash common.Hash) []byte { data, _ := db.Get(hash.Bytes()) @@ -96,6 +108,12 @@ func WriteCode(db ethdb.KeyValueWriter, hash common.Hash, code []byte) { if err := db.Put(codeKey(hash), code); err != nil { log.Crit("Failed to store contract code", "err", err) } + + var sizeData [4]byte + binary.BigEndian.PutUint32(sizeData[:], uint32(len(code))) + if err := db.Put(codeSizeKey(hash), sizeData[:]); err != nil { + log.Crit("Failed to store contract code size", "err", err) + } } // WriteTrieNode writes the provided trie node database. diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 08f373488..cbf1dc40f 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -96,6 +96,7 @@ var ( SnapshotStoragePrefix = []byte("o") // SnapshotStoragePrefix + account hash + storage hash -> storage trie value CodePrefix = []byte("c") // CodePrefix + code hash -> account code skeletonHeaderPrefix = []byte("S") // skeletonHeaderPrefix + num (uint64 big endian) -> header + CodeSizePrefix = []byte("s") // CodePrefixSize PreimagePrefix = []byte("secure-key-") // PreimagePrefix + hash -> preimage configPrefix = []byte("ethereum-config-") // config prefix for the db @@ -230,6 +231,11 @@ func codeKey(hash common.Hash) []byte { return append(CodePrefix, hash.Bytes()...) } +// codeSizekey = CodeSizePreifx + hash +func codeSizeKey(hash common.Hash) []byte { + return append(CodeSizePrefix, hash.Bytes()...) +} + // IsCodeKey reports whether the given byte slice is the key of contract code, // if so return the raw code hash as well. func IsCodeKey(key []byte) (bool, []byte) { diff --git a/core/state/access_list.go b/core/state/access_list.go index 419469134..22812a936 100644 --- a/core/state/access_list.go +++ b/core/state/access_list.go @@ -21,8 +21,9 @@ import ( ) type accessList struct { - addresses map[common.Address]int - slots []map[common.Hash]struct{} + addresses map[common.Address]int + codeInAddresses map[common.Address]bool + slots []map[common.Hash]struct{} } // ContainsAddress returns true if the address is in the access list. @@ -31,6 +32,12 @@ func (al *accessList) ContainsAddress(address common.Address) bool { return ok } +// ContainsAddress returns true if the address is in the access list. +func (al *accessList) ContainsAddressCode(address common.Address) bool { + _, ok := al.codeInAddresses[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) { @@ -60,6 +67,9 @@ func (a *accessList) Copy() *accessList { for k, v := range a.addresses { cp.addresses[k] = v } + for k, v := range a.codeInAddresses { + cp.codeInAddresses[k] = v + } cp.slots = make([]map[common.Hash]struct{}, len(a.slots)) for i, slotMap := range a.slots { newSlotmap := make(map[common.Hash]struct{}, len(slotMap)) @@ -81,6 +91,16 @@ func (al *accessList) AddAddress(address common.Address) bool { return true } +// AddAddressCode adds the code of 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) AddAddressCode(address common.Address) bool { + if _, present := al.codeInAddresses[address]; present { + return false + } + al.codeInAddresses[address] = true + return true +} + // AddSlot adds the specified (addr, slot) combo to the access list. // Return values are: // - address added @@ -134,3 +154,11 @@ func (al *accessList) DeleteSlot(address common.Address, slot common.Hash) { func (al *accessList) DeleteAddress(address common.Address) { delete(al.addresses, address) } + +// DeleteAddressCode removes the code of an address 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) DeleteAddressCode(address common.Address) { + delete(al.codeInAddresses, address) +} diff --git a/core/state/database.go b/core/state/database.go index bbcd2358e..7445e627f 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -194,6 +194,12 @@ func (db *cachingDB) ContractCodeSize(addrHash, codeHash common.Hash) (int, erro if cached, ok := db.codeSizeCache.Get(codeHash); ok { return cached.(int), nil } + + size := rawdb.ReadCodeSize(db.db.DiskDB(), codeHash) + if size != 0 { + return size, nil + } + code, err := db.ContractCode(addrHash, codeHash) return len(code), err } diff --git a/core/state/journal.go b/core/state/journal.go index 57a692dc7..8e2250dde 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -134,6 +134,9 @@ type ( accessListAddAccountChange struct { address *common.Address } + accessListAddAccountCodeChange struct { + address *common.Address + } accessListAddSlotChange struct { address *common.Address slot *common.Hash @@ -260,6 +263,14 @@ func (ch accessListAddAccountChange) dirtied() *common.Address { return nil } +func (ch accessListAddAccountCodeChange) revert(s *StateDB) { + s.accessList.DeleteAddressCode(*ch.address) +} + +func (ch accessListAddAccountCodeChange) dirtied() *common.Address { + return nil +} + func (ch accessListAddSlotChange) revert(s *StateDB) { s.accessList.DeleteSlot(*ch.address, *ch.slot) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 1d31cf470..d95dd79aa 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -984,11 +984,11 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) { } // PrepareAccessList handles the preparatory steps for executing a state transition with -// regards to both EIP-2929 and EIP-2930: +// regards to both EIP-2929, EIP-2930, and EIP-5027: // -// - Add sender to access list (2929) -// - Add destination to access list (2929) -// - Add precompiles to access list (2929) +// - Add sender to access list (2929, 5027) +// - Add destination to access list (2929, 5027) +// - Add precompiles to access list (2929, 5027) // - Add the contents of the optional tx access list (2930) // // This method should only be called if Berlin/2929+2930 is applicable at the current number. @@ -997,12 +997,15 @@ func (s *StateDB) PrepareAccessList(sender common.Address, dst *common.Address, s.accessList = newAccessList() s.AddAddressToAccessList(sender) + s.AddAddressCodeToAccessList(sender) if dst != nil { s.AddAddressToAccessList(*dst) + s.AddAddressCodeToAccessList(*dst) // If it's a create-tx, the destination will be added inside evm.create } for _, addr := range precompiles { s.AddAddressToAccessList(addr) + s.AddAddressCodeToAccessList(addr) } for _, el := range list { s.AddAddressToAccessList(el.Address) @@ -1019,6 +1022,13 @@ func (s *StateDB) AddAddressToAccessList(addr common.Address) { } } +// AddAddressCodeToAccessList adds the given address to the access list +func (s *StateDB) AddAddressCodeToAccessList(addr common.Address) { + if s.accessList.AddAddressCode(addr) { + s.journal.append(accessListAddAccountCodeChange{&addr}) + } +} + // 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) @@ -1042,6 +1052,11 @@ func (s *StateDB) AddressInAccessList(addr common.Address) bool { return s.accessList.ContainsAddress(addr) } +// AddressCodeInAccessList returns true if the given address's code is in the access list. +func (s *StateDB) AddressCodeInAccessList(addr common.Address) bool { + return s.accessList.ContainsAddressCode(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) diff --git a/core/vm/eips.go b/core/vm/eips.go index 4070a2db5..e9a8ee78c 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -31,6 +31,7 @@ var activators = map[int]func(*JumpTable){ 2200: enable2200, 1884: enable1884, 1344: enable1344, + 5027: enable5027, } // EnableEIP enables the given EIP on the config. @@ -147,6 +148,25 @@ func enable2929(jt *JumpTable) { jt[SELFDESTRUCT].dynamicGas = gasSelfdestructEIP2929 } +// enable2929 enables "EIP-2929: Gas cost increases for state access opcodes" +// https://eips.ethereum.org/EIPS/eip-2929 +func enable5027(jt *JumpTable) { + jt[EXTCODECOPY].constantGas = params.WarmStorageReadCostEIP2929 + jt[EXTCODECOPY].dynamicGas = gasExtCodeCopyEIP5027 + + jt[CALL].constantGas = params.WarmStorageReadCostEIP2929 + jt[CALL].dynamicGas = gasCallEIP5027 + + jt[CALLCODE].constantGas = params.WarmStorageReadCostEIP2929 + jt[CALLCODE].dynamicGas = gasCallCodeEIP5027 + + jt[STATICCALL].constantGas = params.WarmStorageReadCostEIP2929 + jt[STATICCALL].dynamicGas = gasStaticCallEIP5027 + + jt[DELEGATECALL].constantGas = params.WarmStorageReadCostEIP2929 + jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP5027 +} + // enable3529 enabled "EIP-3529: Reduction in refunds": // - Removes refunds for selfdestructs // - Reduces refunds for SSTORE diff --git a/core/vm/evm.go b/core/vm/evm.go index dd55618bf..99e57c28e 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -421,6 +421,8 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // the access-list change should not be rolled back if evm.chainRules.IsBerlin { evm.StateDB.AddAddressToAccessList(address) + // TODO: check shanghai + evm.StateDB.AddAddressCodeToAccessList(address) } // Ensure there's no existing contract already at the designated address contractHash := evm.StateDB.GetCodeHash(address) @@ -453,9 +455,9 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, ret, err := evm.interpreter.Run(contract, nil, false) // Check whether the max code size has been exceeded, assign err if the case. - if err == nil && evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize { - err = ErrMaxCodeSizeExceeded - } + // if err == nil && evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize { + // err = ErrMaxCodeSizeExceeded + // } // Reject code starting with 0xEF if EIP-3541 is enabled. if err == nil && len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { diff --git a/core/vm/interface.go b/core/vm/interface.go index ad9b05d66..12660dd08 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -59,6 +59,7 @@ type StateDB interface { PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) AddressInAccessList(addr common.Address) bool + AddressCodeInAccessList(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 @@ -66,6 +67,7 @@ type StateDB interface { // 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) + AddAddressCodeToAccessList(addr common.Address) RevertToSnapshot(int) Snapshot() int diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 551e1f5f1..cb76a4390 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -138,6 +138,41 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo return gas, nil } +// gasExtCodeCopyEIP5027 implements extcodecopy according to EIP-5027 +// EIP spec: +// > If the target is not in accessed_addresses, +// > charge COLD_ACCOUNT_ACCESS_COST * N_CODE_UNIT gas, and add the address to accessed_addresses and accessed_code_in_addresses. +// > Else if the target is not in accessed_code_in_addresses, +// > charge COLD_ACCOUNT_ACCESS_COST * (N_CODE_UNIT - 1) gas, and add the address to accessed_code_in_addresses. +// > Otherwise, charge WARM_STORAGE_READ_COST gas. +func gasExtCodeCopyEIP5027(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + // memory expansion first (dynamic part of pre-5027 implementation) + gas, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize) + if err != nil { + return 0, err + } + 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 + } + } + if !evm.StateDB.AddressCodeInAccessList(addr) { + evm.StateDB.AddAddressCodeToAccessList(addr) + var overflow bool + + // We charge cold for extra code + if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929*getExtraCodeUnit(evm, addr)); overflow { + return 0, ErrGasUintOverflow + } + } + return gas, nil +} + // gasEip2929AccountCheck checks whether the first stack item (as address) is present in the access list. // If it is, this method returns '0', otherwise 'cold-warm' gas, presuming that the opcode using it // is also using 'warm' as constant factor. @@ -191,6 +226,64 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc { } } +func getExtraCodeUnit(evm *EVM, addr common.Address) uint64 { + codeSize := evm.StateDB.GetCodeSize(addr) + extraCodeUnit := uint64(0) + if codeSize > params.CodeSizeUnit { + extraCodeUnit = (uint64(codeSize - 1)) / params.CodeSizeUnit + } + return extraCodeUnit +} + +func makeCallVariantGasCallEIP5027(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) + warmCodeAccess := evm.StateDB.AddressCodeInAccessList(addr) + // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so + // the cost to charge for cold access, if any, is n * Cold - Warm + coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + + if !warmAccess { + evm.StateDB.AddAddressToAccessList(addr) + evm.StateDB.AddAddressCodeToAccessList(addr) + + coldCost += getExtraCodeUnit(evm, addr) * params.ColdAccountCodeAccessCostEIP5027 + + // Charge the remaining difference here already, to correctly calculate available + // gas for call + if !contract.UseGas(coldCost) { + return 0, ErrOutOfGas + } + } else if !warmCodeAccess { + evm.StateDB.AddAddressCodeToAccessList(addr) + + coldCost = getExtraCodeUnit(evm, addr) * params.ColdAccountCodeAccessCostEIP5027 + // Charge the remaining difference here already, to correctly calculate available + // gas for call + if !contract.UseGas(coldCost) { + return 0, ErrOutOfGas + } + } + // Now call the old calculator, which takes into account + // - create new account + // - transfer value + // - memory expansion + // - 63/64ths rule + gas, err := oldCalculator(evm, contract, stack, mem, memorySize) + if (warmAccess && warmCodeAccess) || 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 + return gas + coldCost, nil + } +} + var ( gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCall) gasDelegateCallEIP2929 = makeCallVariantGasCallEIP2929(gasDelegateCall) @@ -200,6 +293,11 @@ var ( // gasSelfdestructEIP3529 implements the changes in EIP-2539 (no refunds) gasSelfdestructEIP3529 = makeSelfdestructGasFn(false) + gasCallEIP5027 = makeCallVariantGasCallEIP5027(gasCall) + gasDelegateCallEIP5027 = makeCallVariantGasCallEIP5027(gasDelegateCall) + gasStaticCallEIP5027 = makeCallVariantGasCallEIP5027(gasStaticCall) + gasCallCodeEIP5027 = makeCallVariantGasCallEIP5027(gasCallCode) + // gasSStoreEIP2929 implements gas cost for SSTORE according to EIP-2929 // // When calling SSTORE, check if the (address, storage_key) pair is in accessed_storage_keys. diff --git a/params/protocol_params.go b/params/protocol_params.go index 5f154597a..c3d5c66ce 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -58,9 +58,11 @@ const ( SstoreResetGasEIP2200 uint64 = 5000 // Once per SSTORE operation from clean non-zero to something else SstoreClearsScheduleRefundEIP2200 uint64 = 15000 // Once per SSTORE operation for clearing an originally existing storage slot - ColdAccountAccessCostEIP2929 = uint64(2600) // COLD_ACCOUNT_ACCESS_COST - ColdSloadCostEIP2929 = uint64(2100) // COLD_SLOAD_COST - WarmStorageReadCostEIP2929 = uint64(100) // WARM_STORAGE_READ_COST + ColdAccountAccessCostEIP2929 = uint64(2600) // COLD_ACCOUNT_ACCESS_COST + ColdSloadCostEIP2929 = uint64(2100) // COLD_SLOAD_COST + WarmStorageReadCostEIP2929 = uint64(100) // WARM_STORAGE_READ_COST + ColdAccountCodeAccessCostEIP5027 = uint64(2600) // COLD_ACCOUNT_CODE_ACCESS_COST_PER_UNIT + WarmAccountCodeAccessCostEIP5027 = uint64(2600) // WARM_ACCOUNT_CODE_ACCESS_COST_PER_UNIT // In EIP-2200: SstoreResetGas was 5000. // In EIP-2929: SstoreResetGas was changed to '5000 - COLD_SLOAD_COST'. @@ -123,7 +125,7 @@ const ( ElasticityMultiplier = 2 // Bounds the maximum gas limit an EIP-1559 block may have. InitialBaseFee = 1000000000 // Initial base fee for EIP-1559 blocks. - MaxCodeSize = 24576 // Maximum bytecode to permit for a contract + CodeSizeUnit = 24576 // Code size unit for gas metering. // Precompiled contract gas prices -- 2.30.1 (Apple Git-130)