From 674a8a35ec51d1e6ce1755ff850e0c567ba6c5d9 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 11 Sep 2024 21:14:45 +0200 Subject: [PATCH] EIP-7702 devnet-3 readiness (#3581) * tx: implement strict 7702 validation * vm: update 7702 tx validation * evm: update 7702 [no ci] * tx: add / fix 7702 tests * vm: fix test encoding of authorization lists [no ci] * vm: correctly put authority nonce * vm: add test 7702 extcodehash/extcodesize evm: fix extcodehash/extcodesize for delegated accounts * vm: expand extcode* tests 7702 [no ci] * tx/vm: update tests [no ci] * evm/vm: update opcodes and fix tests 7702 * fix cspell [no ci] * vm: get params from tx for 7702 [no ci] * vm: 7702 correctly apply the refund [no ci] * vm: 7702: correctly handle self-sponsored txs [no ci] * tx: throw if authorization list is empty * vm: requests do not throw if code is non-existant * evm: ensure correct extcodehash reporting if account is delegated to a non-existing account * vm: 7702 ensure delegated accounts are not deleted [no ci] * evm: 7702 correctly check for gas on delegated code * evm: add verkle gas logic for 7702 * vm/tx: fix 7702 tests * tx: throw if 7702-tx has no `to` field * vm/tx: fix 7702 tests * VM: exit early on non-existing system contracts * 7702: add delegated account to warm address * vm: requests do restore system account * 7702: continue processing once auth ecrecover is invalid * evm/vm: add 7702 delegation constant * vm: fix requests * vm: unduplify 3607 error msg * fix example --------- Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com> --- packages/evm/src/evm.ts | 42 ++++--- packages/evm/src/opcodes/functions.ts | 51 ++++++-- packages/evm/src/opcodes/gas.ts | 49 ++++++++ packages/evm/src/params.ts | 8 -- packages/evm/src/types.ts | 3 + packages/tx/examples/EOACodeTx.ts | 6 +- packages/tx/src/7702/tx.ts | 7 ++ packages/tx/src/capabilities/eip7702.ts | 2 +- packages/tx/src/params.ts | 9 ++ packages/tx/src/types.ts | 4 +- packages/tx/src/util.ts | 48 +++++--- packages/tx/test/eip7702.spec.ts | 35 ++++-- packages/vm/src/runTx.ts | 91 +++++++++----- packages/vm/test/api/EIPs/eip-7702.spec.ts | 132 ++++++++++++++------- packages/vm/test/api/runBlock.spec.ts | 6 +- 15 files changed, 356 insertions(+), 137 deletions(-) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 067c01ff36..16ead4bc27 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -30,6 +30,21 @@ import { getOpcodesForHF } from './opcodes/index.js' import { paramsEVM } from './params.js' import { NobleBLS, getActivePrecompiles, getPrecompileName } from './precompiles/index.js' import { TransientStorage } from './transientStorage.js' +import { + type Block, + type CustomOpcode, + DELEGATION_7702_FLAG, + type EVMBLSInterface, + type EVMBN254Interface, + type EVMEvents, + type EVMInterface, + type EVMMockBlockchainInterface, + type EVMOpts, + type EVMResult, + type EVMRunCallOpts, + type EVMRunCodeOpts, + type ExecResult, +} from './types.js' import type { InterpreterOpts } from './interpreter.js' import type { Timer } from './logger.js' @@ -37,20 +52,6 @@ import type { MessageWithTo } from './message.js' import type { AsyncDynamicGasHandler, SyncDynamicGasHandler } from './opcodes/gas.js' import type { OpHandler, OpcodeList, OpcodeMap } from './opcodes/index.js' import type { CustomPrecompile, PrecompileFunc } from './precompiles/index.js' -import type { - Block, - CustomOpcode, - EVMBLSInterface, - EVMBN254Interface, - EVMEvents, - EVMInterface, - EVMMockBlockchainInterface, - EVMOpts, - EVMResult, - EVMRunCallOpts, - EVMRunCodeOpts, - ExecResult, -} from './types.js' import type { Common, StateManagerInterface } from '@ethereumjs/common' const debug = debugDefault('evm:evm') @@ -1016,6 +1017,19 @@ export class EVM implements EVMInterface { message.isCompiled = true } else { message.code = await this.stateManager.getCode(message.codeAddress) + + // EIP-7702 delegation check + if ( + this.common.isActivatedEIP(7702) && + equalsBytes(message.code.slice(0, 3), DELEGATION_7702_FLAG) + ) { + const address = new Address(message.code.slice(3, 24)) + message.code = await this.stateManager.getCode(address) + if (message.depth === 0) { + this.journal.addAlwaysWarmAddress(address.toString()) + } + } + message.isCompiled = false message.chargeCodeAccesses = true } diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index 827e2a5d6a..e54982f1ad 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -26,12 +26,14 @@ import { getVerkleTreeIndicesForStorageSlot, setLengthLeft, } from '@ethereumjs/util' +import { equalBytes } from '@noble/curves/abstract/utils' import { keccak256 } from 'ethereum-cryptography/keccak.js' import { EOFContainer, EOFContainerMode } from '../eof/container.js' import { EOFError } from '../eof/errors.js' import { EOFBYTES, EOFHASH, isEOF } from '../eof/util.js' import { ERROR } from '../exceptions.js' +import { DELEGATION_7702_FLAG } from '../types.js' import { createAddressFromStackBigInt, @@ -59,6 +61,21 @@ export interface AsyncOpHandler { export type OpHandler = SyncOpHandler | AsyncOpHandler +function getEIP7702DelegatedAddress(code: Uint8Array) { + if (equalBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) { + return new Address(code.slice(3, 24)) + } +} + +async function eip7702CodeCheck(runState: RunState, code: Uint8Array) { + const address = getEIP7702DelegatedAddress(code) + if (address !== undefined) { + return runState.stateManager.getCode(address) + } + + return code +} + // the opcode functions export const handlers: Map = new Map([ // 0x00: STOP @@ -511,20 +528,20 @@ export const handlers: Map = new Map([ // 0x3b: EXTCODESIZE [ 0x3b, - async function (runState) { + async function (runState, common) { const addressBigInt = runState.stack.pop() const address = createAddressFromStackBigInt(addressBigInt) // EOF check - const code = await runState.stateManager.getCode(address) + let code = await runState.stateManager.getCode(address) if (isEOF(code)) { // In legacy code, the target code is treated as to be "EOFBYTES" code runState.stack.push(BigInt(EOFBYTES.length)) return + } else if (common.isActivatedEIP(7702)) { + code = await eip7702CodeCheck(runState, code) } - const size = BigInt( - await runState.stateManager.getCodeSize(createAddressFromStackBigInt(addressBigInt)), - ) + const size = BigInt(code.length) runState.stack.push(size) }, @@ -532,15 +549,18 @@ export const handlers: Map = new Map([ // 0x3c: EXTCODECOPY [ 0x3c, - async function (runState) { + async function (runState, common) { const [addressBigInt, memOffset, codeOffset, dataLength] = runState.stack.popN(4) if (dataLength !== BIGINT_0) { - let code = await runState.stateManager.getCode(createAddressFromStackBigInt(addressBigInt)) + const address = createAddressFromStackBigInt(addressBigInt) + let code = await runState.stateManager.getCode(address) if (isEOF(code)) { // In legacy code, the target code is treated as to be "EOFBYTES" code code = EOFBYTES + } else if (common.isActivatedEIP(7702)) { + code = await eip7702CodeCheck(runState, code) } const data = getDataSlice(code, codeOffset, dataLength) @@ -553,7 +573,7 @@ export const handlers: Map = new Map([ // 0x3f: EXTCODEHASH [ 0x3f, - async function (runState) { + async function (runState, common) { const addressBigInt = runState.stack.pop() const address = createAddressFromStackBigInt(addressBigInt) @@ -564,6 +584,21 @@ export const handlers: Map = new Map([ // Therefore, push the hash of EOFBYTES to the stack runState.stack.push(bytesToBigInt(EOFHASH)) return + } else if (common.isActivatedEIP(7702)) { + const possibleDelegatedAddress = getEIP7702DelegatedAddress(code) + if (possibleDelegatedAddress !== undefined) { + const account = await runState.stateManager.getAccount(possibleDelegatedAddress) + if (!account || account.isEmpty()) { + runState.stack.push(BIGINT_0) + return + } + + runState.stack.push(BigInt(bytesToHex(account.codeHash))) + return + } else { + runState.stack.push(bytesToBigInt(keccak256(code))) + return + } } const account = await runState.stateManager.getAccount(address) diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index 3b70ebb8bb..ad842a971b 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -9,12 +9,14 @@ import { VERKLE_BASIC_DATA_LEAF_KEY, VERKLE_CODE_HASH_LEAF_KEY, bigIntToBytes, + equalsBytes, getVerkleTreeIndicesForStorageSlot, setLengthLeft, } from '@ethereumjs/util' import { EOFError } from '../eof/errors.js' import { ERROR } from '../exceptions.js' +import { DELEGATION_7702_FLAG } from '../types.js' import { updateSstoreGasEIP1283 } from './EIP1283.js' import { updateSstoreGasEIP2200 } from './EIP2200.js' @@ -31,9 +33,23 @@ import { import type { RunState } from '../interpreter.js' import type { Common } from '@ethereumjs/common' +import type { Address } from '@ethereumjs/util' const EXTCALL_TARGET_MAX = BigInt(2) ** BigInt(8 * 20) - BigInt(1) +async function eip7702GasCost( + runState: RunState, + common: Common, + address: Address, + charge2929Gas: boolean, +) { + const code = await runState.stateManager.getCode(address) + if (equalsBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) { + return accessAddressEIP2929(runState, code.slice(3, 24), common, charge2929Gas) + } + return BIGINT_0 +} + /** * This file returns the dynamic parts of opcodes which have dynamic gas * These are not pure functions: some edit the size of the memory @@ -175,6 +191,10 @@ export const dynamicGasHandlers: Map 1) { - throw new Error('Invalid EIP-7702 transaction: nonce list should consist of at most 1 item') - } else if (nonceList.length === 1) { - validateNoLeadingZeroes({ nonce: nonceList[0] }) + if (bytesToBigInt(chainId) > MAX_INTEGER) { + throw new Error('Invalid EIP-7702 transaction: chainId exceeds 2^256 - 1') + } + if (bytesToBigInt(nonce) > MAX_UINT64) { + throw new Error('Invalid EIP-7702 transaction: nonce exceeds 2^64 - 1') + } + const yParityBigInt = bytesToBigInt(yParity) + if (yParityBigInt !== BIGINT_0 && yParityBigInt !== BIGINT_1) { + throw new Error('Invalid EIP-7702 transaction: yParity should be 0 or 1') + } + if (bytesToBigInt(r) > MAX_INTEGER) { + throw new Error('Invalid EIP-7702 transaction: r exceeds 2^256 - 1') + } + if (bytesToBigInt(s) > SECP256K1_ORDER_DIV_2) { + throw new Error('Invalid EIP-7702 transaction: s > secp256k1n/2') } } } diff --git a/packages/tx/test/eip7702.spec.ts b/packages/tx/test/eip7702.spec.ts index d2c3935388..48ee891292 100644 --- a/packages/tx/test/eip7702.spec.ts +++ b/packages/tx/test/eip7702.spec.ts @@ -1,5 +1,14 @@ import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { createAddressFromPrivateKey, createZeroAddress, hexToBytes } from '@ethereumjs/util' +import { + BIGINT_1, + MAX_INTEGER, + MAX_UINT64, + SECP256K1_ORDER_DIV_2, + bigIntToHex, + createAddressFromPrivateKey, + createZeroAddress, + hexToBytes, +} from '@ethereumjs/util' import { assert, describe, it } from 'vitest' import { createEOACode7702Tx } from '../src/index.js' @@ -19,7 +28,7 @@ function getTxData(override: Partial = {}): TxData { const validAuthorizationList: AuthorizationListItem = { chainId: '0x', address: `0x${'20'.repeat(20)}`, - nonce: ['0x1'], + nonce: '0x1', yParity: '0x1', r: ones32, s: ones32, @@ -32,6 +41,7 @@ function getTxData(override: Partial = {}): TxData { ...override, }, ], + to: createZeroAddress(), } } @@ -43,7 +53,7 @@ describe('[EOACode7702Transaction]', () => { maxFeePerGas: 1, maxPriorityFeePerGas: 1, accessList: [], - authorizationList: [], + ...getTxData(), chainId: 1, gasLimit: 100000, to: createZeroAddress(), @@ -65,18 +75,25 @@ describe('[EOACode7702Transaction]', () => { }, 'address length should be 20 bytes', ], - [ - { - nonce: ['0x1', '0x2'], - }, - 'nonce list should consist of at most 1 item', - ], [{ s: undefined as never }, 's is not defined'], [{ r: undefined as never }, 'r is not defined'], [{ yParity: undefined as never }, 'yParity is not defined'], [{ nonce: undefined as never }, 'nonce is not defined'], [{ address: undefined as never }, 'address is not defined'], [{ chainId: undefined as never }, 'chainId is not defined'], + [{ chainId: bigIntToHex(MAX_INTEGER + BIGINT_1) }, 'chainId exceeds 2^256 - 1'], + [ + { nonce: bigIntToHex(MAX_UINT64 + BIGINT_1) }, + 'Invalid EIP-7702 transaction: nonce exceeds 2^64 - 1', + ], + [{ yParity: '0x2' }, 'yParity should be 0 or 1'], + [{ r: bigIntToHex(MAX_INTEGER + BIGINT_1) }, 'r exceeds 2^256 - 1'], + [{ s: bigIntToHex(SECP256K1_ORDER_DIV_2 + BIGINT_1) }, 's > secp256k1n/2'], + [{ yParity: '0x0002' }, 'yParity cannot have leading zeros'], + [{ r: '0x0001' }, 'r cannot have leading zeros'], + [{ s: '0x0001' }, 's cannot have leading zeros'], + [{ nonce: '0x0001' }, 'nonce cannot have leading zeros'], + [{ chainId: '0x0001' }, 'chainId cannot have leading zeros'], ] for (const test of tests) { diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index ba06113818..1036c6e30d 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -6,12 +6,12 @@ import { Account, Address, BIGINT_0, + BIGINT_1, KECCAK256_NULL, bytesToBigInt, bytesToHex, bytesToUnprefixedHex, concatBytes, - createAddressFromString, ecrecover, equalsBytes, hexToBytes, @@ -64,6 +64,9 @@ const journalCacheCleanUpLabel = 'Journal/cache cleanup' const receiptsLabel = 'Receipts' const entireTxLabel = 'Entire tx' +// EIP-7702 flag: if contract code starts with these 3 bytes, it is a 7702-delegated EOA +const DELEGATION_7702_FLAG = new Uint8Array([0xef, 0x01, 0x00]) + /** * @ignore */ @@ -288,8 +291,24 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { } // EIP-3607: Reject transactions from senders with deployed code if (vm.common.isActivatedEIP(3607) && !equalsBytes(fromAccount.codeHash, KECCAK256_NULL)) { - const msg = _errorMsg('invalid sender address, address is not EOA (EIP-3607)', vm, block, tx) - throw new Error(msg) + const isActive7702 = vm.common.isActivatedEIP(7702) + switch (isActive7702) { + case true: { + const code = await state.getCode(caller) + // If the EOA is 7702-delegated, sending txs from this EOA is fine + if (equalsBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) break + // Trying to send TX from account with code (which is not 7702-delegated), falls through and throws + } + default: { + const msg = _errorMsg( + 'invalid sender address, address is not EOA (EIP-3607)', + vm, + block, + tx, + ) + throw new Error(msg) + } + } } // Check balance against upfront tx cost @@ -410,7 +429,8 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { } await vm.evm.journal.putAccount(caller, fromAccount) - const writtenAddresses = new Set() + let gasRefund = BIGINT_0 + if (tx.supports(Capability.EIP7702EOACode)) { // Add contract code for authority tuples provided by EIP 7702 tx const authorizationList = (tx).authorizationList @@ -426,33 +446,59 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { } // Address to take code from const address = data[1] - const nonceList = data[2] + const nonce = data[2] const yParity = bytesToBigInt(data[3]) const r = data[4] const s = data[5] - const rlpdSignedMessage = RLP.encode([chainId, address, nonceList]) + const rlpdSignedMessage = RLP.encode([chainId, address, nonce]) const toSign = keccak256(concatBytes(MAGIC, rlpdSignedMessage)) - const pubKey = ecrecover(toSign, yParity, r, s) + let pubKey + try { + pubKey = ecrecover(toSign, yParity, r, s) + } catch (e) { + // Invalid signature, continue + continue + } // Address to set code to const authority = new Address(publicToAddress(pubKey)) - const account = (await vm.stateManager.getAccount(authority)) ?? new Account() + const accountMaybeUndefined = await vm.stateManager.getAccount(authority) + const accountExists = accountMaybeUndefined !== undefined + const account = accountMaybeUndefined ?? new Account() + // Add authority address to warm addresses + vm.evm.journal.addAlwaysWarmAddress(authority.toString()) if (account.isContract()) { - // Note: vm also checks if the code has already been set once by a previous tuple - // So, if there are multiply entires for the same address, then vm is fine - continue + const code = await vm.stateManager.getCode(authority) + if (!equalsBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) { + // Account is a "normal" contract + continue + } } - if (nonceList.length !== 0 && account.nonce !== bytesToBigInt(nonceList[0])) { + + // Nonce check + if (caller.toString() === authority.toString()) { + if (account.nonce + BIGINT_1 !== bytesToBigInt(nonce)) { + // Edge case: caller is the authority, so is self-signing the delegation + // In this case, we "virtually" bump the account nonce by one + // We CANNOT put this updated nonce into the account trie, because then + // the EVM will bump the nonce once again, thus resulting in a wrong nonce + continue + } + } else if (account.nonce !== bytesToBigInt(nonce)) { continue } - const addressConverted = new Address(address) - const addressCode = await vm.stateManager.getCode(addressConverted) - await vm.stateManager.putCode(authority, addressCode) + if (accountExists) { + const refund = tx.common.param('perEmptyAccountCost') - tx.common.param('perAuthBaseGas') + gasRefund += refund + } - writtenAddresses.add(authority.toString()) - vm.evm.journal.addAlwaysWarmAddress(authority.toString()) + account.nonce++ + await vm.evm.journal.putAccount(authority, account) + + const addressCode = concatBytes(DELEGATION_7702_FLAG, address) + await vm.stateManager.putCode(authority, addressCode) } } @@ -541,7 +587,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { } // Process any gas refund - let gasRefund = results.execResult.gasRefund ?? BIGINT_0 + gasRefund += results.execResult.gasRefund ?? BIGINT_0 results.gasRefund = gasRefund const maxRefundQuotient = vm.common.param('maxRefundQuotient') if (gasRefund !== BIGINT_0) { @@ -635,15 +681,6 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { } } - /** - * Cleanup code of accounts written to in a 7702 transaction - */ - - for (const str of writtenAddresses) { - const address = createAddressFromString(str) - await vm.stateManager.putCode(address, new Uint8Array()) - } - if (enableProfiler) { // eslint-disable-next-line no-console console.timeEnd(accountsCleanUpLabel) diff --git a/packages/vm/test/api/EIPs/eip-7702.spec.ts b/packages/vm/test/api/EIPs/eip-7702.spec.ts index b23503aa0a..cc5d68f6d7 100644 --- a/packages/vm/test/api/EIPs/eip-7702.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7702.spec.ts @@ -5,14 +5,16 @@ import { Account, Address, BIGINT_1, - KECCAK256_NULL, - bigIntToBytes, + bigIntToUnpaddedBytes, concatBytes, createAddressFromString, + createZeroAddress, ecsign, hexToBytes, privateToAddress, + setLengthRight, unpadBytes, + zeros, } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak' import { equalsBytes } from 'ethereum-cryptography/utils' @@ -22,6 +24,7 @@ import { createVM, runTx } from '../../../src/index.js' import type { VM } from '../../../src/index.js' import type { AuthorizationListBytesItem } from '@ethereumjs/tx' +import type { PrefixedHexString } from '@ethereumjs/util' const common = new Common({ chain: Mainnet, hardfork: Hardfork.Cancun, eips: [7702] }) @@ -50,22 +53,25 @@ function getAuthorizationListItem(opts: GetAuthListOpts): AuthorizationListBytes const { chainId, nonce, address, pkey } = actualOpts const chainIdBytes = unpadBytes(hexToBytes(`0x${chainId.toString(16)}`)) - const nonceBytes = nonce !== undefined ? [unpadBytes(hexToBytes(`0x${nonce.toString(16)}`))] : [] + const nonceBytes = + nonce !== undefined ? unpadBytes(hexToBytes(`0x${nonce.toString(16)}`)) : new Uint8Array() const addressBytes = address.toBytes() const rlpdMsg = RLP.encode([chainIdBytes, addressBytes, nonceBytes]) const msgToSign = keccak256(concatBytes(new Uint8Array([5]), rlpdMsg)) const signed = ecsign(msgToSign, pkey) - return [chainIdBytes, addressBytes, nonceBytes, bigIntToBytes(signed.v), signed.r, signed.s] + return [ + chainIdBytes, + addressBytes, + nonceBytes, + bigIntToUnpaddedBytes(signed.v - BigInt(27)), + signed.r, + signed.s, + ] } -async function runTest( - authorizationListOpts: GetAuthListOpts[], - expect: Uint8Array, - vm?: VM, - skipEmptyCode?: boolean, -) { +async function runTest(authorizationListOpts: GetAuthListOpts[], expect: Uint8Array, vm?: VM) { vm = vm ?? (await createVM({ common })) const authList = authorizationListOpts.map((opt) => getAuthorizationListItem(opt)) const tx = createEOACode7702Tx( @@ -94,12 +100,6 @@ async function runTest( const slot = hexToBytes(`0x${'00'.repeat(31)}01`) const value = await vm.stateManager.getStorage(defaultAuthAddr, slot) assert.ok(equalsBytes(unpadBytes(expect), value)) - - if (skipEmptyCode === undefined) { - // Check that the code is cleaned after the `runTx` - const account = (await vm.stateManager.getAccount(defaultAuthAddr)) ?? new Account() - assert.ok(equalsBytes(account.codeHash, KECCAK256_NULL)) - } } describe('EIP 7702: set code to EOA accounts', () => { @@ -115,7 +115,8 @@ describe('EIP 7702: set code to EOA accounts', () => { ) // Try to set code to two different addresses - // Only the first is valid + // Only the first is valid: the second tuple will have the nonce value 0, but the + // nonce of the account is already set to 1 (by the first tuple) await runTest( [ { @@ -183,7 +184,6 @@ describe('EIP 7702: set code to EOA accounts', () => { ], new Uint8Array(), vm, - true, ) }) @@ -200,8 +200,9 @@ describe('EIP 7702: set code to EOA accounts', () => { // 5 * PUSH0: 10 // 1 * PUSH20: 3 // 1 * GAS: 2 - // 1x warm call: 100 - // Total: 115 + // 1x warm call: 100 (to auth address) + // --> This calls into the cold code1Addr, so add 2600 cold account gas cost + // Total: 2715 const checkAddressWarmCode = hexToBytes( `0x5F5F5F5F5F73${defaultAuthAddr.toString().slice(2)}5AF1`, ) @@ -228,44 +229,83 @@ describe('EIP 7702: set code to EOA accounts', () => { await vm.stateManager.putAccount(defaultSenderAddr, acc) const res = await runTx(vm, { tx }) - assert.ok(res.execResult.executionGasUsed === BigInt(115)) + assert.ok(res.execResult.executionGasUsed === BigInt(2715)) }) +}) - // This test shows, that due to EIP-161, if an EOA has 0 nonce and 0 balance, - // if EIP-7702 code is being ran which sets storage on this EOA, - // the account is still deleted after the tx (and thus also the storage is wiped) - it('EIP-161 test case', async () => { - const vm = await createVM({ common }) - const authList = [ - getAuthorizationListItem({ - address: code1Addr, - }), +describe('test EIP-7702 opcodes', () => { + it('should correctly report EXTCODESIZE/EXTCODEHASH/EXTCODECOPY opcodes', async () => { + // extcodesize and extcodehash + const deploymentAddress = createZeroAddress() + const randomCode = hexToBytes('0x010203040506') + const randomCodeAddress = createAddressFromString('0x' + 'aa'.repeat(20)) + + const tests: { + code: PrefixedHexString + expectedStorage: Uint8Array + name: string + }[] = [ + // EXTCODESIZE + { + // PUSH20 EXTCODESIZE PUSH0 SSTORE STOP + code: ('0x73' + defaultAuthAddr.toString().slice(2) + '3b' + '5f5500'), + expectedStorage: bigIntToUnpaddedBytes(BigInt(randomCode.length)), + name: 'EXTCODESIZE', + }, + // EXTCODEHASH + { + // PUSH20 EXTCODEHASH PUSH0 SSTORE STOP + code: ('0x73' + defaultAuthAddr.toString().slice(2) + '3f' + '5f5500'), + expectedStorage: keccak256(randomCode), + name: 'EXTCODEHASH', + }, + // EXTCODECOPY + { + // PUSH1 32 PUSH0 PUSH0 PUSH20 EXTCODEHASH PUSH0 MLOAD PUSH0 SSTORE STOP + code: ( + ('0x60205f5f73' + defaultAuthAddr.toString().slice(2) + '3c' + '5f515f5500') + ), + expectedStorage: setLengthRight(randomCode, 32), + name: 'EXTCODECOPY', + }, ] - const tx = createEOACode7702Tx( + + const authTx = createEOACode7702Tx( { gasLimit: 100000, maxFeePerGas: 1000, - authorizationList: authList, - to: defaultAuthAddr, - // value: BIGINT_1 // Note, by enabling this line, the account will not get deleted - // Therefore, this test will pass + authorizationList: [ + getAuthorizationListItem({ + address: randomCodeAddress, + }), + ], + to: deploymentAddress, + value: BIGINT_1, }, { common }, ).sign(defaultSenderPkey) - // Store value 1 in storage slot 1 - // PUSH1 PUSH1 SSTORE STOP - const code = hexToBytes('0x600160015500') - await vm.stateManager.putCode(code1Addr, code) + async function runOpcodeTest(code: Uint8Array, expectedOutput: Uint8Array, name: string) { + const vm = await createVM({ common }) - const acc = (await vm.stateManager.getAccount(defaultSenderAddr)) ?? new Account() - acc.balance = BigInt(1_000_000_000) - await vm.stateManager.putAccount(defaultSenderAddr, acc) + const acc = (await vm.stateManager.getAccount(defaultSenderAddr)) ?? new Account() + acc.balance = BigInt(1_000_000_000) + await vm.stateManager.putAccount(defaultSenderAddr, acc) + + // The code to either store extcodehash / extcodesize in slot 0 + await vm.stateManager.putCode(deploymentAddress, code) + // The code the authority points to (and should thus be loaded by above script) + await vm.stateManager.putCode(randomCodeAddress, randomCode) + + // Set authority and immediately call into the contract to get the extcodehash / extcodesize + await runTx(vm, { tx: authTx }) - await runTx(vm, { tx }) + const result = await vm.stateManager.getStorage(deploymentAddress, zeros(32)) + assert.ok(equalsBytes(result, expectedOutput), `FAIL test: ${name}`) + } - // Note: due to EIP-161, defaultAuthAddr is now deleted - const account = await vm.stateManager.getAccount(defaultAuthAddr) - assert.ok(account === undefined) + for (const test of tests) { + await runOpcodeTest(hexToBytes(test.code), test.expectedStorage, test.name) + } }) }) diff --git a/packages/vm/test/api/runBlock.spec.ts b/packages/vm/test/api/runBlock.spec.ts index e4f521058d..29d817c562 100644 --- a/packages/vm/test/api/runBlock.spec.ts +++ b/packages/vm/test/api/runBlock.spec.ts @@ -20,7 +20,6 @@ import { Address, BIGINT_1, KECCAK256_RLP, - bigIntToBytes, concatBytes, createAddressFromString, createZeroAddress, @@ -614,7 +613,9 @@ describe('runBlock() -> tx types', async () => { const msgToSign = keccak256(concatBytes(new Uint8Array([5]), rlpdMsg)) const signed = ecsign(msgToSign, pkey) - return [chainIdBytes, addressBytes, nonceBytes, bigIntToBytes(signed.v), signed.r, signed.s] + const yParity = signed.v === BigInt(27) ? new Uint8Array() : new Uint8Array([1]) + + return [chainIdBytes, addressBytes, nonceBytes, yParity, signed.r, signed.s] } const common = new Common({ @@ -639,6 +640,7 @@ describe('runBlock() -> tx types', async () => { const authorizationListOpts2 = [ { address: code2Addr, + nonce: 1, }, ]