From 2f26e9d297e00c0c6eb3245ac0e24a338ec351fa Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 12 Aug 2024 19:00:55 +0200 Subject: [PATCH 01/31] tx: implement strict 7702 validation --- packages/evm/src/params.ts | 8 ----- packages/tx/src/capabilities/eip7702.ts | 2 +- packages/tx/src/params.ts | 9 +++++ packages/tx/src/types.ts | 4 +-- packages/tx/src/util.ts | 45 +++++++++++++++---------- 5 files changed, 40 insertions(+), 28 deletions(-) diff --git a/packages/evm/src/params.ts b/packages/evm/src/params.ts index f4483d1201..3bff7eaab6 100644 --- a/packages/evm/src/params.ts +++ b/packages/evm/src/params.ts @@ -420,12 +420,4 @@ export const paramsEVM: ParamsDict = { eofcreateGas: 32000, // Base fee of the EOFCREATE opcode (Same as CREATE/CREATE2) returncontractGas: 0, // Base fee of the RETURNCONTRACT opcode }, - /** -. * Set EOA account code for one transaction -. */ - 7702: { - // TODO: Set correct minimum hardfork - // gasPrices - perAuthBaseGas: 2500, // Gas cost of each authority item - }, } diff --git a/packages/tx/src/capabilities/eip7702.ts b/packages/tx/src/capabilities/eip7702.ts index 3839485a0b..a847096333 100644 --- a/packages/tx/src/capabilities/eip7702.ts +++ b/packages/tx/src/capabilities/eip7702.ts @@ -10,7 +10,7 @@ import type { EIP7702CompatibleTx } from '../types.js' export function getDataGas(tx: EIP7702CompatibleTx): bigint { const eip2930Cost = BigInt(AccessLists.getDataGasEIP2930(tx.accessList, tx.common)) const eip7702Cost = BigInt( - tx.authorizationList.length * Number(tx.common.param('perAuthBaseGas')), + tx.authorizationList.length * Number(tx.common.param('perEmptyAccountCost')), ) return Legacy.getDataGas(tx, eip2930Cost + eip7702Cost) } diff --git a/packages/tx/src/params.ts b/packages/tx/src/params.ts index 02f487b583..8eac4ae249 100644 --- a/packages/tx/src/params.ts +++ b/packages/tx/src/params.ts @@ -43,4 +43,13 @@ export const paramsTx: ParamsDict = { 4844: { blobCommitmentVersionKzg: 1, // The number indicated a versioned hash is a KZG commitment }, + /** +. * Set EOA account code for one transaction +. */ + 7702: { + // TODO: Set correct minimum hardfork + // gasPrices + perAuthBaseGas: 2500, // Gas cost of each authority item, provided the authority exists in the trie + perEmptyAccountCost: 25000, // Gas cost of each authority item, in case the authority does not exist in the trie + }, } diff --git a/packages/tx/src/types.ts b/packages/tx/src/types.ts index 348122124f..c34e786836 100644 --- a/packages/tx/src/types.ts +++ b/packages/tx/src/types.ts @@ -600,7 +600,7 @@ export type AccessList = AccessListItem[] export type AuthorizationListItem = { chainId: PrefixedHexString address: PrefixedHexString - nonce: PrefixedHexString[] + nonce: PrefixedHexString yParity: PrefixedHexString r: PrefixedHexString s: PrefixedHexString @@ -610,7 +610,7 @@ export type AuthorizationListItem = { export type AuthorizationListBytesItem = [ Uint8Array, Uint8Array, - Uint8Array[], + Uint8Array, Uint8Array, Uint8Array, Uint8Array, diff --git a/packages/tx/src/util.ts b/packages/tx/src/util.ts index 591c0aa853..3d09f6eb13 100644 --- a/packages/tx/src/util.ts +++ b/packages/tx/src/util.ts @@ -1,5 +1,11 @@ import { + BIGINT_0, + BIGINT_1, + MAX_INTEGER, + MAX_UINT64, type PrefixedHexString, + SECP256K1_ORDER_DIV_2, + bytesToBigInt, bytesToHex, hexToBytes, setLengthLeft, @@ -149,15 +155,12 @@ export class AuthorizationLists { } const chainId = hexToBytes(item.chainId) const addressBytes = hexToBytes(item.address) - const nonceList = [] - for (let j = 0; j < item.nonce.length; j++) { - nonceList.push(hexToBytes(item.nonce[j])) - } + const nonce = hexToBytes(item.nonce) const yParity = hexToBytes(item.yParity) const r = hexToBytes(item.r) const s = hexToBytes(item.s) - newAuthorizationList.push([chainId, addressBytes, nonceList, yParity, r, s]) + newAuthorizationList.push([chainId, addressBytes, nonce, yParity, r, s]) } bufferAuthorizationList = newAuthorizationList } else { @@ -168,18 +171,14 @@ export class AuthorizationLists { const data = bufferAuthorizationList[i] const chainId = bytesToHex(data[0]) const address = bytesToHex(data[1]) - const nonces = data[2] - const nonceList: PrefixedHexString[] = [] - for (let j = 0; j < nonces.length; j++) { - nonceList.push(bytesToHex(nonces[j])) - } + const nonce = bytesToHex(data[2]) const yParity = bytesToHex(data[3]) const r = bytesToHex(data[4]) const s = bytesToHex(data[5]) const jsonItem: AuthorizationListItem = { chainId, address, - nonce: nonceList, + nonce, yParity, r, s, @@ -198,19 +197,31 @@ export class AuthorizationLists { public static verifyAuthorizationList(authorizationList: AuthorizationListBytes) { for (let key = 0; key < authorizationList.length; key++) { const authorizationListItem = authorizationList[key] + const chainId = authorizationListItem[0] const address = authorizationListItem[1] - const nonceList = authorizationListItem[2] + const nonce = authorizationListItem[2] const yParity = authorizationListItem[3] const r = authorizationListItem[4] const s = authorizationListItem[5] - validateNoLeadingZeroes({ yParity, r, s }) + validateNoLeadingZeroes({ yParity, r, s, nonce, chainId }) if (address.length !== 20) { throw new Error('Invalid EIP-7702 transaction: address length should be 20 bytes') } - if (nonceList.length > 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') } } } From f59b5a0e15d57b542927907d9eeab9d333dd614a Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 12 Aug 2024 21:51:01 +0200 Subject: [PATCH 02/31] vm: update 7702 tx validation --- packages/vm/src/runTx.ts | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 6832b64028..c9f5c02b53 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -439,33 +439,45 @@ 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) // 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), new Uint8Array([0xef, 0x01, 0x00]))) { + // Account is a "normal" contract + continue + } } - if (nonceList.length !== 0 && account.nonce !== bytesToBigInt(nonceList[0])) { + if (account.nonce !== bytesToBigInt(nonce)) { continue } - const addressConverted = new Address(address) - const addressCode = await vm.stateManager.getCode(addressConverted) + if (accountExists) { + const refund = vm.common.param('perEmptyAccountCost') - vm.common.param('perAuthBaseGas') + fromAccount.balance += refund + } + + fromAccount.nonce++ + await vm.evm.journal.putAccount(caller, fromAccount) + + const addressCode = concatBytes(new Uint8Array([0xef, 0x01, 0x00]), address) await vm.stateManager.putCode(authority, addressCode) writtenAddresses.add(authority.toString()) - vm.evm.journal.addAlwaysWarmAddress(authority.toString()) } } From 5d8ba3959dd71546e2bae89899d837f651718854 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 12 Aug 2024 22:21:41 +0200 Subject: [PATCH 03/31] evm: update 7702 [no ci] --- packages/evm/src/evm.ts | 10 +++++++ packages/evm/src/opcodes/functions.ts | 29 +++++++++++++++----- packages/evm/src/opcodes/gas.ts | 38 +++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index c39855664f..eaaa7354a6 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -1013,6 +1013,16 @@ 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), new Uint8Array([0xef, 0x01, 0x00])) + ) { + const address = new Address(message.code.slice(3, 24)) + message.code = await this.stateManager.getCode(address) + } + message.isCompiled = false message.chargeCodeAccesses = true } diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index d20b18faa0..a1ed5743a9 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -33,6 +33,7 @@ import { setLengthLeft, setLengthRight, } from '@ethereumjs/util' +import { equalBytes } from '@noble/curves/abstract/utils' import { keccak256 } from 'ethereum-cryptography/keccak.js' import { EOFContainer, EOFContainerMode } from '../eof/container.js' @@ -68,6 +69,15 @@ export interface AsyncOpHandler { export type OpHandler = SyncOpHandler | AsyncOpHandler +async function eip7702CodeCheck(runState: RunState, code: Uint8Array) { + if (equalBytes(code, new Uint8Array([0xef, 0x01, 0x00]))) { + const address = new Address(code.slice(3, 24)) + return runState.stateManager.getCode(address) + } + + return code +} + // the opcode functions export const handlers: Map = new Map([ // 0x00: STOP @@ -520,15 +530,17 @@ 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( @@ -541,15 +553,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) @@ -562,17 +577,19 @@ 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) // 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 // Therefore, push the hash of EOFBYTES to the stack runState.stack.push(bytesToBigInt(EOFHASH)) return + } else if (common.isActivatedEIP(7702)) { + code = await eip7702CodeCheck(runState, code) } const account = await runState.stateManager.getAccount(address) diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index 6f8cd64252..2a06785f3d 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -12,6 +12,7 @@ import { VERKLE_CODE_SIZE_LEAF_KEY, VERKLE_VERSION_LEAF_KEY, bigIntToBytes, + equalsBytes, getVerkleTreeIndexesForStorageSlot, setLengthLeft, } from '@ethereumjs/util' @@ -34,9 +35,18 @@ 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) { + const code = await runState.stateManager.getCode(address) + if (equalsBytes(code, new Uint8Array([0xef, 0x01, 0x00]))) { + return accessAddressEIP2929(runState, code.slice(3, 24), common) + } + 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 @@ -183,6 +193,10 @@ export const dynamicGasHandlers: Map Date: Mon, 12 Aug 2024 22:52:32 +0200 Subject: [PATCH 04/31] tx: add / fix 7702 tests --- packages/tx/test/eip7702.spec.ts | 34 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/tx/test/eip7702.spec.ts b/packages/tx/test/eip7702.spec.ts index ef8e980d51..dc116cb644 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 { create7702EOACodeTx } 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, @@ -65,18 +74,25 @@ describe('[EOACodeEIP7702Transaction]', () => { }, '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) { @@ -86,7 +102,7 @@ describe('[EOACodeEIP7702Transaction]', () => { create7702EOACodeTx(txData, { common }), testName }) } - + create7702EOACodeTx(getTxData(), { common }) assert.doesNotThrow(() => { create7702EOACodeTx(getTxData(), { common }) }) From ad3effbdaf00b5d5cb2b94ead819a0522e5b2d75 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 12 Aug 2024 23:45:10 +0200 Subject: [PATCH 05/31] vm: fix test encoding of authorization lists [no ci] --- packages/vm/test/api/EIPs/eip-7702.spec.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/vm/test/api/EIPs/eip-7702.spec.ts b/packages/vm/test/api/EIPs/eip-7702.spec.ts index e4300ba371..405db457a1 100644 --- a/packages/vm/test/api/EIPs/eip-7702.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7702.spec.ts @@ -6,7 +6,7 @@ import { Address, BIGINT_1, KECCAK256_NULL, - bigIntToBytes, + bigIntToUnpaddedBytes, concatBytes, createAddressFromString, ecsign, @@ -49,14 +49,22 @@ 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( From f8950b8786bc22569621da830b931e821e89dddf Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 14 Aug 2024 13:51:34 +0200 Subject: [PATCH 06/31] vm: correctly put authority nonce --- packages/vm/src/runTx.ts | 2 +- packages/vm/test/api/EIPs/eip-7702.spec.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 811666073d..1c0d390e5c 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -472,7 +472,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { } fromAccount.nonce++ - await vm.evm.journal.putAccount(caller, fromAccount) + await vm.evm.journal.putAccount(authority, fromAccount) const addressCode = concatBytes(new Uint8Array([0xef, 0x01, 0x00]), address) await vm.stateManager.putCode(authority, addressCode) diff --git a/packages/vm/test/api/EIPs/eip-7702.spec.ts b/packages/vm/test/api/EIPs/eip-7702.spec.ts index 405db457a1..d9411e0c4d 100644 --- a/packages/vm/test/api/EIPs/eip-7702.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7702.spec.ts @@ -20,7 +20,7 @@ import { assert, describe, it } from 'vitest' import { VM, runTx } from '../../../src/index.js' -import type { AuthorizationListBytesItem } from '@ethereumjs/common' +import type { AuthorizationListBytesItem } from '@ethereumjs/tx' const common = new Common({ chain: Mainnet, hardfork: Hardfork.Cancun, eips: [7702] }) @@ -122,7 +122,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( [ { From 842db9d1a2ac42724628f8bbb05f8f5838d280d5 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Fri, 16 Aug 2024 17:56:11 +0200 Subject: [PATCH 07/31] vm: add test 7702 extcodehash/extcodesize evm: fix extcodehash/extcodesize for delegated accounts --- packages/evm/src/opcodes/functions.ts | 7 ++- packages/vm/test/api/EIPs/eip-7702.spec.ts | 56 ++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index 943a2240ab..6fb0235e25 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -61,7 +61,7 @@ export interface AsyncOpHandler { export type OpHandler = SyncOpHandler | AsyncOpHandler async function eip7702CodeCheck(runState: RunState, code: Uint8Array) { - if (equalBytes(code, new Uint8Array([0xef, 0x01, 0x00]))) { + if (equalBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { const address = new Address(code.slice(3, 24)) return runState.stateManager.getCode(address) } @@ -534,9 +534,7 @@ export const handlers: Map = new Map([ code = await eip7702CodeCheck(runState, code) } - const size = BigInt( - await runState.stateManager.getCodeSize(createAddressFromStackBigInt(addressBigInt)), - ) + const size = BigInt(code.length) runState.stack.push(size) }, @@ -581,6 +579,7 @@ export const handlers: Map = new Map([ return } else if (common.isActivatedEIP(7702)) { code = await eip7702CodeCheck(runState, code) + return keccak256(code) } const account = await runState.stateManager.getAccount(address) diff --git a/packages/vm/test/api/EIPs/eip-7702.spec.ts b/packages/vm/test/api/EIPs/eip-7702.spec.ts index d9411e0c4d..d1e8a285bd 100644 --- a/packages/vm/test/api/EIPs/eip-7702.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7702.spec.ts @@ -7,12 +7,15 @@ import { BIGINT_1, KECCAK256_NULL, bigIntToUnpaddedBytes, + bytesToBigInt, concatBytes, createAddressFromString, + createZeroAddress, ecsign, hexToBytes, privateToAddress, unpadBytes, + zeros, } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak' import { equalsBytes } from 'ethereum-cryptography/utils' @@ -21,6 +24,7 @@ import { assert, describe, it } from 'vitest' import { VM, runTx } 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] }) @@ -277,3 +281,55 @@ describe('EIP 7702: set code to EOA accounts', () => { assert.ok(account === undefined) }) }) + +describe.only('test EIP-7702 opcodes', () => { + it('should correctly report EXTCODE* opcodes', async () => { + // extcodesize and extcodehash + const deploymentAddress = createZeroAddress() + const randomCode = hexToBytes('0x010203040506') + const randomCodeAddress = createAddressFromString('0x' + 'aa'.repeat(20)) + + const opcodes = ['3b', '3f'] + const expected = [BigInt(randomCode.length), bytesToBigInt(keccak256(randomCode))] + + const authTx = create7702EOACodeTx( + { + gasLimit: 100000, + maxFeePerGas: 1000, + authorizationList: [ + getAuthorizationListItem({ + address: randomCodeAddress, + }), + ], + to: deploymentAddress, + value: BIGINT_1, + }, + { common }, + ).sign(defaultSenderPkey) + + // TODO make this more elegant + let i = -1 + for (const opcode of opcodes) { + i++ + const actualCode = hexToBytes( + ('0x73' + defaultAuthAddr.toString().slice(2) + opcode + '5f5500'), + ) + const vm = await VM.create({ common }) + + 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, actualCode) + // 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 }) + + const result = await vm.stateManager.getStorage(deploymentAddress, zeros(32)) + assert.equal(bytesToBigInt(result), expected[i]) + } + }) +}) From 0b6692f4d588611d6cea9ceee92ddef0a280dad1 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sat, 17 Aug 2024 00:32:16 +0200 Subject: [PATCH 08/31] vm: expand extcode* tests 7702 [no ci] --- packages/vm/test/api/EIPs/eip-7702.spec.ts | 50 +++++++++++++++++----- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/vm/test/api/EIPs/eip-7702.spec.ts b/packages/vm/test/api/EIPs/eip-7702.spec.ts index d1e8a285bd..30453803e3 100644 --- a/packages/vm/test/api/EIPs/eip-7702.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7702.spec.ts @@ -14,6 +14,7 @@ import { ecsign, hexToBytes, privateToAddress, + setLengthRight, unpadBytes, zeros, } from '@ethereumjs/util' @@ -290,7 +291,37 @@ describe.only('test EIP-7702 opcodes', () => { const randomCodeAddress = createAddressFromString('0x' + 'aa'.repeat(20)) const opcodes = ['3b', '3f'] - const expected = [BigInt(randomCode.length), bytesToBigInt(keccak256(randomCode))] + const expected = [bigIntToUnpaddedBytes(BigInt(randomCode.length)), keccak256(randomCode)] + + 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 SSTORE STOP + code: ( + ('0x60205f5f73' + defaultAuthAddr.toString().slice(2) + '3c' + '5f5500') + ), + expectedStorage: setLengthRight(randomCode, 32), + name: 'EXTCODECOPY', + }, + ] const authTx = create7702EOACodeTx( { @@ -307,13 +338,7 @@ describe.only('test EIP-7702 opcodes', () => { { common }, ).sign(defaultSenderPkey) - // TODO make this more elegant - let i = -1 - for (const opcode of opcodes) { - i++ - const actualCode = hexToBytes( - ('0x73' + defaultAuthAddr.toString().slice(2) + opcode + '5f5500'), - ) + async function runOpcodeTest(code: Uint8Array, expectedOutput: Uint8Array, name: string) { const vm = await VM.create({ common }) const acc = (await vm.stateManager.getAccount(defaultSenderAddr)) ?? new Account() @@ -321,7 +346,7 @@ describe.only('test EIP-7702 opcodes', () => { await vm.stateManager.putAccount(defaultSenderAddr, acc) // The code to either store extcodehash / extcodesize in slot 0 - await vm.stateManager.putCode(deploymentAddress, actualCode) + 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) @@ -329,7 +354,12 @@ describe.only('test EIP-7702 opcodes', () => { await runTx(vm, { tx: authTx }) const result = await vm.stateManager.getStorage(deploymentAddress, zeros(32)) - assert.equal(bytesToBigInt(result), expected[i]) + console.log(result, expectedOutput) + assert.ok(equalsBytes(result, expectedOutput), `FAIL test: ${name}`) + } + + for (const test of tests) { + await runOpcodeTest(hexToBytes(test.code), test.expectedStorage, test.name) } }) }) From 7a01e8196f1ef1d195d15fdb748a5c40eb5b6466 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sat, 17 Aug 2024 20:18:08 +0200 Subject: [PATCH 09/31] tx/vm: update tests [no ci] --- packages/tx/test/eip7702.spec.ts | 2 +- packages/vm/test/api/EIPs/eip-7702.spec.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/tx/test/eip7702.spec.ts b/packages/tx/test/eip7702.spec.ts index 0eb003caa4..8136e58d1d 100644 --- a/packages/tx/test/eip7702.spec.ts +++ b/packages/tx/test/eip7702.spec.ts @@ -102,7 +102,7 @@ describe('[EOACode7702Transaction]', () => { createEOACode7702Tx(txData, { common }), testName }) } - create7702EOACodeTx(getTxData(), { common }) + assert.doesNotThrow(() => { createEOACode7702Tx(getTxData(), { common }) }) diff --git a/packages/vm/test/api/EIPs/eip-7702.spec.ts b/packages/vm/test/api/EIPs/eip-7702.spec.ts index a968ff5996..2b59998463 100644 --- a/packages/vm/test/api/EIPs/eip-7702.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7702.spec.ts @@ -7,7 +7,6 @@ import { BIGINT_1, KECCAK256_NULL, bigIntToUnpaddedBytes, - bytesToBigInt, concatBytes, createAddressFromString, createZeroAddress, @@ -290,9 +289,6 @@ describe.only('test EIP-7702 opcodes', () => { const randomCode = hexToBytes('0x010203040506') const randomCodeAddress = createAddressFromString('0x' + 'aa'.repeat(20)) - const opcodes = ['3b', '3f'] - const expected = [bigIntToUnpaddedBytes(BigInt(randomCode.length)), keccak256(randomCode)] - const tests: { code: PrefixedHexString expectedStorage: Uint8Array @@ -323,7 +319,7 @@ describe.only('test EIP-7702 opcodes', () => { }, ] - const authTx = create7702EOACodeTx( + const authTx = createEOACode7702Tx( { gasLimit: 100000, maxFeePerGas: 1000, @@ -354,7 +350,6 @@ describe.only('test EIP-7702 opcodes', () => { await runTx(vm, { tx: authTx }) const result = await vm.stateManager.getStorage(deploymentAddress, zeros(32)) - console.log(result, expectedOutput) assert.ok(equalsBytes(result, expectedOutput), `FAIL test: ${name}`) } From 0310175e095cabd2996e3410efc156a6e85c8926 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sat, 17 Aug 2024 21:06:23 +0200 Subject: [PATCH 10/31] evm/vm: update opcodes and fix tests 7702 --- packages/evm/src/opcodes/functions.ts | 3 ++- packages/vm/test/api/EIPs/eip-7702.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index 6fb0235e25..0d29b62bcc 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -579,7 +579,8 @@ export const handlers: Map = new Map([ return } else if (common.isActivatedEIP(7702)) { code = await eip7702CodeCheck(runState, code) - return keccak256(code) + runState.stack.push(bytesToBigInt(keccak256(code))) + return } const account = await runState.stateManager.getAccount(address) diff --git a/packages/vm/test/api/EIPs/eip-7702.spec.ts b/packages/vm/test/api/EIPs/eip-7702.spec.ts index 2b59998463..d7204245cb 100644 --- a/packages/vm/test/api/EIPs/eip-7702.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7702.spec.ts @@ -310,9 +310,9 @@ describe.only('test EIP-7702 opcodes', () => { }, // EXTCODECOPY { - // PUSH1 32 PUSH0 PUSH0 PUSH20 EXTCODEHASH PUSH0 SSTORE STOP + // PUSH1 32 PUSH0 PUSH0 PUSH20 EXTCODEHASH PUSH0 MLOAD PUSH0 SSTORE STOP code: ( - ('0x60205f5f73' + defaultAuthAddr.toString().slice(2) + '3c' + '5f5500') + ('0x60205f5f73' + defaultAuthAddr.toString().slice(2) + '3c' + '5f515f5500') ), expectedStorage: setLengthRight(randomCode, 32), name: 'EXTCODECOPY', From d773d2b734a060cd76cc758a5c214babdb3a16bf Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sat, 17 Aug 2024 21:17:54 +0200 Subject: [PATCH 11/31] fix cspell [no ci] --- packages/vm/test/api/EIPs/eip-7702.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/test/api/EIPs/eip-7702.spec.ts b/packages/vm/test/api/EIPs/eip-7702.spec.ts index d7204245cb..4b922d9044 100644 --- a/packages/vm/test/api/EIPs/eip-7702.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7702.spec.ts @@ -283,7 +283,7 @@ describe('EIP 7702: set code to EOA accounts', () => { }) describe.only('test EIP-7702 opcodes', () => { - it('should correctly report EXTCODE* opcodes', async () => { + it('should correctly report EXTCODESIZE/EXTCODEHASH/EXTCODECOPY opcodes', async () => { // extcodesize and extcodehash const deploymentAddress = createZeroAddress() const randomCode = hexToBytes('0x010203040506') From d4d3100fe2ba47c4be92c7b6cdcacd7512d5ec37 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 19 Aug 2024 03:13:23 +0200 Subject: [PATCH 12/31] vm: get params from tx for 7702 [no ci] --- packages/vm/src/runTx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 66d8bf0eec..4a712eca80 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -467,7 +467,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { } if (accountExists) { - const refund = vm.common.param('perEmptyAccountCost') - vm.common.param('perAuthBaseGas') + const refund = tx.common.param('perEmptyAccountCost') - tx.common.param('perAuthBaseGas') fromAccount.balance += refund } From 0fa00d333a5b67dae475ea226106651c74b3e07c Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 19 Aug 2024 03:44:00 +0200 Subject: [PATCH 13/31] vm: 7702 correctly apply the refund [no ci] --- packages/vm/src/runTx.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 4a712eca80..1fce1d8b6a 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -423,6 +423,8 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { } await vm.evm.journal.putAccount(caller, fromAccount) + let gasRefund = BIGINT_0 + const writtenAddresses = new Set() if (tx.supports(Capability.EIP7702EOACode)) { // Add contract code for authority tuples provided by EIP 7702 tx @@ -468,11 +470,12 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { if (accountExists) { const refund = tx.common.param('perEmptyAccountCost') - tx.common.param('perAuthBaseGas') - fromAccount.balance += refund + gasRefund += refund + await vm.evm.journal.putAccount(caller, fromAccount) } - fromAccount.nonce++ - await vm.evm.journal.putAccount(authority, fromAccount) + account.nonce++ + await vm.evm.journal.putAccount(authority, account) const addressCode = concatBytes(new Uint8Array([0xef, 0x01, 0x00]), address) await vm.stateManager.putCode(authority, addressCode) @@ -566,7 +569,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) { From 1a21f86ac743d6b3006f8cf5bcaa90bf77e1a083 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 19 Aug 2024 04:22:02 +0200 Subject: [PATCH 14/31] vm: 7702: correctly handle self-sponsored txs [no ci] --- packages/vm/src/runTx.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 1fce1d8b6a..c9f5f8a3a1 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -7,6 +7,7 @@ import { Account, Address, BIGINT_0, + BIGINT_1, KECCAK256_NULL, bytesToBigInt, bytesToHex, @@ -292,8 +293,21 @@ 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) + if (vm.common.isActivatedEIP(7702)) { + const code = await state.getCode(caller) + if (!equalsBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { + const msg = _errorMsg( + 'invalid sender address, address is not EOA (EIP-3607)', + vm, + block, + tx, + ) + throw new Error(msg) + } + } else { + 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 @@ -464,14 +478,23 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { continue } } - if (account.nonce !== bytesToBigInt(nonce)) { + + // 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 } if (accountExists) { const refund = tx.common.param('perEmptyAccountCost') - tx.common.param('perAuthBaseGas') gasRefund += refund - await vm.evm.journal.putAccount(caller, fromAccount) } account.nonce++ From 61629571f542d6569bb7fc95e14352a53ec2e904 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 19 Aug 2024 06:25:19 +0200 Subject: [PATCH 15/31] tx: throw if authorization list is empty --- packages/tx/src/util.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/tx/src/util.ts b/packages/tx/src/util.ts index 6fdc9eeb5e..284b46d560 100644 --- a/packages/tx/src/util.ts +++ b/packages/tx/src/util.ts @@ -199,6 +199,9 @@ export class AuthorizationLists { } public static verifyAuthorizationList(authorizationList: AuthorizationListBytes) { + if (authorizationList.length === 0) { + throw new Error('Invalid EIP-7702 transaction: authorization list is empty') + } for (let key = 0; key < authorizationList.length; key++) { const authorizationListItem = authorizationList[key] const chainId = authorizationListItem[0] From 2043d1e79a0b6d9e11ce84a196289f3cb994ca0d Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 19 Aug 2024 06:25:39 +0200 Subject: [PATCH 16/31] vm: requests do not throw if code is non-existant --- packages/vm/src/requests.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vm/src/requests.ts b/packages/vm/src/requests.ts index 52baccb219..e035b9db8d 100644 --- a/packages/vm/src/requests.ts +++ b/packages/vm/src/requests.ts @@ -69,9 +69,9 @@ const accumulateEIP7002Requests = async ( const code = await vm.stateManager.getCode(withdrawalsAddress) if (code.length === 0) { - throw new Error( + /*throw new Error( 'Attempt to accumulate EIP-7002 requests failed: the contract does not exist. Ensure the deployment tx has been run, or that the required contract code is stored', - ) + )*/ } const systemAddressBytes = bigIntToAddressBytes(vm.common.param('systemAddress')) @@ -119,9 +119,9 @@ const accumulateEIP7251Requests = async ( const code = await vm.stateManager.getCode(consolidationsAddress) if (code.length === 0) { - throw new Error( + /*throw new Error( 'Attempt to accumulate EIP-7251 requests failed: the contract does not exist. Ensure the deployment tx has been run, or that the required contract code is stored', - ) + )*/ } const systemAddressBytes = bigIntToAddressBytes(vm.common.param('systemAddress')) From 9bd46adfeaa996c437b52fb6085dffd8cb7dfa99 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 19 Aug 2024 16:49:42 +0200 Subject: [PATCH 17/31] evm: ensure correct extcodehash reporting if account is delegated to a non-existing account --- packages/evm/src/opcodes/functions.ts | 28 +++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index 0d29b62bcc..a79204eb98 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -60,9 +60,15 @@ export interface AsyncOpHandler { export type OpHandler = SyncOpHandler | AsyncOpHandler -async function eip7702CodeCheck(runState: RunState, code: Uint8Array) { +function getEIP7702DelegatedAddress(code: Uint8Array) { if (equalBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { - const address = new Address(code.slice(3, 24)) + 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) } @@ -571,16 +577,26 @@ export const handlers: Map = new Map([ const address = createAddressFromStackBigInt(addressBigInt) // EOF check - let code = await runState.stateManager.getCode(address) + const code = await runState.stateManager.getCode(address) if (isEOF(code)) { // In legacy code, the target code is treated as to be "EOFBYTES" code // Therefore, push the hash of EOFBYTES to the stack runState.stack.push(bytesToBigInt(EOFHASH)) return } else if (common.isActivatedEIP(7702)) { - code = await eip7702CodeCheck(runState, code) - runState.stack.push(bytesToBigInt(keccak256(code))) - return + 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))) + } else { + runState.stack.push(bytesToBigInt(keccak256(code))) + return + } } const account = await runState.stateManager.getAccount(address) From d4f9c604e89f73956cd3509ad5d9d2c174d0d3c1 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 20 Aug 2024 16:45:11 +0200 Subject: [PATCH 18/31] vm: 7702 ensure delegated accounts are not deleted [no ci] --- packages/vm/src/runTx.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 8748748b38..05c0c2f5e1 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -12,7 +12,6 @@ import { bytesToHex, bytesToUnprefixedHex, concatBytes, - createAddressFromString, ecrecover, equalsBytes, hexToBytes, @@ -426,7 +425,6 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { let gasRefund = BIGINT_0 - const writtenAddresses = new Set() if (tx.supports(Capability.EIP7702EOACode)) { // Add contract code for authority tuples provided by EIP 7702 tx const authorizationList = (tx).authorizationList @@ -489,8 +487,6 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { const addressCode = concatBytes(new Uint8Array([0xef, 0x01, 0x00]), address) await vm.stateManager.putCode(authority, addressCode) - - writtenAddresses.add(authority.toString()) } } @@ -673,15 +669,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) From a41ef16d259d553369417e2ded04c7955ff97e06 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Fri, 23 Aug 2024 16:26:27 +0200 Subject: [PATCH 19/31] evm: 7702 correctly check for gas on delegated code --- packages/evm/src/opcodes/gas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index b2a7841165..c3efc512a8 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -40,7 +40,7 @@ const EXTCALL_TARGET_MAX = BigInt(2) ** BigInt(8 * 20) - BigInt(1) async function eip7702GasCost(runState: RunState, common: Common, address: Address) { const code = await runState.stateManager.getCode(address) - if (equalsBytes(code, new Uint8Array([0xef, 0x01, 0x00]))) { + if (equalsBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { return accessAddressEIP2929(runState, code.slice(3, 24), common) } return BIGINT_0 From 30072e25dacf7d91c1a15803f13bcaa6ff7d438c Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Fri, 23 Aug 2024 16:28:33 +0200 Subject: [PATCH 20/31] evm: add verkle gas logic for 7702 --- packages/evm/src/opcodes/gas.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index c3efc512a8..c7b6fbbeea 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -38,10 +38,15 @@ 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) { +async function eip7702GasCost( + runState: RunState, + common: Common, + address: Address, + charge2929Gas: boolean, +) { const code = await runState.stateManager.getCode(address) if (equalsBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { - return accessAddressEIP2929(runState, code.slice(3, 24), common) + return accessAddressEIP2929(runState, code.slice(3, 24), common, charge2929Gas) } return BIGINT_0 } @@ -193,7 +198,7 @@ export const dynamicGasHandlers: Map Date: Sun, 25 Aug 2024 14:37:25 +0200 Subject: [PATCH 21/31] vm/tx: fix 7702 tests --- packages/vm/src/runTx.ts | 4 ++++ packages/vm/test/api/runBlock.spec.ts | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 05c0c2f5e1..0283b2a39d 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -429,13 +429,16 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { // Add contract code for authority tuples provided by EIP 7702 tx const authorizationList = (tx).authorizationList const MAGIC = new Uint8Array([5]) + console.log('CHECK7702') for (let i = 0; i < authorizationList.length; i++) { + console.log('check auth') // Authority tuple validation const data = authorizationList[i] const chainId = data[0] const chainIdBN = bytesToBigInt(chainId) if (chainIdBN !== BIGINT_0 && chainIdBN !== vm.common.chainId()) { // Chain id does not match, continue + console.log('chainid') continue } // Address to take code from @@ -460,6 +463,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { const code = await vm.stateManager.getCode(authority) if (!equalsBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { // Account is a "normal" contract + console.log('contract') continue } } diff --git a/packages/vm/test/api/runBlock.spec.ts b/packages/vm/test/api/runBlock.spec.ts index 2ec22c5ef8..629a433c49 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, @@ -616,7 +615,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({ @@ -641,6 +642,7 @@ describe('runBlock() -> tx types', async () => { const authorizationListOpts2 = [ { address: code2Addr, + nonce: 1, }, ] From 5b4313ad626d7990825ac8583f09f36d83dd2e70 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sun, 25 Aug 2024 14:58:23 +0200 Subject: [PATCH 22/31] tx: throw if 7702-tx has no `to` field --- packages/tx/src/7702/tx.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/tx/src/7702/tx.ts b/packages/tx/src/7702/tx.ts index c22449fa0e..8c7239c861 100644 --- a/packages/tx/src/7702/tx.ts +++ b/packages/tx/src/7702/tx.ts @@ -117,6 +117,13 @@ export class EOACode7702Transaction extends BaseTransaction Date: Sun, 25 Aug 2024 15:09:00 +0200 Subject: [PATCH 23/31] vm/tx: fix 7702 tests --- packages/tx/test/eip7702.spec.ts | 3 +- packages/vm/src/runTx.ts | 4 -- packages/vm/test/api/EIPs/eip-7702.spec.ts | 62 +++------------------- 3 files changed, 8 insertions(+), 61 deletions(-) diff --git a/packages/tx/test/eip7702.spec.ts b/packages/tx/test/eip7702.spec.ts index 8136e58d1d..48ee891292 100644 --- a/packages/tx/test/eip7702.spec.ts +++ b/packages/tx/test/eip7702.spec.ts @@ -41,6 +41,7 @@ function getTxData(override: Partial = {}): TxData { ...override, }, ], + to: createZeroAddress(), } } @@ -52,7 +53,7 @@ describe('[EOACode7702Transaction]', () => { maxFeePerGas: 1, maxPriorityFeePerGas: 1, accessList: [], - authorizationList: [], + ...getTxData(), chainId: 1, gasLimit: 100000, to: createZeroAddress(), diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 0283b2a39d..05c0c2f5e1 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -429,16 +429,13 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { // Add contract code for authority tuples provided by EIP 7702 tx const authorizationList = (tx).authorizationList const MAGIC = new Uint8Array([5]) - console.log('CHECK7702') for (let i = 0; i < authorizationList.length; i++) { - console.log('check auth') // Authority tuple validation const data = authorizationList[i] const chainId = data[0] const chainIdBN = bytesToBigInt(chainId) if (chainIdBN !== BIGINT_0 && chainIdBN !== vm.common.chainId()) { // Chain id does not match, continue - console.log('chainid') continue } // Address to take code from @@ -463,7 +460,6 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { const code = await vm.stateManager.getCode(authority) if (!equalsBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { // Account is a "normal" contract - console.log('contract') continue } } diff --git a/packages/vm/test/api/EIPs/eip-7702.spec.ts b/packages/vm/test/api/EIPs/eip-7702.spec.ts index 4b922d9044..0cdf2183b6 100644 --- a/packages/vm/test/api/EIPs/eip-7702.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7702.spec.ts @@ -5,7 +5,6 @@ import { Account, Address, BIGINT_1, - KECCAK256_NULL, bigIntToUnpaddedBytes, concatBytes, createAddressFromString, @@ -71,12 +70,7 @@ function getAuthorizationListItem(opts: GetAuthListOpts): AuthorizationListBytes ] } -async function runTest( - authorizationListOpts: GetAuthListOpts[], - expect: Uint8Array, - vm?: VM, - skipEmptyCode?: boolean, -) { +async function runTest(authorizationListOpts: GetAuthListOpts[], expect: Uint8Array, vm?: VM) { vm = vm ?? (await VM.create({ common })) const authList = authorizationListOpts.map((opt) => getAuthorizationListItem(opt)) const tx = createEOACode7702Tx( @@ -105,12 +99,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', () => { @@ -195,7 +183,6 @@ describe('EIP 7702: set code to EOA accounts', () => { ], new Uint8Array(), vm, - true, ) }) @@ -212,8 +199,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`, ) @@ -240,49 +228,11 @@ 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)) - }) - - // 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 VM.create({ common }) - const authList = [ - getAuthorizationListItem({ - address: code1Addr, - }), - ] - const tx = 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 - }, - { common }, - ).sign(defaultSenderPkey) - - // Store value 1 in storage slot 1 - // PUSH1 PUSH1 SSTORE STOP - const code = hexToBytes('0x600160015500') - await vm.stateManager.putCode(code1Addr, code) - - const acc = (await vm.stateManager.getAccount(defaultSenderAddr)) ?? new Account() - acc.balance = BigInt(1_000_000_000) - await vm.stateManager.putAccount(defaultSenderAddr, acc) - - await runTx(vm, { tx }) - - // Note: due to EIP-161, defaultAuthAddr is now deleted - const account = await vm.stateManager.getAccount(defaultAuthAddr) - assert.ok(account === undefined) + assert.ok(res.execResult.executionGasUsed === BigInt(2715)) }) }) -describe.only('test EIP-7702 opcodes', () => { +describe('test EIP-7702 opcodes', () => { it('should correctly report EXTCODESIZE/EXTCODEHASH/EXTCODECOPY opcodes', async () => { // extcodesize and extcodehash const deploymentAddress = createZeroAddress() From 2f0d1abd5e288591972d1df7d72524d43ba482ae Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 26 Aug 2024 16:01:12 +0200 Subject: [PATCH 24/31] VM: exit early on non-existing system contracts --- packages/vm/src/requests.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/vm/src/requests.ts b/packages/vm/src/requests.ts index e035b9db8d..5b6558d2c3 100644 --- a/packages/vm/src/requests.ts +++ b/packages/vm/src/requests.ts @@ -79,6 +79,14 @@ const accumulateEIP7002Requests = async ( const originalAccount = await vm.stateManager.getAccount(systemAddress) + if (originalAccount === undefined) { + await vm.stateManager.deleteAccount(systemAddress) + return + } else { + // Restore the original account (the `runCall` updates the nonce) + await vm.stateManager.putAccount(systemAddress, originalAccount) + } + const results = await vm.evm.runCall({ caller: systemAddress, gasLimit: BigInt(1_000_000), @@ -96,13 +104,6 @@ const accumulateEIP7002Requests = async ( requests.push(WithdrawalRequest.fromRequestData({ sourceAddress, validatorPubkey, amount })) } } - - if (originalAccount === undefined) { - await vm.stateManager.deleteAccount(systemAddress) - } else { - // Restore the original account (the `runCall` updates the nonce) - await vm.stateManager.putAccount(systemAddress, originalAccount) - } } const accumulateEIP7251Requests = async ( @@ -129,6 +130,14 @@ const accumulateEIP7251Requests = async ( const originalAccount = await vm.stateManager.getAccount(systemAddress) + if (originalAccount === undefined) { + await vm.stateManager.deleteAccount(systemAddress) + return + } else { + // Restore the original account (the `runCall` updates the nonce) + await vm.stateManager.putAccount(systemAddress, originalAccount) + } + const results = await vm.evm.runCall({ caller: systemAddress, gasLimit: BigInt(1_000_000), @@ -148,13 +157,6 @@ const accumulateEIP7251Requests = async ( ) } } - - if (originalAccount === undefined) { - await vm.stateManager.deleteAccount(systemAddress) - } else { - // Restore the original account (the `runCall` updates the nonce) - await vm.stateManager.putAccount(systemAddress, originalAccount) - } } const accumulateDeposits = async ( From ec85a45ccf99d5c0bb8951f89f2b9805de4c0903 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 27 Aug 2024 18:53:09 +0200 Subject: [PATCH 25/31] 7702: add delegated account to warm address --- packages/evm/src/evm.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index b7482febdf..d73d12c6a1 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -1020,6 +1020,9 @@ export class EVM implements EVMInterface { ) { 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 From 8b0cc5c1648ee81f49d4845b9c6b893d2c995964 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 28 Aug 2024 13:01:59 +0200 Subject: [PATCH 26/31] vm: requests do restore system account --- packages/vm/src/requests.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/vm/src/requests.ts b/packages/vm/src/requests.ts index 5b6558d2c3..934cb316dd 100644 --- a/packages/vm/src/requests.ts +++ b/packages/vm/src/requests.ts @@ -76,6 +76,7 @@ const accumulateEIP7002Requests = async ( const systemAddressBytes = bigIntToAddressBytes(vm.common.param('systemAddress')) const systemAddress = createAddressFromString(bytesToHex(systemAddressBytes)) + const systemAccount = await vm.stateManager.getAccount(systemAddress) const originalAccount = await vm.stateManager.getAccount(systemAddress) @@ -93,6 +94,12 @@ const accumulateEIP7002Requests = async ( to: withdrawalsAddress, }) + if (systemAccount === undefined) { + await vm.stateManager.deleteAccount(systemAddress) + } else { + await vm.stateManager.putAccount(systemAddress, systemAccount) + } + const resultsBytes = results.execResult.returnValue if (resultsBytes.length > 0) { // Each request is 76 bytes @@ -127,6 +134,7 @@ const accumulateEIP7251Requests = async ( const systemAddressBytes = bigIntToAddressBytes(vm.common.param('systemAddress')) const systemAddress = createAddressFromString(bytesToHex(systemAddressBytes)) + const systemAccount = await vm.stateManager.getAccount(systemAddress) const originalAccount = await vm.stateManager.getAccount(systemAddress) @@ -144,6 +152,12 @@ const accumulateEIP7251Requests = async ( to: consolidationsAddress, }) + if (systemAccount === undefined) { + await vm.stateManager.deleteAccount(systemAddress) + } else { + await vm.stateManager.putAccount(systemAddress, systemAccount) + } + const resultsBytes = results.execResult.returnValue if (resultsBytes.length > 0) { // Each request is 116 bytes From e710e78d5194199d96005b7984aff811fd763c4a Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 28 Aug 2024 19:34:39 +0200 Subject: [PATCH 27/31] 7702: continue processing once auth ecrecover is invalid --- packages/vm/src/runTx.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 05c0c2f5e1..dc90d3b9e8 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -447,7 +447,13 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { 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 accountMaybeUndefined = await vm.stateManager.getAccount(authority) From 8cc575524e4e9b800260f18003044dd0f40ad6d0 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 2 Sep 2024 16:40:18 +0200 Subject: [PATCH 28/31] evm/vm: add 7702 delegation constant --- packages/evm/src/evm.ts | 31 ++++++++++++++------------- packages/evm/src/opcodes/functions.ts | 3 ++- packages/evm/src/opcodes/gas.ts | 3 ++- packages/evm/src/types.ts | 3 +++ packages/vm/src/runTx.ts | 9 +++++--- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index d73d12c6a1..4841528c34 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,7 +1017,7 @@ export class EVM implements EVMInterface { // EIP-7702 delegation check if ( this.common.isActivatedEIP(7702) && - equalsBytes(message.code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00])) + 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) diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index 4b20d46c29..29f9fb8e45 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -33,6 +33,7 @@ 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, @@ -61,7 +62,7 @@ export interface AsyncOpHandler { export type OpHandler = SyncOpHandler | AsyncOpHandler function getEIP7702DelegatedAddress(code: Uint8Array) { - if (equalBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { + if (equalBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) { return new Address(code.slice(3, 24)) } } diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index c7b6fbbeea..9d3aa2570e 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -18,6 +18,7 @@ import { 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' @@ -45,7 +46,7 @@ async function eip7702GasCost( charge2929Gas: boolean, ) { const code = await runState.stateManager.getCode(address) - if (equalsBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { + if (equalsBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) { return accessAddressEIP2929(runState, code.slice(3, 24), common, charge2929Gas) } return BIGINT_0 diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index eb4a690002..b4281c52a8 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -507,3 +507,6 @@ export type EOFEnv = { returnStack: number[] } } + +// EIP-7702 flag: if contract code starts with these 3 bytes, it is a 7702-delegated EOA +export const DELEGATION_7702_FLAG = new Uint8Array([0xef, 0x01, 0x00]) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index dc90d3b9e8..8fd6ed5a23 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -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 */ @@ -290,7 +293,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { if (vm.common.isActivatedEIP(3607) && !equalsBytes(fromAccount.codeHash, KECCAK256_NULL)) { if (vm.common.isActivatedEIP(7702)) { const code = await state.getCode(caller) - if (!equalsBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { + if (!equalsBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) { const msg = _errorMsg( 'invalid sender address, address is not EOA (EIP-3607)', vm, @@ -464,7 +467,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { vm.evm.journal.addAlwaysWarmAddress(authority.toString()) if (account.isContract()) { const code = await vm.stateManager.getCode(authority) - if (!equalsBytes(code.slice(0, 3), new Uint8Array([0xef, 0x01, 0x00]))) { + if (!equalsBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) { // Account is a "normal" contract continue } @@ -491,7 +494,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { account.nonce++ await vm.evm.journal.putAccount(authority, account) - const addressCode = concatBytes(new Uint8Array([0xef, 0x01, 0x00]), address) + const addressCode = concatBytes(DELEGATION_7702_FLAG, address) await vm.stateManager.putCode(authority, addressCode) } } From 6325bc3393f1dd9be8765304b9b1a3acfc27bd00 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 2 Sep 2024 17:03:53 +0200 Subject: [PATCH 29/31] vm: fix requests --- packages/vm/src/requests.ts | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/packages/vm/src/requests.ts b/packages/vm/src/requests.ts index da9135f9d0..54230e18bc 100644 --- a/packages/vm/src/requests.ts +++ b/packages/vm/src/requests.ts @@ -66,26 +66,14 @@ const accumulateEIP7002Requests = async ( ) const withdrawalsAddress = createAddressFromString(bytesToHex(addressBytes)) - const code = await vm.stateManager.getCode(withdrawalsAddress) - - if (code.length === 0) { - /*throw new Error( - 'Attempt to accumulate EIP-7002 requests failed: the contract does not exist. Ensure the deployment tx has been run, or that the required contract code is stored', - )*/ - } - const systemAddressBytes = bigIntToAddressBytes(vm.common.param('systemAddress')) const systemAddress = createAddressFromString(bytesToHex(systemAddressBytes)) const systemAccount = await vm.stateManager.getAccount(systemAddress) - const originalAccount = await vm.stateManager.getAccount(systemAddress) + const originalAccount = await vm.stateManager.getAccount(withdrawalsAddress) if (originalAccount === undefined) { - await vm.stateManager.deleteAccount(systemAddress) return - } else { - // Restore the original account (the `runCall` updates the nonce) - await vm.stateManager.putAccount(systemAddress, originalAccount) } const results = await vm.evm.runCall({ @@ -124,26 +112,14 @@ const accumulateEIP7251Requests = async ( ) const consolidationsAddress = createAddressFromString(bytesToHex(addressBytes)) - const code = await vm.stateManager.getCode(consolidationsAddress) - - if (code.length === 0) { - /*throw new Error( - 'Attempt to accumulate EIP-7251 requests failed: the contract does not exist. Ensure the deployment tx has been run, or that the required contract code is stored', - )*/ - } - const systemAddressBytes = bigIntToAddressBytes(vm.common.param('systemAddress')) const systemAddress = createAddressFromString(bytesToHex(systemAddressBytes)) const systemAccount = await vm.stateManager.getAccount(systemAddress) - const originalAccount = await vm.stateManager.getAccount(systemAddress) + const originalAccount = await vm.stateManager.getAccount(consolidationsAddress) if (originalAccount === undefined) { - await vm.stateManager.deleteAccount(systemAddress) return - } else { - // Restore the original account (the `runCall` updates the nonce) - await vm.stateManager.putAccount(systemAddress, originalAccount) } const results = await vm.evm.runCall({ From 4287e95b91da6220ecb17124bb2b9c7d10fdf127 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 2 Sep 2024 20:09:45 +0200 Subject: [PATCH 30/31] vm: unduplify 3607 error msg --- packages/vm/src/runTx.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 8fd6ed5a23..1036c6e30d 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -291,9 +291,15 @@ 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)) { - if (vm.common.isActivatedEIP(7702)) { - const code = await state.getCode(caller) - if (!equalsBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) { + 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, @@ -302,9 +308,6 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { ) throw new Error(msg) } - } else { - const msg = _errorMsg('invalid sender address, address is not EOA (EIP-3607)', vm, block, tx) - throw new Error(msg) } } From 88dfdd0b69c75759cb834ed5d64fbf7d6e564e23 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:30:29 -0400 Subject: [PATCH 31/31] fix example --- packages/tx/examples/EOACodeTx.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tx/examples/EOACodeTx.ts b/packages/tx/examples/EOACodeTx.ts index 482600669b..d23700ae5c 100644 --- a/packages/tx/examples/EOACodeTx.ts +++ b/packages/tx/examples/EOACodeTx.ts @@ -1,7 +1,6 @@ import { Common, Hardfork, Mainnet } from '@ethereumjs/common' import { createEOACode7702Tx } from '@ethereumjs/tx' - -import type { PrefixedHexString } from '@ethereumjs/util' +import { type PrefixedHexString, createAddressFromPrivateKey, randomBytes } from '@ethereumjs/util' const ones32 = `0x${'01'.repeat(32)}` as PrefixedHexString @@ -12,12 +11,13 @@ const tx = createEOACode7702Tx( { chainId: '0x2', address: `0x${'20'.repeat(20)}`, - nonce: ['0x1'], + nonce: '0x1', yParity: '0x1', r: ones32, s: ones32, }, ], + to: createAddressFromPrivateKey(randomBytes(32)), }, { common }, )