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..a8851e41de5 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()) { @@ -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,9 @@ export class EVM implements EVMInterface { if (this.common.gteHardfork(Hardfork.Homestead)) { if (!allowedCodeSize) { if (this.DEBUG) { - debug(`Code size exceeds maximum code size (>= 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/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/EIP7907.ts b/packages/evm/src/opcodes/EIP7907.ts new file mode 100644 index 00000000000..ec3bb3a1a4d --- /dev/null +++ b/packages/evm/src/opcodes/EIP7907.ts @@ -0,0 +1,43 @@ +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 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) + */ +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) + + if (chargeGas) { + // EIP 7907: + // get the large contract cost gas + const excessContractSize = BigInt( + Math.max( + 0, + runState.env.contract.codeSize - Number(common.param('excessCodeSizeThreshold')), + ), + ) + const largeContractCost = ceil32(excessContractSize) * common.param('initCodeWordGas') // 2 + 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 { + 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, + 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 + 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', + ) + }) + + 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 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 resWithout7907 = await evmWithout7907.runCall(runCallArgs) + + // 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( + 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}`, + ) + }) + + // 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.isFalse( + res.execResult.runState?.interpreter.journal.isWarmedCodeAddress(hexToBytes(address)), + `created contract code is not 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 + // 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+ 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 + } + }) +}) diff --git a/packages/tx/src/params.ts b/packages/tx/src/params.ts index 8b97c8f7036..343e12e9f4d 100644 --- a/packages/tx/src/params.ts +++ b/packages/tx/src/params.ts @@ -66,4 +66,15 @@ export const paramsTx: ParamsDict = { 7691: { maxBlobGasPerBlock: 1179648, // The max blob gas allowable per block }, + /** + * 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 + maxInitCodeSize: 524288, // 512 × 1024 = 524,288 bytes (0x80000) + }, }