diff --git a/packages/common/src/eips/3860.json b/packages/common/src/eips/3860.json new file mode 100644 index 0000000000..a9a29a9b40 --- /dev/null +++ b/packages/common/src/eips/3860.json @@ -0,0 +1,23 @@ +{ + "name": "EIP-3860", + "number": 3860, + "comment": "Limit and meter initcode", + "url": "https://eips.ethereum.org/EIPS/eip-3860", + "status": "Review", + "minimumHardfork": "spuriousDragon", + "requiredEIPs": [], + "gasConfig": {}, + "gasPrices": { + "initCodeWordCost": { + "v": 2, + "d": "Gas to pay for each word (32 bytes) of initcode when creating a contract" + } + }, + "vm": { + "maxInitCodeSize": { + "v": 49152, + "d": "Maximum length of initialization code when creating a contract" + } + }, + "pow": {} + } \ No newline at end of file diff --git a/packages/common/src/eips/index.ts b/packages/common/src/eips/index.ts index f8b004bfaf..c8e4b7ee0e 100644 --- a/packages/common/src/eips/index.ts +++ b/packages/common/src/eips/index.ts @@ -15,6 +15,7 @@ export const EIPs: eipsType = { 3607: require('./3607.json'), 3675: require('./3675.json'), 3855: require('./3855.json'), + 3860: require('./3860.json'), 4345: require('./4345.json'), 4399: require('./4399.json'), } diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index 6917a6068b..f9e07ce1f2 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -187,12 +187,21 @@ export abstract class BaseTransaction { const txDataZero = this.common.param('gasPrices', 'txDataZero') const txDataNonZero = this.common.param('gasPrices', 'txDataNonZero') - let cost = 0 + let cost: number | BN = 0 for (let i = 0; i < this.data.length; i++) { this.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero) } - return new BN(cost) + cost = new BN(cost) + if ((this.to === undefined || this.to === null) && this.common.isActivatedEIP(3860)) { + const dataLength = Math.ceil(this.data.length / 32) + const initCodeCost = new BN(this.common.param('gasPrices', 'initCodeWordCost')).imuln( + dataLength + ) + cost.iadd(initCodeCost) + } + + return cost } /** diff --git a/packages/tx/src/eip1559Transaction.ts b/packages/tx/src/eip1559Transaction.ts index 032acebce5..4a85dec630 100644 --- a/packages/tx/src/eip1559Transaction.ts +++ b/packages/tx/src/eip1559Transaction.ts @@ -20,7 +20,7 @@ import { N_DIV_2, TxOptions, } from './types' -import { AccessLists } from './util' +import { AccessLists, checkMaxInitCodeSize } from './util' const TRANSACTION_TYPE = 2 const TRANSACTION_TYPE_BUFFER = Buffer.from(TRANSACTION_TYPE.toString(16).padStart(2, '0'), 'hex') @@ -235,6 +235,10 @@ export default class FeeMarketEIP1559Transaction extends BaseTransaction { } } + if (this.common.isActivatedEIP(3860)) { + checkMaxInitCodeSize(this.common, this.data.length) + } + const freeze = opts?.freeze ?? true if (freeze) { Object.freeze(this) diff --git a/packages/tx/src/util.ts b/packages/tx/src/util.ts index 0175d1ebd0..184d25cb5c 100644 --- a/packages/tx/src/util.ts +++ b/packages/tx/src/util.ts @@ -2,6 +2,17 @@ import Common from '@ethereumjs/common' import { bufferToHex, setLengthLeft, toBuffer } from 'ethereumjs-util' import { AccessList, AccessListBuffer, AccessListItem, isAccessList } from './types' +export function checkMaxInitCodeSize(common: Common, length: number) { + if (length > common.param('vm', 'maxInitCodeSize')) { + throw new Error( + `the initcode size of this transaction is too large: it is ${length} while the max is ${common.param( + 'vm', + 'maxInitCodeSize' + )}` + ) + } +} + export class AccessLists { public static getAccessListData(accessList: AccessListBuffer | AccessList) { let AccessListJSON diff --git a/packages/tx/test/transactionRunner.ts b/packages/tx/test/transactionRunner.ts index b70af21af2..2d1127763e 100644 --- a/packages/tx/test/transactionRunner.ts +++ b/packages/tx/test/transactionRunner.ts @@ -10,6 +10,7 @@ const argv = minimist(process.argv.slice(2)) const file: string | undefined = argv.file const forkNames: ForkName[] = [ + 'London+3860', 'London', 'Berlin', 'Istanbul', @@ -23,6 +24,7 @@ const forkNames: ForkName[] = [ ] const forkNameMap: ForkNamesMap = { + 'London+3860': 'london', London: 'london', Berlin: 'berlin', Istanbul: 'istanbul', @@ -35,6 +37,10 @@ const forkNameMap: ForkNamesMap = { Homestead: 'homestead', } +const EIPs: any = { + 'London+3860': [3860], +} + tape('TransactionTests', async (t) => { const fileFilterRegex = file ? new RegExp(file + '[^\\w]') : undefined await getTests( @@ -46,6 +52,9 @@ tape('TransactionTests', async (t) => { ) => { t.test(testName, (st) => { for (const forkName of forkNames) { + if (testData.result[forkName] === undefined) { + continue + } const forkTestData = testData.result[forkName] const shouldBeInvalid = !!forkTestData.exception @@ -53,6 +62,10 @@ tape('TransactionTests', async (t) => { const rawTx = toBuffer(testData.txbytes) const hardfork = forkNameMap[forkName] const common = new Common({ chain: 1, hardfork }) + const activateEIPs = EIPs[forkName] + if (activateEIPs) { + common.setEIPs(activateEIPs) + } const tx = Transaction.fromSerializedTx(rawTx, { common }) const sender = tx.getSenderAddress().toString() const hash = tx.hash().toString('hex') diff --git a/packages/tx/test/types.ts b/packages/tx/test/types.ts index b98a45b4ca..c37d36a18b 100644 --- a/packages/tx/test/types.ts +++ b/packages/tx/test/types.ts @@ -1,4 +1,5 @@ export type ForkName = + | 'London+3860' | 'London' | 'Berlin' | 'Istanbul' diff --git a/packages/vm/src/evm/eei.ts b/packages/vm/src/evm/eei.ts index 5cdad440a9..d567cb213f 100644 --- a/packages/vm/src/evm/eei.ts +++ b/packages/vm/src/evm/eei.ts @@ -600,9 +600,16 @@ export default class EEI { if (this._env.contract.nonce.gte(MAX_UINT64)) { return new BN(0) } + this._env.contract.nonce.iaddn(1) await this._state.putAccount(this._env.address, this._env.contract) + if (this._common.isActivatedEIP(3860)) { + if (msg.data.length > this._common.param('vm', 'maxInitCodeSize')) { + return new BN(0) + } + } + const results = await this._evm.executeMessage(msg) if (results.execResult.logs) { diff --git a/packages/vm/src/evm/evm.ts b/packages/vm/src/evm/evm.ts index 1f6b49d1f7..eaf4d4da9b 100644 --- a/packages/vm/src/evm/evm.ts +++ b/packages/vm/src/evm/evm.ts @@ -298,6 +298,20 @@ export default class EVM { // Reduce tx value from sender await this._reduceSenderBalance(account, message) + if (this._vm._common.isActivatedEIP(3860)) { + if (message.data.length > this._vm._common.param('vm', 'maxInitCodeSize')) { + return { + gasUsed: message.gasLimit, + createdAddress: message.to, + execResult: { + returnValue: Buffer.alloc(0), + exceptionError: new VmError(ERROR.INITCODE_SIZE_VIOLATION), + gasUsed: message.gasLimit, + }, + } + } + } + message.code = message.data message.data = Buffer.alloc(0) message.to = await this._generateAddress(message) diff --git a/packages/vm/src/evm/opcodes/gas.ts b/packages/vm/src/evm/opcodes/gas.ts index 4d5eaf40ce..471b3ab741 100644 --- a/packages/vm/src/evm/opcodes/gas.ts +++ b/packages/vm/src/evm/opcodes/gas.ts @@ -262,6 +262,14 @@ export const dynamicGasHandlers: Map = new Map< gas.iadd(subMemUsage(runState, offset, length, common)) + if (common.isActivatedEIP(3860)) { + // Meter initcode + const initCodeCost = new BN(common.param('gasPrices', 'initCodeWordCost')).imul( + length.addn(31).divn(32) + ) + gas.iadd(initCodeCost) + } + let gasLimit = new BN(runState.eei.getGasLeft().isub(gas)) gasLimit = maxCallGas(gasLimit.clone(), gasLimit.clone(), runState, common) @@ -419,6 +427,15 @@ export const dynamicGasHandlers: Map = new Map< } gas.iadd(new BN(common.param('gasPrices', 'sha3Word')).imul(divCeil(length, new BN(32)))) + + if (common.isActivatedEIP(3860)) { + // Meter initcode + const initCodeCost = new BN(common.param('gasPrices', 'initCodeWordCost')).imul( + length.addn(31).divn(32) + ) + gas.iadd(initCodeCost) + } + let gasLimit = new BN(runState.eei.getGasLeft().isub(gas)) gasLimit = maxCallGas(gasLimit.clone(), gasLimit.clone(), runState, common) // CREATE2 is only available after TangerineWhistle (Constantinople introduced this opcode) runState.messageGasLimit = gasLimit diff --git a/packages/vm/src/exceptions.ts b/packages/vm/src/exceptions.ts index c1a6c616c3..541ad114c4 100644 --- a/packages/vm/src/exceptions.ts +++ b/packages/vm/src/exceptions.ts @@ -17,6 +17,7 @@ export enum ERROR { INVALID_RETURNSUB = 'invalid RETURNSUB', INVALID_JUMPSUB = 'invalid JUMPSUB', INVALID_BYTECODE_RESULT = 'invalid bytecode deployed', + INITCODE_SIZE_VIOLATION = 'initcode exceeds max initcode size', // BLS errors BLS_12_381_INVALID_INPUT_LENGTH = 'invalid input length', diff --git a/packages/vm/src/index.ts b/packages/vm/src/index.ts index 4963d018d7..7b0ec7f428 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -52,6 +52,7 @@ export interface VMOpts { * - [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529) - Reduction in refunds * - [EIP-3541](https://eips.ethereum.org/EIPS/eip-3541) - Reject new contracts starting with the 0xEF byte * - [EIP-3855](https://eips.ethereum.org/EIPS/eip-3855) - PUSH0 instruction + * - [EIP-3860](https://eips.ethereum.org/EIPS/eip-3860) - Limit and meter initcode * * *Annotations:* * @@ -195,7 +196,9 @@ export default class VM extends AsyncEventEmitter { if (opts.common) { // Supported EIPs - const supportedEIPs = [1559, 2315, 2537, 2565, 2718, 2929, 2930, 3198, 3529, 3541, 3607, 3855] + const supportedEIPs = [ + 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3198, 3529, 3541, 3607, 3855, 3860, + ] for (const eip of opts.common.eips()) { if (!supportedEIPs.includes(eip)) { throw new Error(`EIP-${eip} is not supported by the VM`) diff --git a/packages/vm/tests/api/EIPs/eip-3860.spec.ts b/packages/vm/tests/api/EIPs/eip-3860.spec.ts new file mode 100644 index 0000000000..f89b6ec42d --- /dev/null +++ b/packages/vm/tests/api/EIPs/eip-3860.spec.ts @@ -0,0 +1,40 @@ +import tape from 'tape' +import VM from '../../../src' +import Common, { Chain, Hardfork } from '@ethereumjs/common' +import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx' +import { Address, BN, privateToAddress } from 'ethereumjs-util' +const pkey = Buffer.from('20'.repeat(32), 'hex') +const GWEI = new BN('1000000000') +const sender = new Address(privateToAddress(pkey)) + +tape('EIP 3860 tests', (t) => { + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + eips: [3860], + }) + + t.test('EIP-3860 tests', async (st) => { + const vm = new VM({ common }) + const account = await vm.stateManager.getAccount(sender) + const balance = GWEI.muln(21000).muln(10000000) + account.balance = balance + await vm.stateManager.putAccount(sender, account) + + const buffer = Buffer.allocUnsafe(1000000).fill(0x60) + console.log(common.isActivatedEIP(3860)) + const tx = FeeMarketEIP1559Transaction.fromTxData({ + data: + '0x7F6000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060005260206000F3' + + buffer.toString('hex'), + gasLimit: 100000000000, + maxFeePerGas: 7, + nonce: 0, + }).sign(pkey) + const result = await vm.runTx({ tx }) + st.ok( + (result.execResult.exceptionError?.error as string) === 'initcode exceeds max initcode size', + 'initcode exceeds max size' + ) + }) +})