From 256b107a18b01d55b2ee270eb6d2279765878a90 Mon Sep 17 00:00:00 2001 From: jgresham Date: Fri, 2 May 2025 12:28:42 -0700 Subject: [PATCH 1/7] evm: add eip-7907 to osaka (wip) --- packages/common/src/eips.ts | 8 + packages/common/src/hardforks.ts | 4 +- packages/evm/src/evm.ts | 16 +- packages/evm/src/interpreter.ts | 2 +- packages/evm/src/opcodes/functions.ts | 4 +- packages/evm/src/params.ts | 9 + .../evm/test/eips/eip-7907-initcode.spec.ts | 282 ++++++++++++++++++ packages/tx/src/params.ts | 9 + 8 files changed, 325 insertions(+), 9 deletions(-) create mode 100644 packages/evm/test/eips/eip-7907-initcode.spec.ts diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 58bd81db913..88428c41f40 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -478,4 +478,12 @@ export const eipsDict: EIPsDict = { 7864: { minimumHardfork: Hardfork.London, }, + /** + * Description : Meter Contract Code Size And Increase Limit + * URL : https://eips.ethereum.org/EIPS/eip-7907 + * Status : Draft + */ + 7907: { + minimumHardfork: Hardfork.Osaka, + }, } diff --git a/packages/common/src/hardforks.ts b/packages/common/src/hardforks.ts index 6645984ddaf..3c11b5e1d88 100644 --- a/packages/common/src/hardforks.ts +++ b/packages/common/src/hardforks.ts @@ -162,11 +162,11 @@ export const hardforksDict: HardforksDict = { }, /** * Description: Next feature hardfork after prague, internally used for peerdas/EOF testing/implementation (incomplete/experimental) - * URL : https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/osaka.md + * URL : https://eips.ethereum.org/EIPS/eip-7607 * Status : Final */ osaka: { - eips: [663, 3540, 3670, 4200, 4750, 5450, 6206, 7069, 7480, 7620, 7692, 7698], + eips: [663, 3540, 3670, 4200, 4750, 5450, 6206, 7069, 7480, 7620, 7692, 7698, 7907], }, /** * Description: Next feature hardfork after osaka, internally used for verkle testing/implementation (incomplete/experimental) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 5e157b96171..2bd265b801e 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -251,7 +251,7 @@ export class EVM implements EVMInterface { const supportedEIPs = [ 663, 1153, 1559, 2537, 2565, 2718, 2929, 2930, 2935, 3198, 3529, 3540, 3541, 3607, 3651, 3670, 3855, 3860, 4200, 4399, 4750, 4788, 4844, 4895, 5133, 5450, 5656, 6110, 6206, 6780, 6800, - 7002, 7069, 7251, 7480, 7516, 7620, 7685, 7691, 7692, 7698, 7702, 7709, + 7002, 7069, 7251, 7480, 7516, 7620, 7685, 7691, 7692, 7698, 7702, 7709, 7907, ] for (const eip of this.common.eips()) { @@ -500,7 +500,7 @@ export class EVM implements EVMInterface { // Reduce tx value from sender await this._reduceSenderBalance(account, message) - if (this.common.isActivatedEIP(3860)) { + if (this.common.isActivatedEIP(3860) || this.common.isActivatedEIP(7907)) { if ( message.data.length > Number(this.common.param('maxInitCodeSize')) && !this.allowUnlimitedInitCodeSize @@ -670,7 +670,7 @@ export class EVM implements EVMInterface { } } - // Check for SpuriousDragon EIP-170 code size limit + // Check for SpuriousDragon EIP-170 or Osaka EIP-7907 code size limit let allowedCodeSize = true if ( !result.exceptionError && @@ -707,7 +707,15 @@ export class EVM implements EVMInterface { if (this.common.gteHardfork(Hardfork.Homestead)) { if (!allowedCodeSize) { if (this.DEBUG) { - debug(`Code size exceeds maximum code size (>= SpuriousDragon)`) + if (this.common.isActivatedEIP(7907)) { + debug( + `Code size exceeds maximum code size ${this.common.param('maxCodeSize')}KB (>= Osaka)`, + ) + } else { + debug( + `Code size exceeds maximum code size ${this.common.param('maxCodeSize')}KB (>= SpuriousDragon)`, + ) + } } result = { ...result, ...CodesizeExceedsMaximumError(message.gasLimit) } } else { diff --git a/packages/evm/src/interpreter.ts b/packages/evm/src/interpreter.ts index a56ad9d09d8..a364549afc9 100644 --- a/packages/evm/src/interpreter.ts +++ b/packages/evm/src/interpreter.ts @@ -1123,7 +1123,7 @@ export class Interpreter { this._env.contract.nonce += BIGINT_1 await this.journal.putAccount(this._env.address, this._env.contract) - if (this.common.isActivatedEIP(3860)) { + if (this.common.isActivatedEIP(3860) || this.common.isActivatedEIP(7907)) { if ( codeToRun.length > Number(this.common.param('maxInitCodeSize')) && this._evm.allowUnlimitedInitCodeSize === false diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index e686fbd2faa..e618b5a2c20 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -1340,7 +1340,7 @@ export const handlers: Map = new Map([ const [value, offset, length] = runState.stack.popN(3) if ( - common.isActivatedEIP(3860) && + (common.isActivatedEIP(3860) || common.isActivatedEIP(7907)) && length > Number(common.param('maxInitCodeSize')) && !runState.interpreter._evm.allowUnlimitedInitCodeSize ) { @@ -1376,7 +1376,7 @@ export const handlers: Map = new Map([ const [value, offset, length, salt] = runState.stack.popN(4) if ( - common.isActivatedEIP(3860) && + (common.isActivatedEIP(3860) || common.isActivatedEIP(7907)) && length > Number(common.param('maxInitCodeSize')) && !runState.interpreter._evm.allowUnlimitedInitCodeSize ) { diff --git a/packages/evm/src/params.ts b/packages/evm/src/params.ts index 4b28e728ffb..6175b8a0bcc 100644 --- a/packages/evm/src/params.ts +++ b/packages/evm/src/params.ts @@ -409,4 +409,13 @@ export const paramsEVM: ParamsDict = { eofcreateGas: 32000, // Base fee of the EOFCREATE opcode (Same as CREATE/CREATE2) returncontractGas: 0, // Base fee of the RETURNCONTRACT opcode }, + /** + * Meter Contract Code Size And Increase Limit + */ + 7907: { + // Maximum length of contract code + maxCodeSize: 262144, // 256 × 1024 = 262,144 bytes (0x40000) + // Maximum length of initialization code when creating a contract + maxInitCodeSize: 524288, // 512 × 1024 = 524,288 bytes (0x80000) + }, } diff --git a/packages/evm/test/eips/eip-7907-initcode.spec.ts b/packages/evm/test/eips/eip-7907-initcode.spec.ts new file mode 100644 index 00000000000..6192920543f --- /dev/null +++ b/packages/evm/test/eips/eip-7907-initcode.spec.ts @@ -0,0 +1,282 @@ +import { Common, Hardfork, Mainnet } from '@ethereumjs/common' +import { + Address, + concatBytes, + // createAddressFromString, + // equalsBytes, + hexToBytes, + privateToAddress, +} from '@ethereumjs/util' +import { assert, describe, it } from 'vitest' + +import { createEVM } from '../../src/index.ts' + +const pkey = hexToBytes(`0x${'20'.repeat(32)}`) +const sender = new Address(privateToAddress(pkey)) + +describe('EIP 7907 initcode size tests', () => { + it('code 512KB exceeds max initcode size', async () => { + const common = new Common({ + chain: Mainnet, + hardfork: Hardfork.Osaka, + eips: [7907], + }) + const evm = await createEVM({ + common, + }) + + // 524,288 (512KB) length, 8 bit array, filled + const buffer = new Uint8Array(524288).fill(0x60) + + // setup the call arguments + const runCallArgs = { + sender, // call address + gasLimit: BigInt(0xffffffffff), // ensure we pass a lot of gas, so we do not run out of gas + // Simple test, PUSH PUSH 0 RETURN + // It tries to deploy a contract too large, where the code is all zeros + // (since memory which is not allocated/resized to yet is always defaulted to 0) + data: concatBytes( + hexToBytes( + '0x7F6000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060005260206000F3', + ), + buffer, + ), + } + const result = await evm.runCall(runCallArgs) + assert.isTrue( + (result.execResult.exceptionError?.error as string) === 'initcode exceeds max initcode size', + 'initcode exceeds max size', + ) + }) + + it('code 512KB - 100 bytes does not exceed max initcode size', async () => { + const common = new Common({ + chain: Mainnet, + hardfork: Hardfork.Osaka, + eips: [7907], + }) + const evm = await createEVM({ + common, + }) + + // 524288 - 100("512K-100") length, 8 bit array, filled + const buffer = new Uint8Array(524188).fill(0x60) + + // setup the call arguments + const runCallArgs = { + sender, // call address + gasLimit: BigInt(0xffffffffff), // ensure we pass a lot of gas, so we do not run out of gas + // Simple test, PUSH PUSH 0 RETURN + // It tries to deploy a contract too large, where the code is all zeros + // (since memory which is not allocated/resized to yet is always defaulted to 0) + data: concatBytes( + hexToBytes( + '0x7F6000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060005260206000F3', + ), + buffer, + ), + } + const result = await evm.runCall(runCallArgs) + assert.isTrue( + result.execResult.exceptionError === undefined, + 'successfully created a contract with data size 250KB (> 24KB and < 256KB)', + ) + }) + + // it('ensure EIP-7907 gas is applied on CREATE calls', async () => { + // // Transaction/Contract data taken from https://github.com/ethereum/tests/pull/990 + // const commonWith7907 = new Common({ + // chain: Mainnet, + // hardfork: Hardfork.Osaka, + // eips: [7907], + // }) + // const commonWithout7907 = new Common({ + // chain: Mainnet, + // hardfork: Hardfork.Osaka, + // eips: [], + // }) + // const caller = createAddressFromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') + // const evm = await createEVM({ + // common: commonWith7907, + // }) + // const evmWithout7907 = await createEVM({ + // common: commonWithout7907, + // }) + // const contractFactory = createAddressFromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') + // const contractAccount = await evm.stateManager.getAccount(contractFactory) + // await evm.stateManager.putAccount(contractFactory, contractAccount!) + // await evmWithout7907.stateManager.putAccount(contractFactory, contractAccount!) + // const factoryCode = hexToBytes( + // '0x7f600a80600080396000f3000000000000000000000000000000000000000000006000526000355a8160006000f05a8203600a55806000556001600155505050', + // ) + + // await evm.stateManager.putCode(contractFactory, factoryCode) + // await evmWithout7907.stateManager.putCode(contractFactory, factoryCode) + // const data = hexToBytes('0x000000000000000000000000000000000000000000000000000000000000c000') + // const runCallArgs = { + // from: caller, + // to: contractFactory, + // data, + // gasLimit: BigInt(0xfffffffff), + // } + // const res = await evm.runCall(runCallArgs) + // const res2 = await evmWithout7907.runCall(runCallArgs) + // assert.isTrue( + // res.execResult.executionGasUsed > res2.execResult.executionGasUsed, + // 'execution gas used is higher with EIP 7907 active', + // ) + // }) + + // it('ensure EIP-7907 gas is applied on CREATE2 calls', async () => { + // // Transaction/Contract data taken from https://github.com/ethereum/tests/pull/990 + // const commonWith7907 = new Common({ + // chain: Mainnet, + // hardfork: Hardfork.Osaka, + // eips: [7907], + // }) + // const commonWithout7907 = new Common({ + // chain: Mainnet, + // hardfork: Hardfork.Osaka, + // eips: [], + // }) + // const caller = createAddressFromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') + // const evm = await createEVM({ + // common: commonWith7907, + // }) + // const evmWithout7907 = await createEVM({ + // common: commonWithout7907, + // }) + // const contractFactory = createAddressFromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') + // const contractAccount = await evm.stateManager.getAccount(contractFactory) + // await evm.stateManager.putAccount(contractFactory, contractAccount!) + // await evmWithout7907.stateManager.putAccount(contractFactory, contractAccount!) + // const factoryCode = hexToBytes( + // '0x7f600a80600080396000f3000000000000000000000000000000000000000000006000526000355a60008260006000f55a8203600a55806000556001600155505050', + // ) + + // await evm.stateManager.putCode(contractFactory, factoryCode) + // await evmWithout7907.stateManager.putCode(contractFactory, factoryCode) + // const data = hexToBytes('0x000000000000000000000000000000000000000000000000000000000000c000') + // const runCallArgs = { + // from: caller, + // to: contractFactory, + // data, + // gasLimit: BigInt(0xfffffffff), + // } + // const res = await evm.runCall(runCallArgs) + // const res2 = await evmWithout7907.runCall(runCallArgs) + // assert.isTrue( + // res.execResult.executionGasUsed > res2.execResult.executionGasUsed, + // 'execution gas used is higher with EIP 7907 active', + // ) + // }) + + it('code exceeds max initcode size: allowUnlimitedInitCodeSize active', async () => { + const common = new Common({ + chain: Mainnet, + hardfork: Hardfork.Osaka, + eips: [7907], + }) + const evm = await createEVM({ + common, + allowUnlimitedInitCodeSize: true, + }) + + const bytes = new Uint8Array(1000000).fill(0x60) + + // setup the call arguments + const runCallArgs = { + sender, // call address + gasLimit: BigInt(0xffffffffff), // ensure we pass a lot of gas, so we do not run out of gas + // Simple test, PUSH PUSH 0 RETURN + // It tries to deploy a contract too large, where the code is all zeros + // (since memory which is not allocated/resized to yet is always defaulted to 0) + data: concatBytes( + hexToBytes(`0x${'00'.repeat(Number(common.param('maxInitCodeSize')) + 1)}`), + bytes, + ), + } + const result = await evm.runCall(runCallArgs) + assert.isTrue( + result.execResult.exceptionError === undefined, + 'successfully created a contract with data size > MAX_INITCODE_SIZE and allowUnlimitedInitCodeSize active', + ) + }) + + // it('CREATE with MAX_INITCODE_SIZE+1, allowUnlimitedContractSize active', async () => { + // const commonWith7907 = new Common({ + // chain: Mainnet, + // hardfork: Hardfork.Osaka, + // eips: [7907], + // }) + // const caller = createAddressFromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') + // for (const code of ['F0', 'F5']) { + // const evm = await createEVM({ + // common: commonWith7907, + + // allowUnlimitedInitCodeSize: true, + // }) + // const evmDisabled = await createEVM({ + // common: commonWith7907, + // allowUnlimitedInitCodeSize: false, + // }) + // const contractFactory = createAddressFromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') + // const contractAccount = await evm.stateManager.getAccount(contractFactory) + // await evm.stateManager.putAccount(contractFactory, contractAccount!) + // await evmDisabled.stateManager.putAccount(contractFactory, contractAccount!) + // // This factory code: + // // -> reads 32 bytes from the calldata (X) + // // Attempts to create a contract of X size + // // (the initcode of this contract is just zeros, so STOP opcode + // // It stores the topmost stack item of this CREATE(2) at slot 0 + // // This is either the contract address if it was successful, or 0 in case of error + // const factoryCode = hexToBytes(`0x600060003560006000${code}600055`) + + // await evm.stateManager.putCode(contractFactory, factoryCode) + // await evmDisabled.stateManager.putCode(contractFactory, factoryCode) + + // const runCallArgs = { + // from: caller, + // to: contractFactory, + // gasLimit: BigInt(0xfffffffff), + // data: hexToBytes(`0x${'00'.repeat(30)}C001`), + // } + + // const res = await evm.runCall(runCallArgs) + // await evmDisabled.runCall(runCallArgs) + + // const key0 = hexToBytes(`0x${'00'.repeat(32)}`) + // const storageActive = await evm.stateManager.getStorage(contractFactory, key0) + // const storageInactive = await evmDisabled.stateManager.getStorage(contractFactory, key0) + + // assert.isTrue( + // !equalsBytes(storageActive, new Uint8Array()), + // 'created contract with MAX_INITCODE_SIZE + 1 length, allowUnlimitedInitCodeSize=true', + // ) + // assert.isTrue( + // equalsBytes(storageInactive, new Uint8Array()), + // 'did not create contract with MAX_INITCODE_SIZE + 1 length, allowUnlimitedInitCodeSize=false', + // ) + + // // gas check + + // const runCallArgs2 = { + // from: caller, + // to: contractFactory, + // gasLimit: BigInt(0xfffffffff), + // data: hexToBytes(`0x${'00'.repeat(30)}C000`), + // } + + // // Test: + // // On the `allowUnlimitedInitCodeSize = true`, create contract with MAX_INITCODE_SIZE + 1 + // // On `allowUnlimitedInitCodeSize = false`, create contract with MAX_INITCODE_SIZE + // // Verify that the gas cost on the prior one is higher than the first one + // const res2 = await evmDisabled.runCall(runCallArgs2) + + // assert.isTrue( + // res.execResult.executionGasUsed > res2.execResult.executionGasUsed, + // 'charged initcode analysis gas cost on both allowUnlimitedCodeSize=true, allowUnlimitedInitCodeSize=false', + // ) + // } + // }) +}) diff --git a/packages/tx/src/params.ts b/packages/tx/src/params.ts index 8b97c8f7036..530b8895f51 100644 --- a/packages/tx/src/params.ts +++ b/packages/tx/src/params.ts @@ -66,4 +66,13 @@ export const paramsTx: ParamsDict = { 7691: { maxBlobGasPerBlock: 1179648, // The max blob gas allowable per block }, + /** + * Meter Contract Code Size And Increase Limit + */ + 7907: { + // Maximum length of contract code + maxCodeSize: 262144, // 256 × 1024 = 262,144 bytes (0x40000) + // Maximum length of initialization code when creating a contract + maxInitCodeSize: 524288, // 512 × 1024 = 524,288 bytes (0x80000) + }, } From 9932bfd5979f8efddb5a2bb5349c338f690f037a Mon Sep 17 00:00:00 2001 From: jgresham Date: Mon, 19 May 2025 11:57:31 -0700 Subject: [PATCH 2/7] 3860 active implies common.isActivatedEIP 7907 --- packages/evm/src/evm.ts | 14 ++++---------- packages/evm/src/interpreter.ts | 2 +- packages/evm/src/opcodes/functions.ts | 4 ++-- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 2bd265b801e..a8851e41de5 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -500,7 +500,7 @@ export class EVM implements EVMInterface { // Reduce tx value from sender await this._reduceSenderBalance(account, message) - if (this.common.isActivatedEIP(3860) || this.common.isActivatedEIP(7907)) { + if (this.common.isActivatedEIP(3860)) { if ( message.data.length > Number(this.common.param('maxInitCodeSize')) && !this.allowUnlimitedInitCodeSize @@ -707,15 +707,9 @@ export class EVM implements EVMInterface { if (this.common.gteHardfork(Hardfork.Homestead)) { if (!allowedCodeSize) { if (this.DEBUG) { - if (this.common.isActivatedEIP(7907)) { - debug( - `Code size exceeds maximum code size ${this.common.param('maxCodeSize')}KB (>= Osaka)`, - ) - } else { - debug( - `Code size exceeds maximum code size ${this.common.param('maxCodeSize')}KB (>= SpuriousDragon)`, - ) - } + debug( + `Code size exceeds maximum code size ${this.common.param('maxCodeSize')}KB (>= ${this.common.isActivatedEIP(7907) ? 'Osaka' : 'SpuriousDragon'})`, + ) } result = { ...result, ...CodesizeExceedsMaximumError(message.gasLimit) } } else { diff --git a/packages/evm/src/interpreter.ts b/packages/evm/src/interpreter.ts index a364549afc9..a56ad9d09d8 100644 --- a/packages/evm/src/interpreter.ts +++ b/packages/evm/src/interpreter.ts @@ -1123,7 +1123,7 @@ export class Interpreter { this._env.contract.nonce += BIGINT_1 await this.journal.putAccount(this._env.address, this._env.contract) - if (this.common.isActivatedEIP(3860) || this.common.isActivatedEIP(7907)) { + if (this.common.isActivatedEIP(3860)) { if ( codeToRun.length > Number(this.common.param('maxInitCodeSize')) && this._evm.allowUnlimitedInitCodeSize === false diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index e618b5a2c20..e686fbd2faa 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -1340,7 +1340,7 @@ export const handlers: Map = new Map([ const [value, offset, length] = runState.stack.popN(3) if ( - (common.isActivatedEIP(3860) || common.isActivatedEIP(7907)) && + common.isActivatedEIP(3860) && length > Number(common.param('maxInitCodeSize')) && !runState.interpreter._evm.allowUnlimitedInitCodeSize ) { @@ -1376,7 +1376,7 @@ export const handlers: Map = new Map([ const [value, offset, length, salt] = runState.stack.popN(4) if ( - (common.isActivatedEIP(3860) || common.isActivatedEIP(7907)) && + common.isActivatedEIP(3860) && length > Number(common.param('maxInitCodeSize')) && !runState.interpreter._evm.allowUnlimitedInitCodeSize ) { From c5bb893584b72ab5296e202b6af5bb049db9bdcc Mon Sep 17 00:00:00 2001 From: jgresham Date: Mon, 19 May 2025 16:48:10 -0700 Subject: [PATCH 3/7] add excess code size gas for cold access --- packages/evm/src/opcodes/EIP2929.ts | 17 ++++++++++++++++- packages/evm/src/opcodes/util.ts | 7 +++++++ packages/evm/src/params.ts | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/evm/src/opcodes/EIP2929.ts b/packages/evm/src/opcodes/EIP2929.ts index 75b8caaa981..fffef826daa 100644 --- a/packages/evm/src/opcodes/EIP2929.ts +++ b/packages/evm/src/opcodes/EIP2929.ts @@ -2,6 +2,7 @@ import { BIGINT_0 } from '@ethereumjs/util' import type { Common } from '@ethereumjs/common' import type { RunState } from '../interpreter.ts' +import { ceil32 } from './util.ts' /** * Adds address to accessedAddresses set if not already included. @@ -30,7 +31,21 @@ export function accessAddressEIP2929( // selfdestruct beneficiary address reads are charged an *additional* cold access // if verkle not activated if (chargeGas && !(common.isActivatedEIP(6800) || common.isActivatedEIP(7864))) { - return common.param('coldaccountaccessGas') + // EIP 7907: + // get the excess contract code size gas and add it to the cold account access gas + // initCodeWordGas (2) * every byte over the excessCodeSizeThreshold + let coldAccountAccessGas = common.param('coldaccountaccessGas') + if (common.isActivatedEIP(7907)) { + const excessContractSize = BigInt( + Math.max( + 0, + runState.env.contract.codeSize - Number(common.param('excessCodeSizeThreshold')), + ), + ) + const excessContractSizeGas = ceil32(excessContractSize) * common.param('initCodeWordGas') + coldAccountAccessGas += excessContractSizeGas + } + return coldAccountAccessGas } else if (chargeGas && (common.isActivatedEIP(6800) || common.isActivatedEIP(7864))) { // If Verkle is active, then the warmstoragereadGas should still be charged // This is because otherwise opcodes will have cost 0 (this is thus the base fee) diff --git a/packages/evm/src/opcodes/util.ts b/packages/evm/src/opcodes/util.ts index 5b4af7775dd..3eae377d8f9 100644 --- a/packages/evm/src/opcodes/util.ts +++ b/packages/evm/src/opcodes/util.ts @@ -3,6 +3,7 @@ import { BIGINT_0, BIGINT_1, BIGINT_2, + BIGINT_31, BIGINT_32, BIGINT_64, BIGINT_160, @@ -47,6 +48,12 @@ export function abs(a: bigint) { return a * BIGINT_NEG1 } +export function ceil32(n: bigint) { + // Division with BigInt in JavaScript always performs + // integer division (i.e., it truncates toward zero) + return (n + BIGINT_31) / BIGINT_32 +} + const N = BigInt(115792089237316195423570985008687907853269984665640564039457584007913129639936) export function exponentiation(bas: bigint, exp: bigint) { let t = BIGINT_1 diff --git a/packages/evm/src/params.ts b/packages/evm/src/params.ts index 6175b8a0bcc..788defa7e4f 100644 --- a/packages/evm/src/params.ts +++ b/packages/evm/src/params.ts @@ -413,6 +413,8 @@ export const paramsEVM: ParamsDict = { * Meter Contract Code Size And Increase Limit */ 7907: { + // The threshold for the excess contract code size (prev. maxCodeSize EIP-607) + excessCodeSizeThreshold: 24576, // 24 x 1024 = 24576 bytes (0x6000) // Maximum length of contract code maxCodeSize: 262144, // 256 × 1024 = 262,144 bytes (0x40000) // Maximum length of initialization code when creating a contract From d2011c6420bad6b406a76f34dde3baaec77505d2 Mon Sep 17 00:00:00 2001 From: jgresham Date: Fri, 23 May 2025 11:43:39 -0700 Subject: [PATCH 4/7] eip7907: add warm code address set to journal and opcodes/eip7907 --- packages/evm/src/journal.ts | 22 +++++++++++ packages/evm/src/opcodes/EIP2929.ts | 17 +------- packages/evm/src/opcodes/EIP7907.ts | 46 ++++++++++++++++++++++ packages/evm/src/opcodes/gas.ts | 61 +++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 16 deletions(-) create mode 100644 packages/evm/src/opcodes/EIP7907.ts diff --git a/packages/evm/src/journal.ts b/packages/evm/src/journal.ts index 1b7153845e3..7c1ae720ced 100644 --- a/packages/evm/src/journal.ts +++ b/packages/evm/src/journal.ts @@ -45,6 +45,8 @@ export class Journal { private journalHeight: JournalHeight + private warmedCodeAddresses!: Set + public accessList?: Map> public preimages?: Map @@ -184,6 +186,7 @@ export class Journal { this.alwaysWarmJournal = new Map() this.touched = new Set() this.journalDiff = [[0, [new Set(), new Map(), new Set()]]] + this.warmedCodeAddresses = new Set() } /** @@ -242,6 +245,16 @@ export class Journal { return warm } + /** + * Returns true if the address's code is warm in the current context + * @param address - The address (as a Uint8Array) to check + */ + isWarmedCodeAddress(address: Uint8Array): boolean { + const addressHex = bytesToUnprefixedHex(address) + const warm = this.warmedCodeAddresses.has(addressHex) + return warm + } + /** * Add a warm address in the current context * @param addressArr - The address (as a Uint8Array) to check @@ -260,6 +273,15 @@ export class Journal { } } + /** + * Add an address to the set of warmed code addresses in the current context + * @param addressArr - The address (as a Uint8Array) to check + */ + addWarmedCodeAddress(addressArr: Uint8Array): void { + const address = bytesToUnprefixedHex(addressArr) + this.warmedCodeAddresses.add(address) + } + /** * Returns true if the slot of the address is warm * @param address - The address (as a Uint8Array) to check diff --git a/packages/evm/src/opcodes/EIP2929.ts b/packages/evm/src/opcodes/EIP2929.ts index fffef826daa..75b8caaa981 100644 --- a/packages/evm/src/opcodes/EIP2929.ts +++ b/packages/evm/src/opcodes/EIP2929.ts @@ -2,7 +2,6 @@ import { BIGINT_0 } from '@ethereumjs/util' import type { Common } from '@ethereumjs/common' import type { RunState } from '../interpreter.ts' -import { ceil32 } from './util.ts' /** * Adds address to accessedAddresses set if not already included. @@ -31,21 +30,7 @@ export function accessAddressEIP2929( // selfdestruct beneficiary address reads are charged an *additional* cold access // if verkle not activated if (chargeGas && !(common.isActivatedEIP(6800) || common.isActivatedEIP(7864))) { - // EIP 7907: - // get the excess contract code size gas and add it to the cold account access gas - // initCodeWordGas (2) * every byte over the excessCodeSizeThreshold - let coldAccountAccessGas = common.param('coldaccountaccessGas') - if (common.isActivatedEIP(7907)) { - const excessContractSize = BigInt( - Math.max( - 0, - runState.env.contract.codeSize - Number(common.param('excessCodeSizeThreshold')), - ), - ) - const excessContractSizeGas = ceil32(excessContractSize) * common.param('initCodeWordGas') - coldAccountAccessGas += excessContractSizeGas - } - return coldAccountAccessGas + return common.param('coldaccountaccessGas') } else if (chargeGas && (common.isActivatedEIP(6800) || common.isActivatedEIP(7864))) { // If Verkle is active, then the warmstoragereadGas should still be charged // This is because otherwise opcodes will have cost 0 (this is thus the base fee) diff --git a/packages/evm/src/opcodes/EIP7907.ts b/packages/evm/src/opcodes/EIP7907.ts new file mode 100644 index 00000000000..3a8e050b0ac --- /dev/null +++ b/packages/evm/src/opcodes/EIP7907.ts @@ -0,0 +1,46 @@ +import { BIGINT_0 } from '@ethereumjs/util' + +import type { Common } from '@ethereumjs/common' +import type { RunState } from '../interpreter.ts' +import { ceil32 } from './util.ts' + +/** + * Adds address to accessAddressCode set if not already included. + * Adjusts cost incurred for executing opcode based on whether address code + * is warm/cold and whether the address is a large contract. (EIP 7907) + * @param {RunState} runState + * @param {Address} address + * @param {Common} common + * @param {Boolean} chargeGas (default: true) + * @param {Boolean} isSelfdestruct (default: false) + */ +export function accessAddressCodeEIP7907( + runState: RunState, + address: Uint8Array, + common: Common, + chargeGas = true, +): bigint { + if (!common.isActivatedEIP(7907)) return BIGINT_0 + + // Cold + if (!runState.interpreter.journal.isWarmedCodeAddress(address)) { + runState.interpreter.journal.addWarmedCodeAddress(address) + + // CREATE, CREATE2 opcodes have the address code warmed for free. + if (chargeGas) { + // EIP 7907: + // get the large contract cost gas + // initCodeWordGas (2) * every byte (rounded up to the next 32 bytes) over the excessCodeSizeThreshold + const excessContractSize = BigInt( + Math.max( + 0, + runState.env.contract.codeSize - Number(common.param('excessCodeSizeThreshold')), + ), + ) + const largeContractCost = ceil32(excessContractSize) * common.param('initCodeWordGas') + return largeContractCost + } + } + // No Warm case: there is no additional large contract cost for warm code addresses + return BIGINT_0 +} diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index 7d3660b0374..5f6c8c27c5b 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -18,6 +18,7 @@ import { DELEGATION_7702_FLAG } from '../types.ts' import { updateSstoreGasEIP1283 } from './EIP1283.ts' import { updateSstoreGasEIP2200 } from './EIP2200.ts' import { accessAddressEIP2929, accessStorageEIP2929 } from './EIP2929.ts' +import { accessAddressCodeEIP7907 } from './EIP7907.ts' import { createAddressFromStackBigInt, divCeil, @@ -214,6 +215,11 @@ export const dynamicGasHandlers: Map Date: Fri, 23 May 2025 12:15:30 -0700 Subject: [PATCH 5/7] fix 7907: warm newly created contract from factory with test --- packages/evm/src/evm.ts | 5 + .../evm/test/eips/eip-7907-initcode.spec.ts | 114 +++++++++++------- 2 files changed, 77 insertions(+), 42 deletions(-) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index a8851e41de5..4048bf4dd9f 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -981,6 +981,11 @@ export class EVM implements EVMInterface { this.journal.addWarmedAddress((await this._generateAddress(message)).bytes) } + if (!message.to && this.common.isActivatedEIP(7907)) { + message.code = message.data + this.journal.addWarmedCodeAddress((await this._generateAddress(message)).bytes) + } + await this.journal.checkpoint() if (this.common.isActivatedEIP(1153)) this.transientStorage.checkpoint() if (this.DEBUG) { diff --git a/packages/evm/test/eips/eip-7907-initcode.spec.ts b/packages/evm/test/eips/eip-7907-initcode.spec.ts index 6192920543f..4325c3edac6 100644 --- a/packages/evm/test/eips/eip-7907-initcode.spec.ts +++ b/packages/evm/test/eips/eip-7907-initcode.spec.ts @@ -2,6 +2,7 @@ import { Common, Hardfork, Mainnet } from '@ethereumjs/common' import { Address, concatBytes, + createAddressFromString, // createAddressFromString, // equalsBytes, hexToBytes, @@ -83,49 +84,78 @@ describe('EIP 7907 initcode size tests', () => { ) }) - // it('ensure EIP-7907 gas is applied on CREATE calls', async () => { - // // Transaction/Contract data taken from https://github.com/ethereum/tests/pull/990 - // const commonWith7907 = new Common({ - // chain: Mainnet, - // hardfork: Hardfork.Osaka, - // eips: [7907], - // }) - // const commonWithout7907 = new Common({ - // chain: Mainnet, - // hardfork: Hardfork.Osaka, - // eips: [], - // }) - // const caller = createAddressFromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') - // const evm = await createEVM({ - // common: commonWith7907, - // }) - // const evmWithout7907 = await createEVM({ - // common: commonWithout7907, - // }) - // const contractFactory = createAddressFromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') - // const contractAccount = await evm.stateManager.getAccount(contractFactory) - // await evm.stateManager.putAccount(contractFactory, contractAccount!) - // await evmWithout7907.stateManager.putAccount(contractFactory, contractAccount!) - // const factoryCode = hexToBytes( - // '0x7f600a80600080396000f3000000000000000000000000000000000000000000006000526000355a8160006000f05a8203600a55806000556001600155505050', - // ) + it('ensure EIP-7907 code warm is applied on CREATE calls to factory and new contract', async () => { + // Transaction/Contract data taken from https://github.com/ethereum/tests/pull/990 + const commonWith7907 = new Common({ + chain: Mainnet, + hardfork: Hardfork.Osaka, + eips: [7907], + }) + const commonWithout7907 = new Common({ + chain: Mainnet, + hardfork: Hardfork.Prague, + eips: [], + }) + const caller = createAddressFromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') + const evm = await createEVM({ + common: commonWith7907, + }) + const evmWithout7907 = await createEVM({ + common: commonWithout7907, + }) + const contractFactory = createAddressFromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') + const contractAccount = await evm.stateManager.getAccount(contractFactory) + await evm.stateManager.putAccount(contractFactory, contractAccount!) + await evmWithout7907.stateManager.putAccount(contractFactory, contractAccount!) + const factoryCode = hexToBytes( + '0x7f600a80600080396000f3000000000000000000000000000000000000000000006000526000355a8160006000f05a8203600a55806000556001600155505050', + ) - // await evm.stateManager.putCode(contractFactory, factoryCode) - // await evmWithout7907.stateManager.putCode(contractFactory, factoryCode) - // const data = hexToBytes('0x000000000000000000000000000000000000000000000000000000000000c000') - // const runCallArgs = { - // from: caller, - // to: contractFactory, - // data, - // gasLimit: BigInt(0xfffffffff), - // } - // const res = await evm.runCall(runCallArgs) - // const res2 = await evmWithout7907.runCall(runCallArgs) - // assert.isTrue( - // res.execResult.executionGasUsed > res2.execResult.executionGasUsed, - // 'execution gas used is higher with EIP 7907 active', - // ) - // }) + await evm.stateManager.putCode(contractFactory, factoryCode) + await evmWithout7907.stateManager.putCode(contractFactory, factoryCode) + const data = hexToBytes('0x000000000000000000000000000000000000000000000000000000000000c000') + const runCallArgs = { + from: caller, + to: contractFactory, + data, + gasLimit: BigInt(0xfffffffff), + } + const res = await evm.runCall(runCallArgs) + const resWithout7907 = await evmWithout7907.runCall(runCallArgs) + + // Check that the created contract address is warmed. + // This logic isn't changed in 7907, but is in place to confirm that + // the code warm is applied inline with the address access warm on CREATE calls. + assert.isTrue( + res.execResult.runState?.interpreter.journal.isWarmedAddress(contractFactory.bytes), + 'contract factory address is warmed', + ) + res.execResult.createdAddresses?.forEach((address) => { + assert.isTrue( + res.execResult.runState?.interpreter.journal.isWarmedAddress(hexToBytes(address)), + `created contract address is warmed: ${address}`, + ) + }) + + // Both the contract factory and the created contract codes are warmed. + assert.isTrue( + res.execResult.runState?.interpreter.journal.isWarmedCodeAddress(contractFactory.bytes), + 'contract factory code is warmed', + ) + res.execResult.createdAddresses?.forEach((address) => { + assert.isTrue( + res.execResult.runState?.interpreter.journal.isWarmedCodeAddress(hexToBytes(address)), + `created contract code is warmed: ${address}`, + ) + }) + + assert.isFalse( + resWithout7907.execResult.runState?.interpreter.journal.isWarmedCodeAddress( + contractFactory.bytes, + ), + 'contract factory code is not warmed with EIP 7907 inactive', + ) + }) // it('ensure EIP-7907 gas is applied on CREATE2 calls', async () => { // // Transaction/Contract data taken from https://github.com/ethereum/tests/pull/990 From a8940abd41566e891e441f35d7a4fe674fb9386d Mon Sep 17 00:00:00 2001 From: jgresham Date: Fri, 23 May 2025 14:40:38 -0700 Subject: [PATCH 6/7] revert warm newly created contract from CREATE. new tests at maxInitCodeSize --- packages/evm/src/evm.ts | 5 - .../evm/test/eips/eip-7907-initcode.spec.ts | 289 ++++++++++++------ 2 files changed, 199 insertions(+), 95 deletions(-) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 4048bf4dd9f..a8851e41de5 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -981,11 +981,6 @@ export class EVM implements EVMInterface { this.journal.addWarmedAddress((await this._generateAddress(message)).bytes) } - if (!message.to && this.common.isActivatedEIP(7907)) { - message.code = message.data - this.journal.addWarmedCodeAddress((await this._generateAddress(message)).bytes) - } - await this.journal.checkpoint() if (this.common.isActivatedEIP(1153)) this.transientStorage.checkpoint() if (this.DEBUG) { diff --git a/packages/evm/test/eips/eip-7907-initcode.spec.ts b/packages/evm/test/eips/eip-7907-initcode.spec.ts index 4325c3edac6..08c0fc09de6 100644 --- a/packages/evm/test/eips/eip-7907-initcode.spec.ts +++ b/packages/evm/test/eips/eip-7907-initcode.spec.ts @@ -3,6 +3,7 @@ import { Address, concatBytes, createAddressFromString, + equalsBytes, // createAddressFromString, // equalsBytes, hexToBytes, @@ -16,7 +17,7 @@ const pkey = hexToBytes(`0x${'20'.repeat(32)}`) const sender = new Address(privateToAddress(pkey)) describe('EIP 7907 initcode size tests', () => { - it('code 512KB exceeds max initcode size', async () => { + it('create contract code size 512KB exactly max initcode size does not fail from exceeding max initcode size', async () => { const common = new Common({ chain: Mainnet, hardfork: Hardfork.Osaka, @@ -33,17 +34,38 @@ describe('EIP 7907 initcode size tests', () => { const runCallArgs = { sender, // call address gasLimit: BigInt(0xffffffffff), // ensure we pass a lot of gas, so we do not run out of gas - // Simple test, PUSH PUSH 0 RETURN - // It tries to deploy a contract too large, where the code is all zeros - // (since memory which is not allocated/resized to yet is always defaulted to 0) - data: concatBytes( - hexToBytes( - '0x7F6000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060005260206000F3', - ), - buffer, - ), + data: buffer, + to: undefined, // create contract } const result = await evm.runCall(runCallArgs) + // fails for being an invalid contract, but should pass the initcode size limit check + assert.isTrue( + (result.execResult.exceptionError?.error as string) !== 'initcode exceeds max initcode size', + 'create contract with data size 512KB (exactly max initcode size) does not fail from exceeding max initcode size', + ) + }) + + it('create contract code size 512KB (exactly max initcode size) +1 byte fails from size limit', async () => { + const common = new Common({ + chain: Mainnet, + hardfork: Hardfork.Osaka, + eips: [7907], + }) + const evm = await createEVM({ + common, + }) + + // 524288 + 1 ("512KB+1B") length, 8 bit array, filled + const buffer = new Uint8Array(524289).fill(0x60) + + // setup the call arguments + const runCallArgs = { + sender, // call address + gasLimit: BigInt(0xffffffffff), // ensure we pass a lot of gas, so we do not run out of gas + data: concatBytes(buffer), + } + const result = await evm.runCall(runCallArgs) + assert.isTrue( (result.execResult.exceptionError?.error as string) === 'initcode exceeds max initcode size', 'initcode exceeds max size', @@ -123,7 +145,7 @@ describe('EIP 7907 initcode size tests', () => { const res = await evm.runCall(runCallArgs) const resWithout7907 = await evmWithout7907.runCall(runCallArgs) - // Check that the created contract address is warmed. + // Check that the factory and created contracts addresses are warmed. // This logic isn't changed in 7907, but is in place to confirm that // the code warm is applied inline with the address access warm on CREATE calls. assert.isTrue( @@ -137,15 +159,15 @@ describe('EIP 7907 initcode size tests', () => { ) }) - // Both the contract factory and the created contract codes are warmed. + // Just the contract factory code is warmed. The created contract code is not warmed. assert.isTrue( res.execResult.runState?.interpreter.journal.isWarmedCodeAddress(contractFactory.bytes), 'contract factory code is warmed', ) res.execResult.createdAddresses?.forEach((address) => { - assert.isTrue( + assert.isFalse( res.execResult.runState?.interpreter.journal.isWarmedCodeAddress(hexToBytes(address)), - `created contract code is warmed: ${address}`, + `created contract code is not warmed: ${address}`, ) }) @@ -233,80 +255,167 @@ describe('EIP 7907 initcode size tests', () => { ) }) - // it('CREATE with MAX_INITCODE_SIZE+1, allowUnlimitedContractSize active', async () => { - // const commonWith7907 = new Common({ - // chain: Mainnet, - // hardfork: Hardfork.Osaka, - // eips: [7907], - // }) - // const caller = createAddressFromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') - // for (const code of ['F0', 'F5']) { - // const evm = await createEVM({ - // common: commonWith7907, - - // allowUnlimitedInitCodeSize: true, - // }) - // const evmDisabled = await createEVM({ - // common: commonWith7907, - // allowUnlimitedInitCodeSize: false, - // }) - // const contractFactory = createAddressFromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') - // const contractAccount = await evm.stateManager.getAccount(contractFactory) - // await evm.stateManager.putAccount(contractFactory, contractAccount!) - // await evmDisabled.stateManager.putAccount(contractFactory, contractAccount!) - // // This factory code: - // // -> reads 32 bytes from the calldata (X) - // // Attempts to create a contract of X size - // // (the initcode of this contract is just zeros, so STOP opcode - // // It stores the topmost stack item of this CREATE(2) at slot 0 - // // This is either the contract address if it was successful, or 0 in case of error - // const factoryCode = hexToBytes(`0x600060003560006000${code}600055`) - - // await evm.stateManager.putCode(contractFactory, factoryCode) - // await evmDisabled.stateManager.putCode(contractFactory, factoryCode) - - // const runCallArgs = { - // from: caller, - // to: contractFactory, - // gasLimit: BigInt(0xfffffffff), - // data: hexToBytes(`0x${'00'.repeat(30)}C001`), - // } - - // const res = await evm.runCall(runCallArgs) - // await evmDisabled.runCall(runCallArgs) - - // const key0 = hexToBytes(`0x${'00'.repeat(32)}`) - // const storageActive = await evm.stateManager.getStorage(contractFactory, key0) - // const storageInactive = await evmDisabled.stateManager.getStorage(contractFactory, key0) - - // assert.isTrue( - // !equalsBytes(storageActive, new Uint8Array()), - // 'created contract with MAX_INITCODE_SIZE + 1 length, allowUnlimitedInitCodeSize=true', - // ) - // assert.isTrue( - // equalsBytes(storageInactive, new Uint8Array()), - // 'did not create contract with MAX_INITCODE_SIZE + 1 length, allowUnlimitedInitCodeSize=false', - // ) - - // // gas check - - // const runCallArgs2 = { - // from: caller, - // to: contractFactory, - // gasLimit: BigInt(0xfffffffff), - // data: hexToBytes(`0x${'00'.repeat(30)}C000`), - // } - - // // Test: - // // On the `allowUnlimitedInitCodeSize = true`, create contract with MAX_INITCODE_SIZE + 1 - // // On `allowUnlimitedInitCodeSize = false`, create contract with MAX_INITCODE_SIZE - // // Verify that the gas cost on the prior one is higher than the first one - // const res2 = await evmDisabled.runCall(runCallArgs2) - - // assert.isTrue( - // res.execResult.executionGasUsed > res2.execResult.executionGasUsed, - // 'charged initcode analysis gas cost on both allowUnlimitedCodeSize=true, allowUnlimitedInitCodeSize=false', - // ) - // } - // }) + it('CREATE with MAX_INITCODE_SIZE+ approx 200KB, allowUnlimitedContractSize active', async () => { + const commonWith7907 = new Common({ + chain: Mainnet, + hardfork: Hardfork.Osaka, + eips: [7907], + }) + const caller = createAddressFromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') + for (const code of ['F0', 'F5']) { + const evm = await createEVM({ + common: commonWith7907, + + allowUnlimitedInitCodeSize: true, + }) + const evmDisabled = await createEVM({ + common: commonWith7907, + allowUnlimitedInitCodeSize: false, + }) + const contractFactory = createAddressFromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') + const contractAccount = await evm.stateManager.getAccount(contractFactory) + await evm.stateManager.putAccount(contractFactory, contractAccount!) + await evmDisabled.stateManager.putAccount(contractFactory, contractAccount!) + // This factory code: + // -> reads 32 bytes from the calldata (X) + // Attempts to create a contract of X size + // (the initcode of this contract is just zeros, so STOP opcode + // It stores the topmost stack item of this CREATE(2) at slot 0 + // This is either the contract address if it was successful, or 0 in case of error + const factoryCode = hexToBytes(`0x600060003560006000${code}600055`) + + await evm.stateManager.putCode(contractFactory, factoryCode) + await evmDisabled.stateManager.putCode(contractFactory, factoryCode) + + const runCallArgs = { + from: caller, + to: contractFactory, + gasLimit: BigInt(0xfffffffff), + data: hexToBytes(`0x${'00'.repeat(29)}C0001`), // C0001 = 786,433 + } + + const res = await evm.runCall(runCallArgs) + await evmDisabled.runCall(runCallArgs) + + const key0 = hexToBytes(`0x${'00'.repeat(32)}`) + const storageActive = await evm.stateManager.getStorage(contractFactory, key0) + const storageInactive = await evmDisabled.stateManager.getStorage(contractFactory, key0) + + assert.isTrue( + !equalsBytes(storageActive, new Uint8Array()), + 'created contract with MAX_INITCODE_SIZE + 1 length, allowUnlimitedInitCodeSize=true', + ) + assert.isTrue( + equalsBytes(storageInactive, new Uint8Array()), + 'did not create contract with MAX_INITCODE_SIZE + 1 length, allowUnlimitedInitCodeSize=false', + ) + + // gas check + + const runCallArgs2 = { + from: caller, + to: contractFactory, + gasLimit: BigInt(0xfffffffff), + data: hexToBytes(`0x${'00'.repeat(30)}C000`), + } + + // Test: + // On the `allowUnlimitedInitCodeSize = true`, create contract with MAX_INITCODE_SIZE + 1 + // On `allowUnlimitedInitCodeSize = false`, create contract with MAX_INITCODE_SIZE + // Verify that the gas cost on the prior one is higher than the first one + const res2 = await evmDisabled.runCall(runCallArgs2) + + assert.isTrue( + res.execResult.executionGasUsed > res2.execResult.executionGasUsed, + 'charged initcode analysis gas cost on both allowUnlimitedCodeSize=true, allowUnlimitedInitCodeSize=false', + ) + } + }) + + it('CREATE with exactly MAX_INITCODE_SIZE passes', async () => { + const commonWith7907 = new Common({ + chain: Mainnet, + hardfork: Hardfork.Osaka, + eips: [7907], + }) + const caller = createAddressFromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') + for (const code of ['F0', 'F5']) { + const evm = await createEVM({ + common: commonWith7907, + + allowUnlimitedInitCodeSize: true, + }) + const evmDisabled = await createEVM({ + common: commonWith7907, + allowUnlimitedInitCodeSize: false, + }) + const contractFactory = createAddressFromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') + const contractAccount = await evm.stateManager.getAccount(contractFactory) + await evm.stateManager.putAccount(contractFactory, contractAccount!) + await evmDisabled.stateManager.putAccount(contractFactory, contractAccount!) + // This factory code: + // -> reads 32 bytes from the calldata (X) + // Attempts to create a contract of X size + // (the initcode of this contract is just zeros, so STOP opcode + // It stores the topmost stack item of this CREATE(2) at slot 0 + // This is either the contract address if it was successful, or 0 in case of error + const factoryCode = hexToBytes(`0x600060003560006000${code}600055`) + + await evm.stateManager.putCode(contractFactory, factoryCode) + await evmDisabled.stateManager.putCode(contractFactory, factoryCode) + + const runCallArgs = { + from: caller, + to: contractFactory, + gasLimit: BigInt(0xfffffffff), + data: hexToBytes(`0x${'00'.repeat(29)}080000`), // 512 × 1024 = 524,288 bytes (0x80000) + } + + const res = await evm.runCall(runCallArgs) + const resEnabledLimit = await evmDisabled.runCall(runCallArgs) + + const key0 = hexToBytes(`0x${'00'.repeat(32)}`) + const storageActive = await evm.stateManager.getStorage(contractFactory, key0) + const storageInactive = await evmDisabled.stateManager.getStorage(contractFactory, key0) + + assert.isTrue( + !equalsBytes(storageActive, new Uint8Array()), + 'created contract with MAX_INITCODE_SIZE length, allowUnlimitedInitCodeSize=true', + ) + assert.isTrue( + !equalsBytes(storageInactive, new Uint8Array()), + 'created contract with MAX_INITCODE_SIZE length, allowUnlimitedInitCodeSize=false', + ) + assert.isTrue( + res.execResult.exceptionError === undefined, + 'no error for successfully created a contract with data size 250KB (> 24KB and < 256KB)', + ) + assert.isTrue( + resEnabledLimit.execResult.exceptionError === undefined, + 'no error for successfully created a contract with data size 250KB (> 24KB and < 256KB)', + ) + + // over initcode size limit, 512KB + 1 byte + const runCallArgsOverLimit = { + from: caller, + to: contractFactory, + gasLimit: BigInt(0xfffffffff), + data: hexToBytes(`0x${'00'.repeat(29)}080001`), // 512 × 1024 = 524,288 bytes (0x80000) + 1 byte + } + + const res2 = await evm.runCall(runCallArgsOverLimit) + const resEnabledLimi2 = await evmDisabled.runCall(runCallArgsOverLimit) + assert.isTrue( + res2.execResult.exceptionError === undefined, + 'no error for successfully created a contract with data size 512KB + 1 byte when limit is disabled', + ) + assert.isTrue( + (resEnabledLimi2.execResult.exceptionError?.error as string) === + 'initcode exceeds max initcode size', + 'error for creating a contract with data size 512KB + 1 byte', + ) + + // gas check todo + } + }) }) From 6e73e7074c5eb1b28d837e932b10da03be5ef579 Mon Sep 17 00:00:00 2001 From: jgresham Date: Fri, 23 May 2025 15:31:57 -0700 Subject: [PATCH 7/7] minor cleanup EIP7907.ts and add param to tx/src/params --- packages/evm/src/opcodes/EIP7907.ts | 7 ++----- packages/tx/src/params.ts | 2 ++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/evm/src/opcodes/EIP7907.ts b/packages/evm/src/opcodes/EIP7907.ts index 3a8e050b0ac..ec3bb3a1a4d 100644 --- a/packages/evm/src/opcodes/EIP7907.ts +++ b/packages/evm/src/opcodes/EIP7907.ts @@ -5,14 +5,13 @@ import type { RunState } from '../interpreter.ts' import { ceil32 } from './util.ts' /** - * Adds address to accessAddressCode set if not already included. + * Adds address to warmedCodeAddress set if not already included. * Adjusts cost incurred for executing opcode based on whether address code * is warm/cold and whether the address is a large contract. (EIP 7907) * @param {RunState} runState * @param {Address} address * @param {Common} common * @param {Boolean} chargeGas (default: true) - * @param {Boolean} isSelfdestruct (default: false) */ export function accessAddressCodeEIP7907( runState: RunState, @@ -26,18 +25,16 @@ export function accessAddressCodeEIP7907( if (!runState.interpreter.journal.isWarmedCodeAddress(address)) { runState.interpreter.journal.addWarmedCodeAddress(address) - // CREATE, CREATE2 opcodes have the address code warmed for free. if (chargeGas) { // EIP 7907: // get the large contract cost gas - // initCodeWordGas (2) * every byte (rounded up to the next 32 bytes) over the excessCodeSizeThreshold const excessContractSize = BigInt( Math.max( 0, runState.env.contract.codeSize - Number(common.param('excessCodeSizeThreshold')), ), ) - const largeContractCost = ceil32(excessContractSize) * common.param('initCodeWordGas') + const largeContractCost = ceil32(excessContractSize) * common.param('initCodeWordGas') // 2 return largeContractCost } } diff --git a/packages/tx/src/params.ts b/packages/tx/src/params.ts index 530b8895f51..343e12e9f4d 100644 --- a/packages/tx/src/params.ts +++ b/packages/tx/src/params.ts @@ -70,6 +70,8 @@ export const paramsTx: ParamsDict = { * Meter Contract Code Size And Increase Limit */ 7907: { + // The threshold for the excess contract code size (prev. maxCodeSize EIP-607) + excessCodeSizeThreshold: 24576, // 24 x 1024 = 24576 bytes (0x6000) // Maximum length of contract code maxCodeSize: 262144, // 256 × 1024 = 262,144 bytes (0x40000) // Maximum length of initialization code when creating a contract