From 1214e4fd811797627c60dd3928b59e56eff7d6c7 Mon Sep 17 00:00:00 2001 From: Daniel Liu Date: Thu, 22 Aug 2024 12:34:47 +0800 Subject: [PATCH] core/vm: implement EIP-3860: Limit and meter initcode (#23847) --- core/bench_test.go | 2 +- core/error.go | 4 +++ core/state_transition.go | 47 ++++++++++++++++++++------ core/txpool/txpool.go | 2 +- core/vm/eips.go | 9 +++++ core/vm/errors.go | 1 + core/vm/gas_table.go | 52 +++++++++++++++++++++++----- core/vm/gas_table_test.go | 71 +++++++++++++++++++++++++++++++++++++++ core/vm/instructions.go | 1 - core/vm/jump_table.go | 1 + eth/tracers/tracer.go | 3 +- light/txpool.go | 4 ++- params/protocol_params.go | 9 +++-- 13 files changed, 179 insertions(+), 27 deletions(-) diff --git a/core/bench_test.go b/core/bench_test.go index 1129142b1af6..a43dc6480a7e 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -85,7 +85,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) { return func(i int, gen *BlockGen) { toaddr := common.Address{} data := make([]byte, nbytes) - gas, _ := IntrinsicGas(data, nil, false, false) + gas, _ := IntrinsicGas(data, nil, false, false, false) tx, _ := types.SignTx(types.NewTransaction(gen.TxNonce(benchRootAddr), toaddr, big.NewInt(1), gas, nil, data), types.HomesteadSigner{}, benchRootKey) gen.AddTx(tx) } diff --git a/core/error.go b/core/error.go index f7f452b182ce..654a23789438 100644 --- a/core/error.go +++ b/core/error.go @@ -47,6 +47,10 @@ var ( // by a transaction is higher than what's left in the block. ErrGasLimitReached = errors.New("gas limit reached") + // ErrMaxInitCodeSizeExceeded is returned if creation transaction provides the init code bigger + // than init code size limit. + ErrMaxInitCodeSizeExceeded = errors.New("max initcode size exceeded") + // 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_transition.go b/core/state_transition.go index 60e32f70cc6b..bb7723a9c2f8 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -90,7 +90,7 @@ type Message interface { } // IntrinsicGas computes the 'intrinsic gas' for a message with the given data. -func IntrinsicGas(data []byte, accessList types.AccessList, isContractCreation, isHomestead bool) (uint64, error) { +func IntrinsicGas(data []byte, accessList types.AccessList, isContractCreation, isHomestead bool, isEIP3860 bool) (uint64, error) { // Set the starting gas for the raw transaction var gas uint64 if isContractCreation && isHomestead { @@ -98,8 +98,9 @@ func IntrinsicGas(data []byte, accessList types.AccessList, isContractCreation, } else { gas = params.TxGas } + dataLen := uint64(len(data)) // Bump the required gas by the amount of transactional data - if len(data) > 0 { + if dataLen > 0 { // Zero and non-zero bytes are priced differently var nz uint64 for _, byt := range data { @@ -113,11 +114,19 @@ func IntrinsicGas(data []byte, accessList types.AccessList, isContractCreation, } gas += nz * params.TxDataNonZeroGas - z := uint64(len(data)) - nz + z := dataLen - nz if (math.MaxUint64-gas)/params.TxDataZeroGas < z { return 0, ErrGasUintOverflow } gas += z * params.TxDataZeroGas + + if isContractCreation && isEIP3860 { + lenWords := toWordSize(dataLen) + if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords { + return 0, ErrGasUintOverflow + } + gas += lenWords * params.InitCodeWordGas + } } if accessList != nil { gas += uint64(len(accessList)) * params.TxAccessListAddressGas @@ -126,6 +135,15 @@ func IntrinsicGas(data []byte, accessList types.AccessList, isContractCreation, return gas, nil } +// toWordSize returns the ceiled word size required for init code payment calculation. +func toWordSize(size uint64) uint64 { + if size > math.MaxUint64-31 { + return math.MaxUint64/32 + 1 + } + + return (size + 31) / 32 +} + // NewStateTransition initialises and returns a new state transition object. func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool) *StateTransition { return &StateTransition{ @@ -261,14 +279,18 @@ func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedG if err = st.preCheck(); err != nil { return nil, 0, false, err, nil } - msg := st.msg - sender := st.from() // err checked in preCheck - homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber) - eip3529 := st.evm.ChainConfig().IsEIP1559(st.evm.Context.BlockNumber) - contractCreation := msg.To() == nil + + var ( + msg = st.msg + sender = st.from() // err checked in preCheck + rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber) + homestead = st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber) + eip3529 = st.evm.ChainConfig().IsEIP1559(st.evm.Context.BlockNumber) + contractCreation = msg.To() == nil + ) // Check clauses 4-5, subtract intrinsic gas if everything is correct - gas, err := IntrinsicGas(st.data, st.msg.AccessList(), contractCreation, homestead) + gas, err := IntrinsicGas(st.data, st.msg.AccessList(), contractCreation, homestead, rules.IsShanghai) if err != nil { return nil, 0, false, err, nil } @@ -277,7 +299,12 @@ func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedG } st.gas -= gas - if rules := st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber); rules.IsEIP1559 { + // Check whether the init code size has been exceeded. + if rules.IsEIP1559 && contractCreation && len(st.data) > params.MaxInitCodeSize { + return nil, 0, false, fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, len(st.data), params.MaxInitCodeSize), nil + } + + if rules.IsEIP1559 { st.state.PrepareAccessList(msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList()) } diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 60ed2fceed8e..96bfb70fe8e4 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -702,7 +702,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { if tx.To() == nil || (tx.To() != nil && !tx.IsSpecialTransaction()) { // Ensure the transaction has more gas than the basic tx fee. - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true, pool.eip1559) if err != nil { return err } diff --git a/core/vm/eips.go b/core/vm/eips.go index 426c20b3abc9..d28cb6d5eca3 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -31,6 +31,8 @@ func EnableEIP(eipNum int, jt *JumpTable) error { switch eipNum { case 3855: enable3855(jt) + case 3860: + enable3860(jt) case 3529: enable3529(jt) case 3198: @@ -182,3 +184,10 @@ func opPush0(pc *uint64, interpreter *EVMInterpreter, callContext *ScopeContext) callContext.Stack.push(new(uint256.Int)) return nil, nil } + +// ebnable3860 enables "EIP-3860: Limit and meter initcode" +// https://eips.ethereum.org/EIPS/eip-3860 +func enable3860(jt *JumpTable) { + jt[CREATE].dynamicGas = gasCreateEip3860 + jt[CREATE2].dynamicGas = gasCreate2Eip3860 +} diff --git a/core/vm/errors.go b/core/vm/errors.go index 236e22568b58..31457630ace8 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -29,6 +29,7 @@ var ( ErrInsufficientBalance = errors.New("insufficient balance for transfer") ErrContractAddressCollision = errors.New("contract address collision") ErrExecutionReverted = errors.New("execution reverted") + ErrMaxInitCodeSizeExceeded = errors.New("max initcode size exceeded") ErrMaxCodeSizeExceeded = errors.New("max code size exceeded") ErrInvalidJump = errors.New("invalid jump destination") ErrWriteProtection = errors.New("write protection") diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 45589157bb00..2287dc1f7579 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -163,19 +163,19 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi return params.NetSstoreDirtyGas, nil } -// 0. If *gasleft* is less than or equal to 2300, fail the current call. -// 1. If current value equals new value (this is a no-op), SLOAD_GAS is deducted. -// 2. If current value does not equal new value: -// 2.1. If original value equals current value (this storage slot has not been changed by the current execution context): +// 0. If *gasleft* is less than or equal to 2300, fail the current call. +// 1. If current value equals new value (this is a no-op), SLOAD_GAS is deducted. +// 2. If current value does not equal new value: +// 2.1. If original value equals current value (this storage slot has not been changed by the current execution context): // 2.1.1. If original value is 0, SSTORE_SET_GAS (20K) gas is deducted. // 2.1.2. Otherwise, SSTORE_RESET_GAS gas is deducted. If new value is 0, add SSTORE_CLEARS_SCHEDULE to refund counter. -// 2.2. If original value does not equal current value (this storage slot is dirty), SLOAD_GAS gas is deducted. Apply both of the following clauses: +// 2.2. If original value does not equal current value (this storage slot is dirty), SLOAD_GAS gas is deducted. Apply both of the following clauses: // 2.2.1. If original value is not 0: -// 2.2.1.1. If current value is 0 (also means that new value is not 0), subtract SSTORE_CLEARS_SCHEDULE gas from refund counter. -// 2.2.1.2. If new value is 0 (also means that current value is not 0), add SSTORE_CLEARS_SCHEDULE gas to refund counter. +// 2.2.1.1. If current value is 0 (also means that new value is not 0), subtract SSTORE_CLEARS_SCHEDULE gas from refund counter. +// 2.2.1.2. If new value is 0 (also means that current value is not 0), add SSTORE_CLEARS_SCHEDULE gas to refund counter. // 2.2.2. If original value equals new value (this storage slot is reset): -// 2.2.2.1. If original value is 0, add SSTORE_SET_GAS - SLOAD_GAS to refund counter. -// 2.2.2.2. Otherwise, add SSTORE_RESET_GAS - SLOAD_GAS gas to refund counter. +// 2.2.2.1. If original value is 0, add SSTORE_SET_GAS - SLOAD_GAS to refund counter. +// 2.2.2.2. Otherwise, add SSTORE_RESET_GAS - SLOAD_GAS gas to refund counter. func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { // If we fail the minimum gas availability invariant, fail (0) if contract.Gas <= params.SstoreSentryGasEIP2200 { @@ -300,6 +300,40 @@ func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memoryS return gas, nil } +func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return 0, err + } + size, overflow := stack.Back(2).Uint64WithOverflow() + if overflow || size > params.MaxInitCodeSize { + return 0, ErrGasUintOverflow + } + // Since size <= params.MaxInitCodeSize, these multiplication cannot overflow + moreGas := params.InitCodeWordGas * ((size + 31) / 32) + if gas, overflow = math.SafeAdd(gas, moreGas); overflow { + return 0, ErrGasUintOverflow + } + return gas, nil +} + +func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return 0, err + } + size, overflow := stack.Back(2).Uint64WithOverflow() + if overflow || size > params.MaxInitCodeSize { + return 0, ErrGasUintOverflow + } + // Since size <= params.MaxInitCodeSize, these multiplication cannot overflow + moreGas := (params.InitCodeWordGas + params.Keccak256WordGas) * ((size + 31) / 32) + if gas, overflow = math.SafeAdd(gas, moreGas); overflow { + return 0, ErrGasUintOverflow + } + return gas, nil +} + func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { expByteLen := uint64((stack.data[stack.len()-2].BitLen() + 7) / 8) diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go index 501a0364c65b..a239ab44d1c2 100644 --- a/core/vm/gas_table_test.go +++ b/core/vm/gas_table_test.go @@ -17,8 +17,10 @@ package vm import ( + "bytes" "math" "math/big" + "sort" "testing" "github.com/XinFinOrg/XDPoSChain/core/rawdb" @@ -106,3 +108,72 @@ func TestEIP2200(t *testing.T) { } } } + +var createGasTests = []struct { + code string + eip3860 bool + gasUsed uint64 + minimumGas uint64 +}{ + // legacy create(0, 0, 0xc000) without 3860 used + {"0x61C00060006000f0" + "600052" + "60206000F3", false, 41237, 41237}, + // legacy create(0, 0, 0xc000) _with_ 3860 + {"0x61C00060006000f0" + "600052" + "60206000F3", true, 44309, 44309}, + // create2(0, 0, 0xc001, 0) without 3860 + {"0x600061C00160006000f5" + "600052" + "60206000F3", false, 50471, 100_000}, + // create2(0, 0, 0xc001, 0) (too large), with 3860 + {"0x600061C00160006000f5" + "600052" + "60206000F3", true, 32012, 100_000}, + // create2(0, 0, 0xc000, 0) + // This case is trying to deploy code at (within) the limit + {"0x600061C00060006000f5" + "600052" + "60206000F3", true, 53528, 100_000}, + // create2(0, 0, 0xc001, 0) + // This case is trying to deploy code exceeding the limit + {"0x600061C00160006000f5" + "600052" + "60206000F3", true, 32024, 100_000}} + +func TestCreateGas(t *testing.T) { + for i, tt := range createGasTests { + var gasUsed = uint64(0) + doCheck := func(testGas int) bool { + address := common.BytesToAddress([]byte("contract")) + statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase())) + statedb.CreateAccount(address) + statedb.SetCode(address, hexutil.MustDecode(tt.code)) + statedb.Finalise(true) + vmctx := Context{ + CanTransfer: func(StateDB, common.Address, *big.Int) bool { return true }, + Transfer: func(StateDB, common.Address, common.Address, *big.Int) {}, + BlockNumber: big.NewInt(0), + } + config := Config{} + if tt.eip3860 { + config.ExtraEips = []int{3860} + } + + vmenv := NewEVM(vmctx, statedb, nil, params.AllEthashProtocolChanges, config) + var startGas = uint64(testGas) + ret, gas, err := vmenv.Call(AccountRef(common.Address{}), address, nil, startGas, new(big.Int)) + if err != nil { + return false + } + gasUsed = startGas - gas + if len(ret) != 32 { + t.Fatalf("test %d: expected 32 bytes returned, have %d", i, len(ret)) + } + if bytes.Equal(ret, make([]byte, 32)) { + // Failure + return false + } + return true + } + minGas := sort.Search(100_000, doCheck) + if uint64(minGas) != tt.minimumGas { + t.Fatalf("test %d: min gas error, want %d, have %d", i, tt.minimumGas, minGas) + } + // If the deployment succeeded, we also check the gas used + if minGas < 100_000 { + if gasUsed != tt.gasUsed { + t.Errorf("test %d: gas used mismatch: have %v, want %v", i, gasUsed, tt.gasUsed) + } + } + } +} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 361fd31e1c3a..f31ea3b2478a 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -640,7 +640,6 @@ func opCreate2(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([] input = scope.Memory.GetCopy(int64(offset.Uint64()), int64(size.Uint64())) gas = scope.Contract.Gas ) - // Apply EIP150 gas -= gas / 64 scope.Contract.UseGas(gas) diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 2fa70810e608..f50b95f27ba4 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -64,6 +64,7 @@ func newEip1559InstructionSet() JumpTable { instructionSet := newShanghaiInstructionSet() enable2929(&instructionSet) // Gas cost increases for state access opcodes https://eips.ethereum.org/EIPS/eip-2929 enable3529(&instructionSet) // EIP-3529: Reduction in refunds https://eips.ethereum.org/EIPS/eip-3529 + enable3860(&instructionSet) // Limit and meter initcode return instructionSet } diff --git a/eth/tracers/tracer.go b/eth/tracers/tracer.go index bbec5ad8a952..cb1b7c7bbb1a 100644 --- a/eth/tracers/tracer.go +++ b/eth/tracers/tracer.go @@ -682,9 +682,10 @@ func (jst *JsTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Ad // Compute intrinsic gas isHomestead := env.ChainConfig().IsHomestead(env.Context.BlockNumber) + isEIP1559 := env.ChainConfig().IsEIP1559(env.Context.BlockNumber) // after update core.IntrinsicGas, use isIstanbul in it // isIstanbul := env.ChainConfig().IsIstanbul(env.Context.BlockNumber) - intrinsicGas, err := core.IntrinsicGas(input, nil, jst.ctx["type"] == "CREATE", isHomestead) + intrinsicGas, err := core.IntrinsicGas(input, nil, jst.ctx["type"] == "CREATE", isHomestead, isEIP1559) if err != nil { return } diff --git a/light/txpool.go b/light/txpool.go index b84195dc165d..f84b9dc33e2a 100644 --- a/light/txpool.go +++ b/light/txpool.go @@ -69,6 +69,7 @@ type TxPool struct { homestead bool eip2718 bool // Fork indicator whether we are in the eip2718 stage. + eip1559 bool // Fork indicator whether we are in the eip1559 stage. } // TxRelayBackend provides an interface to the mechanism that forwards transacions @@ -317,6 +318,7 @@ func (pool *TxPool) setNewHead(head *types.Header) { next := new(big.Int).Add(head.Number, big.NewInt(1)) pool.homestead = pool.config.IsHomestead(head.Number) pool.eip2718 = pool.config.IsEIP1559(next) + pool.eip1559 = pool.config.IsEIP1559(next) } // Stop stops the light transaction pool @@ -408,7 +410,7 @@ func (pool *TxPool) validateTx(ctx context.Context, tx *types.Transaction) error } // Should supply enough intrinsic gas - gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, pool.homestead) + gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, pool.homestead, pool.eip1559) if err != nil { return err } diff --git a/params/protocol_params.go b/params/protocol_params.go index 098f69403c60..389a1086b7a7 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -42,8 +42,10 @@ const ( LogDataGas uint64 = 8 // Per byte in a LOG* operation's data. CallStipend uint64 = 2300 // Free gas given at beginning of call. - Keccak256Gas uint64 = 30 // Once per KECCAK256 operation. - Keccak256WordGas uint64 = 6 // Once per word of the KECCAK256 operation's data. + Keccak256Gas uint64 = 30 // Once per KECCAK256 operation. + Keccak256WordGas uint64 = 6 // Once per word of the KECCAK256 operation's data. + InitCodeWordGas uint64 = 2 // Once per word of the init code when creating a contract. + SstoreResetGas uint64 = 5000 // Once per SSTORE operation if the zeroness changes from zero. SstoreClearGas uint64 = 5000 // Once per SSTORE operation if the zeroness doesn't change. SstoreRefundGas uint64 = 15000 // Once per SSTORE operation if the zeroness changes to zero. @@ -69,7 +71,8 @@ const ( InitialBaseFee = 12500000000 // Initial base fee for EIP-1559 blocks. - MaxCodeSize = 24576 // Maximum bytecode to permit for a contract + MaxCodeSize = 24576 // Maximum bytecode to permit for a contract + MaxInitCodeSize = 2 * MaxCodeSize // Maximum initcode to permit in a creation transaction and create instructions // Precompiled contract gas prices