Skip to content

Commit

Permalink
EIP-7702 devnet-3 readiness (#3581)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
jochem-brouwer and acolytec3 committed Sep 11, 2024
1 parent 22766f2 commit 674a8a3
Show file tree
Hide file tree
Showing 15 changed files with 356 additions and 137 deletions.
42 changes: 28 additions & 14 deletions packages/evm/src/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,28 @@ 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'
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')
Expand Down Expand Up @@ -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
}
Expand Down
51 changes: 43 additions & 8 deletions packages/evm/src/opcodes/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<number, OpHandler> = new Map([
// 0x00: STOP
Expand Down Expand Up @@ -511,36 +528,39 @@ export const handlers: Map<number, OpHandler> = 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)
},
],
// 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)
Expand All @@ -553,7 +573,7 @@ export const handlers: Map<number, OpHandler> = new Map([
// 0x3f: EXTCODEHASH
[
0x3f,
async function (runState) {
async function (runState, common) {
const addressBigInt = runState.stack.pop()
const address = createAddressFromStackBigInt(addressBigInt)

Expand All @@ -564,6 +584,21 @@ export const handlers: Map<number, OpHandler> = 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)
Expand Down
49 changes: 49 additions & 0 deletions packages/evm/src/opcodes/gas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -175,6 +191,10 @@ export const dynamicGasHandlers: Map<number, AsyncDynamicGasHandler | SyncDynami
gas += accessAddressEIP2929(runState, address.bytes, common, charge2929Gas)
}

if (common.isActivatedEIP(7702)) {
gas += await eip7702GasCost(runState, common, address, charge2929Gas)
}

return gas
},
],
Expand Down Expand Up @@ -208,6 +228,10 @@ export const dynamicGasHandlers: Map<number, AsyncDynamicGasHandler | SyncDynami
gas += accessAddressEIP2929(runState, address.bytes, common, charge2929Gas)
}

if (common.isActivatedEIP(7702)) {
gas += await eip7702GasCost(runState, common, address, charge2929Gas)
}

if (dataLength !== BIGINT_0) {
gas += common.param('copyGas') * divCeil(dataLength, BIGINT_32)

Expand Down Expand Up @@ -273,6 +297,10 @@ export const dynamicGasHandlers: Map<number, AsyncDynamicGasHandler | SyncDynami
gas += accessAddressEIP2929(runState, address.bytes, common, charge2929Gas)
}

if (common.isActivatedEIP(7702)) {
gas += await eip7702GasCost(runState, common, address, charge2929Gas)
}

return gas
},
],
Expand Down Expand Up @@ -573,6 +601,10 @@ export const dynamicGasHandlers: Map<number, AsyncDynamicGasHandler | SyncDynami
gas += accessAddressEIP2929(runState, toAddress.bytes, common, charge2929Gas)
}

if (common.isActivatedEIP(7702)) {
gas += await eip7702GasCost(runState, common, toAddress, charge2929Gas)
}

if (value !== BIGINT_0 && !common.isActivatedEIP(6800)) {
gas += common.param('callValueTransferGas')
}
Expand Down Expand Up @@ -647,6 +679,10 @@ export const dynamicGasHandlers: Map<number, AsyncDynamicGasHandler | SyncDynami
)
}

if (common.isActivatedEIP(7702)) {
gas += await eip7702GasCost(runState, common, toAddress, charge2929Gas)
}

if (value !== BIGINT_0) {
gas += common.param('callValueTransferGas')
}
Expand Down Expand Up @@ -708,6 +744,10 @@ export const dynamicGasHandlers: Map<number, AsyncDynamicGasHandler | SyncDynami
)
}

if (common.isActivatedEIP(7702)) {
gas += await eip7702GasCost(runState, common, toAddress, charge2929Gas)
}

const gasLimit = maxCallGas(
currentGasLimit,
runState.interpreter.getGasLeft() - gas,
Expand Down Expand Up @@ -907,6 +947,15 @@ export const dynamicGasHandlers: Map<number, AsyncDynamicGasHandler | SyncDynami
)
}

if (common.isActivatedEIP(7702)) {
gas += await eip7702GasCost(
runState,
common,
createAddressFromStackBigInt(toAddr),
charge2929Gas,
)
}

const gasLimit = maxCallGas(
currentGasLimit,
runState.interpreter.getGasLeft() - gas,
Expand Down
8 changes: 0 additions & 8 deletions packages/evm/src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,12 +409,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
},
}
3 changes: 3 additions & 0 deletions packages/evm/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])
6 changes: 3 additions & 3 deletions packages/tx/examples/EOACodeTx.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 },
)
Expand Down
7 changes: 7 additions & 0 deletions packages/tx/src/7702/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ export class EOACode7702Transaction extends BaseTransaction<TransactionType.EOAC
EIP2718.validateYParity(this)
Legacy.validateHighS(this)

if (this.to === undefined) {
const msg = this._errorMsg(
`tx should have a "to" field and cannot be used to create contracts`,
)
throw new Error(msg)
}

const freeze = opts?.freeze ?? true
if (freeze) {
Object.freeze(this)
Expand Down
2 changes: 1 addition & 1 deletion packages/tx/src/capabilities/eip7702.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
9 changes: 9 additions & 0 deletions packages/tx/src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
}
4 changes: 2 additions & 2 deletions packages/tx/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -610,7 +610,7 @@ export type AuthorizationListItem = {
export type AuthorizationListBytesItem = [
Uint8Array,
Uint8Array,
Uint8Array[],
Uint8Array,
Uint8Array,
Uint8Array,
Uint8Array,
Expand Down
Loading

0 comments on commit 674a8a3

Please sign in to comment.