From 7b8d4d1b8e00c0515ead0abb3f556e2b5a0617a7 Mon Sep 17 00:00:00 2001
From: Qi Zhou <qzhou64@gmail.com>
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)

