Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create EIP-5027 #5027

Merged
merged 20 commits into from May 22, 2022
Merged
90 changes: 90 additions & 0 deletions EIPS/eip-5027.md
@@ -0,0 +1,90 @@
---
eip: 5027
title: Remove the limit on contract code size
description: Change the limit on contract size from 24576 to infinity
author: Qi Zhou (@qizhou)
qizhou marked this conversation as resolved.
Show resolved Hide resolved
discussions-to: https://ethereum-magicians.org/t/eip-5027-unlimit-contract-code-size/9010
status: Draft
type: Standards Track
category: Core
created: 2022-04-21
requires: 170
---


## Abstract

Remove the limit on the contract code size, i.e., only limit the contract code size by block gas limit, with minimal changes to existing code and proper gas metering adjustment to avoid possible attacks.


## Motivation

The motivation is to remove the limit on the code size so that users can deploy a large-code contract without worrying about splitting the contract into several sub-contracts.

With the dramatic growth of dApplications, the functionalities of smart contracts are becoming more and more complicated, and thus, the sizes of newly developed contracts are steadily increasing. As a result, we are facing more and more issues with the 24576-bytes contract size limit. Although several techniques such as splitting a large contract into several sub-contracts can alleviate the issue, these techniques inevitably increase the burden of developing/deploying/maintaining smart contracts.

The proposal implements a solution to remove the existing 24576-bytes limit of the code size. Further, the proposal aims to minimize the changes in the client implementation (e.g., Geth) with
- proper gas metering to avoid abusing the node resources for contract-related opcodes, i.e, `CODESIZE (0x38)/CODECOPY (0x39)/EXTCODESIZE (0x3B)/EXTCODECOPY (0x3C)/EXTCODEHASH (0x3F)/DELEGATECALL (0xF4)/CALL (0xF1)/CALLCODE (0xF2)/STATICCALL (0xFA)/CREATE (0xF0)/CREATE2 (0xF5)`; and
- no change to the existing structure of the Ethereum state.


## Specification

### Parameters

| Constant | Value |
| ------------------------- | ---------------- |
| `FORK_BLKNUM` | TBD |
| `CODE_SIZE_UNIT` | 24576 |
| `READ_GAS_PER_UNIT` | 700 |
| `CREATE_DATA_GAS` | 200 |

If `block.number >= FORK_BLKNUM`, the contract creation initialization can return data with any length, but the contract-related opcodes will take extra gas as defined below:

- For `CODESIZE/CODECOPY/EXTCODESIZE/EXTCODEHASH`, the gas is unchanged.

- For `EXTCODECOPY/CALL/CALLCODE/DELEGATECALL/STATICCALL`, if the contract code size > `CODE_SIZE_UNIT`, then the opcodes will take extra gas as

```
(CODE_SIZE - 1) // CODE_SIZE_UNIT * READ_GAS_PER_UNIT
```

where `//` is the integer divide operator.

- For CREATE/CREATE2, if the newly created contract size > `CODE_SIZE_UNIT`, the opcodes will take extra write gas as

`(CODE_SIZE - CODE_SIZE_UNIT) * CREATE_DATA_GAS`.

## Rationale
qizhou marked this conversation as resolved.
Show resolved Hide resolved

### Gas Metering
The goal is to measure the CPU/IO cost of the contract read/write operations reusing existing gas metering so that the resources will not be abused.

- For code-size-related opcodes (`CODESIZE`/`EXTCODESIZE`), we would expect the client to implement a mapping from the hash of code to the size, so reading the code size of a large contract should still be O(1).

- For `CODECOPY`, the data is already loaded in memory (as part of `CALL/CALLCODE/DELEGATECALL/STATICCALL`), so we do not charge extra gas.

- For `EXTCODEHASH`, the value is already in the account, so we do not charge extra gas.

- For `EXTCODECOPY/CALL/CALLCODE/DELEGATECALL/STATICCALL`, since it will read extra data from the database, we will additionally charge READ_GAS_PER_UNIT per extra `CODE_SIZE`. Note that `READ_GAS_PER_UNIT = CALLGAS = EXTCODECOPYBASE = 700`.

- For `CREATE/CREATE2`, since it will create extra data to the database, we will additionally charge `CREATE_DATA_GAS` per extra bytes.


## Backward Compatibility

All existing contracts will not be impacted by the proposal.
qizhou marked this conversation as resolved.
Show resolved Hide resolved

Only contracts deployed before EIP-170 could possibly be longer than the current max code size, and the reference implementation was able to successfully import all blocks before that fork.

## Reference Implementation

The reference implementation on Geth is available at [0001-unlimit-code-size.patch](../assets/eip-5027/0001-unlimit-code-size.patch).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand it GPL licensed code should not be in the EIP repository. Can this be changed to pointing to a commit in a pull request in Geth?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the external link is not recommended in the EIP repository either (as the previous version of this EIP uses the pointer to the commit).


## Security Considerations
TBD

## Copyright
Copyright and related rights waived via [CC0](../LICENSE.md).


229 changes: 229 additions & 0 deletions assets/eip-5027/0001-unlimit-code-size.patch
@@ -0,0 +1,229 @@
From 82ce60226a81e6a3acb92525482c16d254f63816 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

---
core/rawdb/accessors_state.go | 18 ++++++++++++++++++
core/rawdb/schema.go | 6 ++++++
core/state/database.go | 6 ++++++
core/vm/evm.go | 6 +++---
core/vm/gas_table.go | 25 +++++++++++++++++++++++++
core/vm/instructions.go | 7 +++++++
params/protocol_params.go | 2 +-
7 files changed, 66 insertions(+), 4 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/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/vm/evm.go b/core/vm/evm.go
index dd55618bf..5dc3ed6ca 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -453,9 +453,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/gas_table.go b/core/vm/gas_table.go
index 4c2cb3e5c..d0118b1ee 100644
--- a/core/vm/gas_table.go
+++ b/core/vm/gas_table.go
@@ -325,6 +325,16 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor
return gas, nil
}

+func addGasExtraCodeSize(evm *EVM, address common.Address, gas uint64) (uint64, bool) {
+ codeSize := evm.StateDB.GetCodeSize(address)
+ if codeSize <= params.CodeSizeUnit {
+ return gas, false
+ }
+
+ extraGas := (uint64(codeSize) - 1) / params.CodeSizeUnit * params.CallGasEIP150
+ return math.SafeAdd(gas, extraGas)
+}
+
func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
gas uint64
@@ -357,6 +367,9 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
+ if gas, overflow = addGasExtraCodeSize(evm, address, gas); overflow {
+ return 0, ErrGasUintOverflow
+ }
return gas, nil
}

@@ -368,6 +381,7 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory
var (
gas uint64
overflow bool
+ address = common.Address(stack.Back(1).Bytes20())
)
if stack.Back(2).Sign() != 0 {
gas += params.CallValueTransferGas
@@ -382,10 +396,14 @@ 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 = addGasExtraCodeSize(evm, address, gas); overflow {
+ return 0, ErrGasUintOverflow
+ }
return gas, nil
}

func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ address := common.Address(stack.Back(1).Bytes20())
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
@@ -398,10 +416,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 gas, overflow = addGasExtraCodeSize(evm, address, gas); overflow {
+ return 0, ErrGasUintOverflow
+ }
return gas, nil
}

func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ address := common.Address(stack.Back(1).Bytes20())
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
@@ -414,6 +436,9 @@ func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
+ if gas, overflow = addGasExtraCodeSize(evm, address, gas); overflow {
+ return 0, ErrGasUintOverflow
+ }
return gas, nil
}

diff --git a/core/vm/instructions.go b/core/vm/instructions.go
index db507c481..f1a6112bd 100644
--- a/core/vm/instructions.go
+++ b/core/vm/instructions.go
@@ -383,6 +383,13 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext)
uint64CodeOffset = 0xffffffffffffffff
}
addr := common.Address(a.Bytes20())
+ codeSize := interpreter.evm.StateDB.GetCodeSize(addr)
+ if codeSize > params.CodeSizeUnit {
+ extraGas := (uint64(codeSize - 1)) / params.CodeSizeUnit * params.ExtcodeSizeGasEIP150
+ if !scope.Contract.UseGas(extraGas) {
+ return nil, ErrOutOfGas
+ }
+ }
codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64())
scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy)

diff --git a/params/protocol_params.go b/params/protocol_params.go
index 5f154597a..c30b54da8 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -123,7 +123,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)