diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 04401ccbdd..bb0072c46f 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -133,14 +133,6 @@ export const eipsDict: EIPsDict = { 2935: { minimumHardfork: Hardfork.Chainstart, }, - /** - * Description : AUTH and AUTHCALL opcodes - * URL : https://github.com/ethereum/EIPs/commit/eca4416ff3c025fcb6ec8cd4eac481e74e108481 - * Status : Review - */ - 3074: { - minimumHardfork: Hardfork.London, - }, /** * Description : BASEFEE opcode * URL : https://eips.ethereum.org/EIPS/eip-3198 diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index c39855664f..936a287512 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -177,12 +177,11 @@ export class EVM implements EVMInterface { // Supported EIPs const supportedEIPs = [ - 663, 1153, 1153, 1559, 1559, 2537, 2537, 2565, 2565, 2718, 2718, 2929, 2929, 2930, 2930, 2935, - 2935, 3074, 3074, 3198, 3198, 3529, 3529, 3540, 3540, 3541, 3541, 3607, 3607, 3651, 3651, - 3670, 3670, 3855, 3855, 3860, 3860, 4200, 4399, 4399, 4750, 4788, 4788, 4844, 4844, 4895, - 4895, 5133, 5133, 5450, 5656, 5656, 6110, 6110, 6206, 6780, 6780, 6800, 6800, 7002, 7002, - 7069, 7251, 7251, 7480, 7516, 7516, 7620, 7685, 7685, 7692, 7698, 7702, 7702, 7709, 7709, + 663, 1153, 1559, 2537, 2565, 2718, 2929, 2930, 2935, 3198, 3529, 3540, 3541, 3607, 3651, 3670, + 3855, 3860, 4200, 4399, 4750, 4788, 4844, 4895, 5133, 5450, 5656, 6110, 6206, 6780, 6800, + 7002, 7069, 7251, 7480, 7516, 7620, 7685, 7692, 7698, 7702, 7709, ] + for (const eip of this.common.eips()) { if (!supportedEIPs.includes(eip)) { throw new Error(`EIP-${eip} is not supported by the EVM`) diff --git a/packages/evm/src/interpreter.ts b/packages/evm/src/interpreter.ts index ea3332427c..2ea57847d2 100644 --- a/packages/evm/src/interpreter.ts +++ b/packages/evm/src/interpreter.ts @@ -93,7 +93,6 @@ export interface RunState { interpreter: Interpreter gasRefund: bigint // Tracks the current refund gasLeft: bigint // Current gas left - auth?: Address /** EIP-3074 AUTH parameter */ returnBytes: Uint8Array /* Current bytes in the return Uint8Array. Cleared each time a CALL/CREATE is made in the current frame. */ } @@ -867,31 +866,6 @@ export class Interpreter { return this._baseCall(msg) } - /** - * Sends a message with arbitrary data to a given address path. - */ - async authcall( - gasLimit: bigint, - address: Address, - value: bigint, - data: Uint8Array, - ): Promise { - const msg = new Message({ - caller: this._runState.auth, - gasLimit, - to: address, - value, - data, - isStatic: this._env.isStatic, - depth: this._env.depth + 1, - authcallOrigin: this._env.address, - blobVersionedHashes: this._env.blobVersionedHashes, - accessWitness: this._env.accessWitness, - }) - - return this._baseCall(msg) - } - /** * Message-call into this account with an alternative account's code. */ diff --git a/packages/evm/src/opcodes/codes.ts b/packages/evm/src/opcodes/codes.ts index d7f51241b0..f782c54560 100644 --- a/packages/evm/src/opcodes/codes.ts +++ b/packages/evm/src/opcodes/codes.ts @@ -294,13 +294,6 @@ const eipOpcodes: { eip: number; opcodes: OpcodeEntry }[] = [ 0x5f: { name: 'PUSH0', isAsync: false, dynamicGas: false }, }, }, - { - eip: 3074, - opcodes: { - 0xf6: { name: 'AUTH', isAsync: true, dynamicGas: true }, - 0xf7: { name: 'AUTHCALL', isAsync: true, dynamicGas: true }, - }, - }, { eip: 4200, opcodes: { diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index d20b18faa0..65cda002e1 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -1,5 +1,4 @@ import { - Account, Address, BIGINT_0, BIGINT_1, @@ -8,7 +7,6 @@ import { BIGINT_224, BIGINT_255, BIGINT_256, - BIGINT_27, BIGINT_2EXP160, BIGINT_2EXP224, BIGINT_2EXP96, @@ -18,7 +16,6 @@ import { BIGINT_8, BIGINT_96, MAX_INTEGER_BIGINT, - SECP256K1_ORDER_DIV_2, TWO_POW256, bigIntToAddressBytes, bigIntToBytes, @@ -26,12 +23,8 @@ import { bytesToHex, bytesToInt, concatBytes, - ecrecover, getVerkleTreeIndexesForStorageSlot, - hexToBytes, - publicToAddress, setLengthLeft, - setLengthRight, } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' @@ -56,8 +49,6 @@ import { import type { RunState } from '../interpreter.js' import type { Common } from '@ethereumjs/common' -const EIP3074MAGIC = hexToBytes('0x04') - export interface SyncOpHandler { (runState: RunState, common: Common): void } @@ -1492,119 +1483,6 @@ export const handlers: Map = new Map([ runState.stack.push(ret) }, ], - // 0xf6: AUTH - [ - 0xf6, - async function (runState) { - // eslint-disable-next-line prefer-const - let [authority, memOffset, memLength] = runState.stack.popN(3) - - if (memLength > BigInt(97)) { - memLength = BigInt(97) - } - - let mem = runState.memory.read(Number(memOffset), Number(memLength)) - if (mem.length < 97) { - mem = setLengthRight(mem, 97) - } - - const yParity = BigInt(mem[0]) - const r = mem.subarray(1, 33) - const s = mem.subarray(33, 65) - const commit = mem.subarray(65, 97) - - if (bytesToBigInt(s) > SECP256K1_ORDER_DIV_2) { - runState.stack.push(BIGINT_0) - runState.auth = undefined - return - } - if (yParity > BIGINT_1) { - runState.stack.push(BIGINT_0) - runState.auth = undefined - return - } - - // we don't want strick check here on authority being in address range just last 20 bytes - const expectedAddress = new Address(bigIntToAddressBytes(authority, false)) - const account = (await runState.stateManager.getAccount(expectedAddress)) ?? new Account() - - if (account.isContract()) { - // EXTCODESIZE > 0 - runState.stack.push(BIGINT_0) - runState.auth = undefined - return - } - - const accountNonce = account.nonce - - const invokedAddress = setLengthLeft(runState.interpreter._env.address.bytes, 32) - const chainId = setLengthLeft(bigIntToBytes(runState.interpreter.getChainId()), 32) - const nonce = setLengthLeft(bigIntToBytes(accountNonce), 32) - const message = concatBytes(EIP3074MAGIC, chainId, nonce, invokedAddress, commit) - - const keccakFunction = runState.interpreter._evm.common.customCrypto.keccak256 ?? keccak256 - const msgHash = keccakFunction(message) - - let recover - const ecrecoverFunction = runState.interpreter._evm.common.customCrypto.ecrecover ?? ecrecover - try { - recover = ecrecoverFunction(msgHash, yParity + BIGINT_27, r, s) - } catch (e) { - // Malformed signature, push 0 on stack, clear auth variable - runState.stack.push(BIGINT_0) - runState.auth = undefined - return - } - - const addressBuffer = publicToAddress(recover) - const address = new Address(addressBuffer) - runState.auth = address - - if (!expectedAddress.equals(address)) { - // expected address does not equal the recovered address, clear auth variable - runState.stack.push(BIGINT_0) - runState.auth = undefined - return - } - - runState.auth = address - runState.stack.push(BIGINT_1) - }, - ], - // 0xf7: AUTHCALL (3074) / RETURNDATALOAD (7069) - [ - 0xf7, - async function (runState, common) { - if (common.isActivatedEIP(3074) && runState.env.eof === undefined) { - // AUTHCALL logic - const [_currentGasLimit, addr, value, argsOffset, argsLength, retOffset, retLength] = - runState.stack.popN(7) - - const toAddress = createAddressFromStackBigInt(addr) - - const gasLimit = runState.messageGasLimit! - runState.messageGasLimit = undefined - - let data = new Uint8Array(0) - if (argsLength !== BIGINT_0) { - data = runState.memory.read(Number(argsOffset), Number(argsLength)) - } - - const ret = await runState.interpreter.authcall(gasLimit, toAddress, value, data) - // Write return data to memory - writeCallOutput(runState, retOffset, retLength) - runState.stack.push(ret) - } else if (common.isActivatedEIP(7069)) { - // RETURNDATALOAD logic - const returnDataOffset = runState.stack.pop() - const data = getDataSlice(runState.interpreter.getReturnData(), returnDataOffset, BIGINT_32) - runState.stack.push(bytesToBigInt(data)) - } else { - // This should be unreachable - trap(ERROR.INVALID_OPCODE) - } - }, - ], // 0xf8: EXTCALL [ 0xf8, diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index 6f8cd64252..05c576f47d 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -1,6 +1,5 @@ import { Hardfork } from '@ethereumjs/common' import { - Account, BIGINT_0, BIGINT_1, BIGINT_3, @@ -769,78 +768,6 @@ export const dynamicGasHandlers: Map { - const [address, memOffset, memLength] = runState.stack.peek(3) - // Note: 2929 is always active if AUTH can be reached, - // since it needs London as minimum hardfork - gas += accessAddressEIP2929(runState, bigIntToBytes(address), common) - gas += subMemUsage(runState, memOffset, memLength, common) - return gas - }, - ], - [ - /* AUTHCALL */ - 0xf7, - async function (runState, gas, common): Promise { - if (runState.auth === undefined) { - trap(ERROR.AUTHCALL_UNSET) - } - - const [currentGasLimit, addr, value, argsOffset, argsLength, retOffset, retLength] = - runState.stack.peek(7) - - const toAddress = createAddressFromStackBigInt(addr) - - gas += common.param('warmstoragereadGas') - - gas += accessAddressEIP2929(runState, toAddress.bytes, common, true, true) - - gas += subMemUsage(runState, argsOffset, argsLength, common) - gas += subMemUsage(runState, retOffset, retLength, common) - - if (value > BIGINT_0) { - gas += common.param('authcallValueTransferGas') - const account = await runState.stateManager.getAccount(toAddress) - if (!account) { - gas += common.param('callNewAccountGas') - } - } - - let gasLimit = maxCallGas( - runState.interpreter.getGasLeft() - gas, - runState.interpreter.getGasLeft() - gas, - runState, - common, - ) - if (currentGasLimit !== BIGINT_0) { - if (currentGasLimit > gasLimit) { - trap(ERROR.OUT_OF_GAS) - } - gasLimit = currentGasLimit - } - - runState.messageGasLimit = gasLimit - - if (value > BIGINT_0) { - const account = (await runState.stateManager.getAccount(runState.auth!)) ?? new Account() - if (account.balance < value) { - trap(ERROR.OUT_OF_GAS) - } - account.balance -= value - - const toAddr = createAddressFromStackBigInt(addr) - const target = (await runState.stateManager.getAccount(toAddr)) ?? new Account() - target.balance += value - - await runState.stateManager.putAccount(toAddr, target) - } - - return gas - }, - ], /* EXTCALL */ [ 0xf8, diff --git a/packages/evm/src/params.ts b/packages/evm/src/params.ts index f4483d1201..22507da33b 100644 --- a/packages/evm/src/params.ts +++ b/packages/evm/src/params.ts @@ -278,15 +278,6 @@ export const paramsEVM: ParamsDict = { historyServeWindow: 8192, // The amount of blocks to be served by the historical blockhash contract }, /** -. * AUTH and AUTHCALL opcodes -. */ - 3074: { - // gasPrices - authGas: 3100, // Gas cost of the AUTH opcode - authcallGas: 0, // Gas cost of the AUTHCALL opcode - authcallValueTransferGas: 6700, // Paid for CALL when the value transfer is non-zero - }, - /** . * BASEFEE opcode . */ 3198: { diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index 752836afc0..f385929e55 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -191,7 +191,6 @@ export interface EVMOpts { * - [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) - Save historical block hashes in state (`experimental`) * - [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929) - gas cost increases for state access opcodes * - [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) - Optional access list tx type - * - [EIP-3074](https://eips.ethereum.org/EIPS/eip-3074) - AUTH and AUTHCALL opcodes * - [EIP-3198](https://eips.ethereum.org/EIPS/eip-3198) - Base fee Opcode * - [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529) - Reduction in refunds * - [EIP-3540](https://eips.ethereum.org/EIPS/eip-3541) - EVM Object Format (EOF) v1 (`outdated`) diff --git a/packages/vm/test/api/EIPs/eip-3074-authcall.spec.ts b/packages/vm/test/api/EIPs/eip-3074-authcall.spec.ts deleted file mode 100644 index 2834a16a55..0000000000 --- a/packages/vm/test/api/EIPs/eip-3074-authcall.spec.ts +++ /dev/null @@ -1,861 +0,0 @@ -import { createBlock } from '@ethereumjs/block' -import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { EVMErrorMessage } from '@ethereumjs/evm' -import { createLegacyTx } from '@ethereumjs/tx' -import { - Account, - Address, - BIGINT_0, - BIGINT_1, - bigIntToBytes, - bytesToBigInt, - concatBytes, - ecsign, - hexToBytes, - privateToAddress, - setLengthLeft, - toBytes, - zeros, -} from '@ethereumjs/util' -import { keccak256 } from 'ethereum-cryptography/keccak' -import { assert, describe, it } from 'vitest' - -import { VM, runTx } from '../../../src/index.js' - -import type { InterpreterStep } from '@ethereumjs/evm' -import type { ECDSASignature } from '@ethereumjs/util' - -const common = new Common({ - chain: Mainnet, - hardfork: Hardfork.London, - eips: [3074], -}) - -// setup the accounts for this test -const privateKey = hexToBytes('0xe331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109') -const authAddress = new Address(privateToAddress(privateKey)) - -const block = createBlock( - { - header: { - baseFeePerGas: BigInt(7), - }, - }, - { common }, -) - -const callerPrivateKey = hexToBytes(`0x${'44'.repeat(32)}`) -const callerAddress = new Address(privateToAddress(callerPrivateKey)) -const PREBALANCE = BigInt(10000000) - -const address = new Address(privateToAddress(privateKey)) -const contractAddress = new Address(hexToBytes(`0x${'ff'.repeat(20)}`)) -const contractStorageAddress = new Address(hexToBytes(`0x${'ee'.repeat(20)}`)) - -// Bytecode to exit call frame and return the topmost stack item -const RETURNTOP = hexToBytes('0x60005260206000F3') -//Bytecode to exit call frame and return the current memory size -const RETURNMEMSIZE = hexToBytes('0x5960005260206000F3') -// Bytecode to store CALLER in slot 0 and GAS in slot 1 and the first 32 bytes of the input in slot 2 -// Returns the entire input as output -const STORECALLER = hexToBytes('0x5A60015533600055600035600255366000600037366000F3') - -/** - * This signs a message to be used for AUTH opcodes - * @param commitUnpadded - The commit which we want to sign - * @param address - The contract address we are using AUTH on - * @param privateKey - The private key of the account to sign - * @returns The signed message - */ -function signMessage( - commitUnpadded: Uint8Array, - address: Address, - privateKey: Uint8Array, - nonce: bigint = BIGINT_0, -) { - const commit = setLengthLeft(commitUnpadded, 32) - const paddedInvokerAddress = setLengthLeft(address.bytes, 32) - const chainId = setLengthLeft(bigIntToBytes(common.chainId()), 32) - const noncePadded = setLengthLeft(bigIntToBytes(nonce), 32) - const message = concatBytes( - hexToBytes('0x04'), - chainId, - noncePadded, - paddedInvokerAddress, - commit, - ) - const msgHash = keccak256(message) - return ecsign(msgHash, privateKey) -} - -/** - * This method returns the bytecode in order to set AUTH - * @param commitUnpadded - The commit - * @param signature - The signature as obtained by `signMessage` - * @param address - The address which signed the commit - * @param msizeBytes - Optional: memory size buffUint8Arrayer, defaults to `0x80` (128 bytes) - */ -function getAuthCode( - commitUnpadded: Uint8Array, - signature: ECDSASignature, - address: Address, - msizeBuffer?: Uint8Array, -) { - const commit = setLengthLeft(commitUnpadded, 32) - let v: Uint8Array - if (signature.v === BigInt(27)) { - v = hexToBytes('0x00') - } else if (signature.v === BigInt(28)) { - v = hexToBytes('0x01') - } else { - v = toBytes(signature.v) - if (v.length > 1) { - throw new Error('v too long') - } - if (v.length === 0) { - v = hexToBytes('0x00') - } - } - - const PUSH32 = hexToBytes('0x7F') - const PUSH1 = hexToBytes('0x60') - const AUTH = hexToBytes('0xF6') - const MSTORE = hexToBytes('0x52') - const MSTORE8 = hexToBytes('0x53') - const mslot0 = zeros(32) - const mslot1 = concatBytes(zeros(31), hexToBytes('0x01')) - const mslot2 = concatBytes(zeros(31), hexToBytes('0x21')) - const mslot3 = concatBytes(zeros(31), hexToBytes('0x41')) - const addressBuffer = setLengthLeft(address.bytes, 32) - // This bytecode setups the stack to be used for AUTH - return concatBytes( - PUSH32, - signature.s, - PUSH32, - mslot2, - MSTORE, - PUSH32, - signature.r, - PUSH32, - mslot1, - MSTORE, - PUSH1, - v, - PUSH32, - mslot0, - MSTORE8, - PUSH32, - commit, - PUSH32, - mslot3, - MSTORE, - hexToBytes('0x60'), - msizeBuffer ?? hexToBytes('0x80'), - hexToBytes('0x6000'), - PUSH32, - addressBuffer, - AUTH, - ) -} - -// This type has all arguments to be used on AUTHCALL -type AuthcallData = { - gasLimit?: bigint - address: Address - value?: bigint - argsOffset?: bigint - argsLength?: bigint - retOffset?: bigint - retLength?: bigint -} - -/** - * Returns the bytecode to store a `value` at memory slot `position` - * @param position - * @param value - */ -function MSTORE(position: Uint8Array, value: Uint8Array) { - return concatBytes( - hexToBytes('0x7F'), - setLengthLeft(value, 32), - hexToBytes('0x7F'), - setLengthLeft(position, 32), - hexToBytes('0x52'), - ) -} - -/** - * This method returns the bytecode to invoke AUTHCALL with the desired data - * @param data - The data to be use for AUTHCALL, anything not present will be zeroed out - * @returns - The bytecode to execute AUTHCALL - */ -function getAuthCallCode(data: AuthcallData) { - const gasLimitBuffer = setLengthLeft(bigIntToBytes(data.gasLimit ?? BigInt(0)), 32) - const addressBuffer = setLengthLeft(data.address.bytes, 32) - const valueBuffer = setLengthLeft(bigIntToBytes(data.value ?? BigInt(0)), 32) - const argsOffsetBuffer = setLengthLeft(bigIntToBytes(data.argsOffset ?? BigInt(0)), 32) - const argsLengthBuffer = setLengthLeft(bigIntToBytes(data.argsLength ?? BigInt(0)), 32) - const retOffsetBuffer = setLengthLeft(bigIntToBytes(data.retOffset ?? BigInt(0)), 32) - const retLengthBuffer = setLengthLeft(bigIntToBytes(data.retLength ?? BigInt(0)), 32) - const PUSH32 = hexToBytes('0x7f') - const AUTHCALL = hexToBytes('0xf7') - const order = [ - retLengthBuffer, - retOffsetBuffer, - argsLengthBuffer, - argsOffsetBuffer, - valueBuffer, - addressBuffer, - gasLimitBuffer, - ] - const bufferList = [] - order.map((e: Uint8Array) => { - bufferList.push(PUSH32) - bufferList.push(e) - }) - bufferList.push(AUTHCALL) - return concatBytes(...bufferList) -} - -// This flips the signature: the result is a signature which has the same public key upon key recovery, -// But the s-value is now > N_DIV_2 -function flipSignature(signature: any) { - const s = bytesToBigInt(signature.s) - const flipped = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n - s - - if (signature.v === 27) { - signature.v = 28 - } else { - signature.v = 27 - } - signature.s = setLengthLeft(bigIntToBytes(flipped), 32) - return signature -} - -describe('EIP-3074 AUTH', () => { - it('Should execute AUTH correctly', async () => { - const vm = await VM.create({ common }) - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes(getAuthCode(message, signature, authAddress), RETURNTOP) - - await vm.stateManager.putCode(contractAddress, code) - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await vm.stateManager.putAccount(callerAddress, new Account()) - const account = await vm.stateManager.getAccount(callerAddress) - account!.balance = BigInt(10000000) - await vm.stateManager.putAccount(callerAddress, account!) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - const buf = result.execResult.returnValue.slice(31) - assert.deepEqual(buf, hexToBytes('0x01'), 'auth should return 1') - }) - - it('Should not set AUTH if signature is invalid', async () => { - const vm = await VM.create({ common }) - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - signature.r = signature.s - const code = concatBytes(getAuthCode(message, signature, authAddress), RETURNTOP) - - await vm.stateManager.putCode(contractAddress, code) - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await vm.stateManager.putAccount(callerAddress, new Account()) - const account = await vm.stateManager.getAccount(callerAddress) - account!.balance = BigInt(10000000) - await vm.stateManager.putAccount(callerAddress, account!) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - const buf = result.execResult.returnValue - assert.deepEqual(buf, zeros(32), 'auth puts 0 on stack on invalid signature') - }) - - it('Should not set AUTH if reported address is invalid', async () => { - const vm = await VM.create({ common }) - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - signature.r = signature.s - // use the contractAddress instead of authAddress for the expected address (this should fail) - const code = concatBytes(getAuthCode(message, signature, contractAddress), RETURNTOP) - - await vm.stateManager.putCode(contractAddress, code) - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await vm.stateManager.putAccount(callerAddress, new Account()) - const account = await vm.stateManager.getAccount(callerAddress) - account!.balance = BigInt(10000000) - await vm.stateManager.putAccount(callerAddress, account!) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - const buf = result.execResult.returnValue - assert.deepEqual(buf, zeros(32), 'auth puts 0') - }) - - it('Should set AUTH to unauthorized if signature s > N_DIV_2', async () => { - const vm = await VM.create({ common }) - const message = hexToBytes('0x01') - const signature = flipSignature(signMessage(message, contractAddress, privateKey)) - const code = concatBytes(getAuthCode(message, signature, authAddress), RETURNTOP) - - await vm.stateManager.putCode(contractAddress, code) - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await vm.stateManager.putAccount(callerAddress, new Account()) - const account = await vm.stateManager.getAccount(callerAddress) - account!.balance = BigInt(10000000) - await vm.stateManager.putAccount(callerAddress, account!) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - const buf = result.execResult.returnValue - assert.deepEqual(buf, zeros(32), 'auth puts 0') - }) - - it('Should set AUTH to unautorized if signatature y > 1', async () => { - const vm = await VM.create({ common }) - const message = hexToBytes('0x01') - const signature = flipSignature(signMessage(message, contractAddress, privateKey)) - signature.v = 2 - const code = concatBytes(getAuthCode(message, signature, authAddress), RETURNTOP) - - await vm.stateManager.putCode(contractAddress, code) - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await vm.stateManager.putAccount(callerAddress, new Account()) - const account = await vm.stateManager.getAccount(callerAddress) - account!.balance = BigInt(10000000) - await vm.stateManager.putAccount(callerAddress, account!) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - const buf = result.execResult.returnValue - assert.deepEqual(buf, zeros(32), 'auth puts 0') - }) - - it('Should be able to call AUTH multiple times', async () => { - const vm = await VM.create({ common }) - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - // This test also tests that the nonce is being read from the account - const signature2 = signMessage(message, contractAddress, callerPrivateKey, BIGINT_1) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCode(message, signature2, callerAddress), - RETURNTOP, - ) - - await vm.stateManager.putCode(contractAddress, code) - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await vm.stateManager.putAccount(callerAddress, new Account()) - const account = await vm.stateManager.getAccount(callerAddress) - account!.balance = BigInt(10000000) - await vm.stateManager.putAccount(callerAddress, account!) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - const buf = result.execResult.returnValue.slice(31) - assert.deepEqual(buf, hexToBytes('0x01'), 'auth returned right address') - }) - - it('Should use zeros in case that memory size < 128', async () => { - const vm = await VM.create({ common }) - const message = hexToBytes('0x00') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes( - getAuthCode(message, signature, authAddress, hexToBytes('0x60')), - RETURNTOP, - ) - - await vm.stateManager.putCode(contractAddress, code) - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await vm.stateManager.putAccount(callerAddress, new Account()) - const account = await vm.stateManager.getAccount(callerAddress) - account!.balance = BigInt(10000000) - await vm.stateManager.putAccount(callerAddress, account!) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - const buf = result.execResult.returnValue.slice(31) - assert.deepEqual(buf, hexToBytes('0x01'), 'auth returned right address') - }) - - it('Should charge memory expansion gas if the memory size > 128', async () => { - const vm = await VM.create({ common }) - const message = hexToBytes('0x00') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes(getAuthCode(message, signature, authAddress), RETURNMEMSIZE) - - await vm.stateManager.putCode(contractAddress, code) - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await vm.stateManager.putAccount(callerAddress, new Account()) - const account = await vm.stateManager.getAccount(callerAddress) - account!.balance = BigInt(20000000) - await vm.stateManager.putAccount(callerAddress, account!) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - - assert.deepEqual( - result.execResult.returnValue.slice(31), - hexToBytes('0x80'), - 'reported msize is correct', - ) - const gas = result.execResult.executionGasUsed - - const code2 = concatBytes( - getAuthCode(message, signature, authAddress, hexToBytes('0x90')), - RETURNMEMSIZE, - ) - - await vm.stateManager.putCode(contractAddress, code2) - const tx2 = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - nonce: 1, - }).sign(callerPrivateKey) - - const result2 = await runTx(vm, { tx: tx2, block, skipHardForkValidation: true }) - - // the memory size in AUTH is 0x90 (so extra 16 bytes), but memory expands with words (32 bytes) - // so the correct amount of msize is 0xa0, not 0x90 - assert.deepEqual( - result2.execResult.returnValue.slice(31), - hexToBytes('0xa0'), - 'reported msize is correct', - ) - assert.ok(result2.execResult.executionGasUsed > gas, 'charged more gas for memory expansion') - }) -}) - -// Setups the environment for the VM, puts `code` at contractAddress and also puts the STORECALLER bytecode at the contractStorageAddress -async function setupVM(code: Uint8Array) { - const vm = await VM.create({ common: common.copy() }) - await vm.stateManager.putCode(contractAddress, code) - await vm.stateManager.putCode(contractStorageAddress, STORECALLER) - await vm.stateManager.putAccount(callerAddress, new Account()) - const account = await vm.stateManager.getAccount(callerAddress) - account!.balance = PREBALANCE - await vm.stateManager.modifyAccountFields(callerAddress, { balance: PREBALANCE }) - return vm -} - -describe('EIP-3074 AUTHCALL', () => { - it('Should execute AUTHCALL correctly', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCallCode({ - address: contractStorageAddress, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - - const buf = result.execResult.returnValue.slice(31) - assert.deepEqual(buf, hexToBytes('0x01'), 'authcall success') - - const storage = await vm.stateManager.getStorage(contractStorageAddress, zeros(32)) - assert.deepEqual(storage, address.bytes, 'caller set correctly') - }) - - it('Should forward max call gas when gas set to 0', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCallCode({ - address: contractStorageAddress, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - - let gas: bigint - vm.evm.events!.on('step', (e: InterpreterStep) => { - if (e.opcode.name === 'AUTHCALL') { - gas = e.gasLeft - } - }) - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await runTx(vm, { tx, block, skipHardForkValidation: true }) - - const gasUsed = await vm.stateManager.getStorage( - contractStorageAddress, - hexToBytes(`0x${'00'.repeat(31)}01`), - ) - const gasBigInt = bytesToBigInt(gasUsed) - const preGas = - gas! - common.param('warmstoragereadGas')! - common.param('coldaccountaccessGas')! - const expected = preGas - preGas / 64n - 2n - assert.equal(gasBigInt, expected, 'forwarded max call gas') - }) - - it('Should forward max call gas when gas set to 0 - warm account', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCallCode({ - address: contractStorageAddress, - }), - getAuthCallCode({ - address: contractStorageAddress, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - - let gas: bigint - vm.evm.events!.on('step', async (e: InterpreterStep) => { - if (e.opcode.name === 'AUTHCALL') { - gas = e.gasLeft // This thus overrides the first time AUTHCALL is used and thus the gas for the second call is stored - } - }) - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await runTx(vm, { tx, block, skipHardForkValidation: true }) - - const gasUsed = await vm.stateManager.getStorage( - contractStorageAddress, - hexToBytes(`0x${'00'.repeat(31)}01`), - ) - const gasBigInt = bytesToBigInt(gasUsed) - const preGas = gas! - common.param('warmstoragereadGas')! - const expected = preGas - preGas / 64n - 2n - assert.equal(gasBigInt, expected, 'forwarded max call gas') - }) - - it('Should forward max call gas when gas set to 0 - cold account, nonzero transfer, create new account', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCallCode({ - address: new Address(hexToBytes(`0x${'cc'.repeat(20)}`)), - value: 1n, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - const account = new Account(BIGINT_0, BIGINT_1) - await vm.stateManager.putAccount(authAddress, account) - - let gas: bigint - let gasAfterCall: bigint - vm.evm.events!.on('step', async (e: InterpreterStep) => { - if (gas && gasAfterCall === undefined) { - gasAfterCall = e.gasLeft - } - if (e.opcode.name === 'AUTHCALL') { - gas = e.gasLeft - } - }) - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 900000, - gasPrice: 10, - value: 1, - }).sign(callerPrivateKey) - - await runTx(vm, { tx, block, skipHardForkValidation: true }) - - const gasBigInt = gas! - gasAfterCall! - const expected = - common.param('coldaccountaccessGas')! + - common.param('warmstoragereadGas')! + - common.param('callNewAccountGas')! + - common.param('authcallValueTransferGas')! - - assert.equal(gasBigInt, expected, 'forwarded max call gas') - }) - - it('Should charge value transfer gas when transferring and transfer from contract, not authcall address', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCallCode({ - address: contractStorageAddress, - value: 1n, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - const authAccount = new Account(BIGINT_0, BIGINT_1) - await vm.stateManager.putAccount(authAddress, authAccount) - - let gas: bigint - vm.evm.events!.on('step', (e: InterpreterStep) => { - if (e.opcode.name === 'AUTHCALL') { - gas = e.gasLeft - } - }) - - const value = 3n - const gasPrice = 10n - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: PREBALANCE / gasPrice - value * gasPrice, - gasPrice, - value, - }).sign(callerPrivateKey) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - - const gasUsed = await vm.stateManager.getStorage( - contractStorageAddress, - hexToBytes(`0x${'00'.repeat(31)}01`), - ) - const gasBigInt = bytesToBigInt(gasUsed) - const preGas = - gas! - - common.param('warmstoragereadGas')! - - common.param('authcallValueTransferGas')! - - common.param('coldaccountaccessGas')! - const expected = preGas - preGas / 64n - 2n - assert.equal(gasBigInt, expected, 'forwarded max call gas') - - const expectedBalance = PREBALANCE - result.amountSpent - value - const account = await vm.stateManager.getAccount(callerAddress) - - assert.equal(account!.balance, expectedBalance, 'caller balance ok') - - const contractAccount = await vm.stateManager.getAccount(contractAddress) - assert.equal(contractAccount!.balance, 2n, 'contract balance ok') - - const contractStorageAccount = await vm.stateManager.getAccount(contractStorageAddress) - assert.equal(contractStorageAccount!.balance, 2n, 'storage balance ok') - }) - - it('Should throw if authorized account does not have enough balance', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCallCode({ - address: contractStorageAddress, - value: 1n, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - - const value = 3n - const gasPrice = 10n - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: PREBALANCE / gasPrice - value * gasPrice, - gasPrice, - value, - }).sign(callerPrivateKey) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - - assert.ok(result.execResult.exceptionError?.error === EVMErrorMessage.OUT_OF_GAS) - }) - - it('Should throw if AUTH not set', async () => { - const code = concatBytes( - getAuthCallCode({ - address: contractStorageAddress, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - assert.equal( - result.execResult.exceptionError?.error, - EVMErrorMessage.AUTHCALL_UNSET, - 'threw with right error', - ) - assert.equal(result.amountSpent, tx.gasPrice * tx.gasLimit, 'spent all gas') - }) - - it('Should unset AUTH in case of invalid signature', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const signature2 = { - v: signature.v, - r: signature.s, - s: signature.s, - } - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCallCode({ - address: contractStorageAddress, - }), - getAuthCode(message, signature2, authAddress), - getAuthCallCode({ - address: contractStorageAddress, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - assert.equal( - result.execResult.exceptionError?.error, - EVMErrorMessage.AUTHCALL_UNSET, - 'threw with right error', - ) - assert.equal(result.amountSpent, tx.gasPrice * tx.gasLimit, 'spent all gas') - }) - - it('Should throw if not enough gas is available', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCallCode({ - address: contractStorageAddress, - gasLimit: 10000000n, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - assert.equal(result.amountSpent, tx.gasLimit * tx.gasPrice, 'spent all gas') - assert.equal( - result.execResult.exceptionError?.error, - EVMErrorMessage.OUT_OF_GAS, - 'correct error type', - ) - }) - - it('Should forward the right amount of gas', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - getAuthCallCode({ - address: contractStorageAddress, - gasLimit: 700000n, - }), - RETURNTOP, - ) - const vm = await setupVM(code) - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - await runTx(vm, { tx, block, skipHardForkValidation: true }) - const gas = await vm.stateManager.getStorage( - contractStorageAddress, - hexToBytes(`0x${'00'.repeat(31)}01`), - ) - const gasBigInt = bytesToBigInt(gas) - assert.equal(gasBigInt, BigInt(700000 - 2), 'forwarded the right amount of gas') // The 2 is subtracted due to the GAS opcode base fee - }) - - it('Should set input and output correctly', async () => { - const message = hexToBytes('0x01') - const signature = signMessage(message, contractAddress, privateKey) - const input = hexToBytes(`0x${'aa'.repeat(32)}`) - const code = concatBytes( - getAuthCode(message, signature, authAddress), - MSTORE(hexToBytes('0x20'), input), - getAuthCallCode({ - address: contractStorageAddress, - argsOffset: 32n, - argsLength: 32n, - retOffset: 64n, - retLength: 32n, - }), - hexToBytes('0x60206040F3'), // PUSH 32 PUSH 64 RETURN -> This returns the 32 bytes at memory position 64 - ) - const vm = await setupVM(code) - - const tx = createLegacyTx({ - to: contractAddress, - gasLimit: 1000000, - gasPrice: 10, - }).sign(callerPrivateKey) - - const result = await runTx(vm, { tx, block, skipHardForkValidation: true }) - const callInput = await vm.stateManager.getStorage( - contractStorageAddress, - hexToBytes(`0x${'00'.repeat(31)}02`), - ) - assert.deepEqual(callInput, input, 'authcall input ok') - assert.deepEqual(result.execResult.returnValue, input, 'authcall output ok') - }) -})