From cd551ec3fae34518319437023848050b8e8ef7cd Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 3 Nov 2022 17:41:01 +0100 Subject: [PATCH 1/2] evm: fix 3860 implementation + tests (#2397) * evm: fix 3860 implementation + tests * Adapt original EIP-3860 tests from vm * add test for Create2 * Add test for CREATE Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com> --- packages/evm/src/opcodes/gas.ts | 10 ++ packages/evm/test/eips/eip-3860.spec.ts | 136 ++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 packages/evm/test/eips/eip-3860.spec.ts diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index b165b92c569..9ae501097ff 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -295,6 +295,11 @@ export const dynamicGasHandlers: Map { + t.test('code exceeds max initcode size', async (st) => { + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + eips: [3860], + }) + const eei = await getEEI() + const evm = await EVM.create({ common, eei }) + + const buffer = Buffer.allocUnsafe(1000000).fill(0x60) + + // setup the call arguments + const runCallArgs = { + sender, // call address + gasLimit: BigInt(0xffffffffff), // ensure we pass a lot of gas, so we do not run out of gas + // Simple test, PUSH PUSH 0 RETURN + // It tries to deploy a contract too large, where the code is all zeros + // (since memory which is not allocated/resized to yet is always defaulted to 0) + data: Buffer.concat([ + Buffer.from( + '0x7F6000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060005260206000F3', + 'hex' + ), + buffer, + ]), + } + const result = await evm.runCall(runCallArgs) + st.ok( + (result.execResult.exceptionError?.error as string) === 'initcode exceeds max initcode size', + 'initcode exceeds max size' + ) + }) + + t.test('ensure EIP-3860 gas is applied on CREATE calls', async (st) => { + // Transaction/Contract data taken from https://github.com/ethereum/tests/pull/990 + const commonWith3860 = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + eips: [3860], + }) + const commonWithout3860 = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + eips: [], + }) + const caller = Address.fromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') + const eei = await getEEI() + const evm = await EVM.create({ common: commonWith3860, eei }) + const evmWithout3860 = await EVM.create({ common: commonWithout3860, eei: eei.copy() }) + const contractFactory = Address.fromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') + const contractAccount = await evm.eei.getAccount(contractFactory) + await evm.eei.putAccount(contractFactory, contractAccount) + await evmWithout3860.eei.putAccount(contractFactory, contractAccount) + const factoryCode = Buffer.from( + '7f600a80600080396000f3000000000000000000000000000000000000000000006000526000355a8160006000f05a8203600a55806000556001600155505050', + 'hex' + ) + + await evm.eei.putContractCode(contractFactory, factoryCode) + await evmWithout3860.eei.putContractCode(contractFactory, factoryCode) + const data = Buffer.from( + '000000000000000000000000000000000000000000000000000000000000c000', + 'hex' + ) + const runCallArgs = { + from: caller, + to: contractFactory, + data, + gasLimit: BigInt(0xfffffffff), + } + const res = await evm.runCall(runCallArgs) + const res2 = await evmWithout3860.runCall(runCallArgs) + st.ok( + res.execResult.executionGasUsed > res2.execResult.executionGasUsed, + 'execution gas used is higher with EIP 3860 active' + ) + st.end() + }) + + t.test('ensure EIP-3860 gas is applied on CREATE2 calls', async (st) => { + // Transaction/Contract data taken from https://github.com/ethereum/tests/pull/990 + const commonWith3860 = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + eips: [3860], + }) + const commonWithout3860 = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + eips: [], + }) + const caller = Address.fromString('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b') + const eei = await getEEI() + const evm = await EVM.create({ common: commonWith3860, eei }) + const evmWithout3860 = await EVM.create({ common: commonWithout3860, eei: eei.copy() }) + const contractFactory = Address.fromString('0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b') + const contractAccount = await evm.eei.getAccount(contractFactory) + await evm.eei.putAccount(contractFactory, contractAccount) + await evmWithout3860.eei.putAccount(contractFactory, contractAccount) + const factoryCode = Buffer.from( + '7f600a80600080396000f3000000000000000000000000000000000000000000006000526000355a60008260006000f55a8203600a55806000556001600155505050', + 'hex' + ) + + await evm.eei.putContractCode(contractFactory, factoryCode) + await evmWithout3860.eei.putContractCode(contractFactory, factoryCode) + const data = Buffer.from( + '000000000000000000000000000000000000000000000000000000000000c000', + 'hex' + ) + const runCallArgs = { + from: caller, + to: contractFactory, + data, + gasLimit: BigInt(0xfffffffff), + } + const res = await evm.runCall(runCallArgs) + const res2 = await evmWithout3860.runCall(runCallArgs) + st.ok( + res.execResult.executionGasUsed > res2.execResult.executionGasUsed, + 'execution gas used is higher with EIP 3860 active' + ) + st.end() + }) +}) From 4d8bbd10847659f885ccf489246b74b92f1c0066 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 3 Nov 2022 21:50:56 +0100 Subject: [PATCH 2/2] Implement EIP4895: Beacon Chain withdrawals (#2353) * common: add eip 4895 * block: implement EIP4895 * vm: add EIP4895 * block: eip4895 tests * vm: add eip4895 tests * block: fix trest * vm: fix tests * change withdrawal type to object format and add validator index * fix vm withdrawal spec Co-authored-by: harkamal --- packages/block/src/block.ts | 95 ++++++++- packages/block/src/header.ts | 30 +++ packages/block/src/helpers.ts | 4 +- packages/block/src/types.ts | 26 ++- packages/block/test/eip4895block.spec.ts | 182 ++++++++++++++++++ packages/block/test/header.spec.ts | 2 +- packages/common/src/eips/4895.json | 13 ++ packages/common/src/eips/index.ts | 1 + packages/evm/src/evm.ts | 3 +- packages/vm/src/runBlock.ts | 26 ++- .../eip-4895-BeaconChainWithdrawals.spec.ts | 112 +++++++++++ packages/vm/test/api/types.spec.ts | 2 +- 12 files changed, 481 insertions(+), 15 deletions(-) create mode 100644 packages/block/test/eip4895block.spec.ts create mode 100644 packages/common/src/eips/4895.json create mode 100644 packages/vm/test/api/EIPs/eip-4895-BeaconChainWithdrawals.spec.ts diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 4d69ee21b61..880451b4109 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -10,6 +10,7 @@ import { bufferToHex, intToHex, isHexPrefixed, + toBuffer, } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak' import { ethers } from 'ethers' @@ -17,7 +18,14 @@ import { ethers } from 'ethers' import { blockFromRpc } from './from-rpc' import { BlockHeader } from './header' -import type { BlockBuffer, BlockData, BlockOptions, JsonBlock, JsonRpcBlock } from './types' +import type { + BlockBuffer, + BlockData, + BlockOptions, + JsonBlock, + JsonRpcBlock, + Withdrawal, +} from './types' import type { Common } from '@ethereumjs/common' import type { FeeMarketEIP1559Transaction, @@ -25,6 +33,7 @@ import type { TxOptions, TypedTransaction, } from '@ethereumjs/tx' +import type { Address } from '@ethereumjs/util' /** * An object that represents the block. @@ -33,6 +42,7 @@ export class Block { public readonly header: BlockHeader public readonly transactions: TypedTransaction[] = [] public readonly uncleHeaders: BlockHeader[] = [] + public readonly withdrawals?: Withdrawal[] public readonly txTrie = new Trie() public readonly _common: Common @@ -43,7 +53,12 @@ export class Block { * @param opts */ public static fromBlockData(blockData: BlockData = {}, opts?: BlockOptions) { - const { header: headerData, transactions: txsData, uncleHeaders: uhsData } = blockData + const { + header: headerData, + transactions: txsData, + uncleHeaders: uhsData, + withdrawals, + } = blockData const header = BlockHeader.fromHeaderData(headerData, opts) // parse transactions @@ -78,7 +93,7 @@ export class Block { uncleHeaders.push(uh) } - return new Block(header, transactions, uncleHeaders, opts) + return new Block(header, transactions, uncleHeaders, opts, withdrawals) } /** @@ -104,11 +119,11 @@ export class Block { * @param opts */ public static fromValuesArray(values: BlockBuffer, opts?: BlockOptions) { - if (values.length > 3) { + if (values.length > 4) { throw new Error('invalid block. More values than expected were received') } - const [headerData, txsData, uhsData] = values + const [headerData, txsData, uhsData, withdrawalsData] = values const header = BlockHeader.fromValuesArray(headerData, opts) @@ -144,7 +159,16 @@ export class Block { uncleHeaders.push(BlockHeader.fromValuesArray(uncleHeaderData, uncleOpts)) } - return new Block(header, transactions, uncleHeaders, opts) + let withdrawals + if (withdrawalsData) { + withdrawals = [] + for (const withdrawal of withdrawalsData) { + const [index, validatorIndex, address, amount] = withdrawal + withdrawals.push({ index, validatorIndex, address, amount }) + } + } + + return new Block(header, transactions, uncleHeaders, opts, withdrawals) } /** @@ -212,7 +236,8 @@ export class Block { header?: BlockHeader, transactions: TypedTransaction[] = [], uncleHeaders: BlockHeader[] = [], - opts: BlockOptions = {} + opts: BlockOptions = {}, + withdrawals?: Withdrawal[] ) { this.header = header ?? BlockHeader.fromHeaderData({}, opts) this.transactions = transactions @@ -234,23 +259,55 @@ export class Block { } } + if (this._common.isActivatedEIP(4895) && withdrawals === undefined) { + throw new Error('Need a withdrawals field if EIP 4895 is active') + } else if (!this._common.isActivatedEIP(4895) && withdrawals !== undefined) { + throw new Error('Cannot have a withdrawals field if EIP 4895 is not active') + } + + this.withdrawals = withdrawals + const freeze = opts?.freeze ?? true if (freeze) { Object.freeze(this) } } + /** + * Convert a withdrawal to a buffer array + * @param withdrawal the withdrawal to convert + * @returns buffer array of the withdrawal + */ + private withdrawalToBufferArray(withdrawal: Withdrawal): [Buffer, Buffer, Buffer, Buffer] { + const { index, validatorIndex, address, amount } = withdrawal + let addressBuffer: Buffer + if (typeof address === 'string') { + addressBuffer = Buffer.from(address.slice(2)) + } else if (Buffer.isBuffer(address)) { + addressBuffer = address + } else { + addressBuffer = (
address).buf + } + return [toBuffer(index), toBuffer(validatorIndex), addressBuffer, toBuffer(amount)] + } + /** * Returns a Buffer Array of the raw Buffers of this block, in order. */ raw(): BlockBuffer { - return [ + const bufferArray = [ this.header.raw(), this.transactions.map((tx) => tx.supports(Capability.EIP2718TypedTransaction) ? tx.serialize() : tx.raw() ) as Buffer[], this.uncleHeaders.map((uh) => uh.raw()), ] + if (this.withdrawals) { + for (const withdrawal of this.withdrawals) { + bufferArray.push(this.withdrawalToBufferArray(withdrawal)) + } + } + return bufferArray } /** @@ -369,6 +426,11 @@ export class Block { const msg = this._errorMsg('invalid uncle hash') throw new Error(msg) } + + if (this._common.isActivatedEIP(4895) && !(await this.validateWithdrawalsTrie())) { + const msg = this._errorMsg('invalid withdrawals trie') + throw new Error(msg) + } } /** @@ -380,6 +442,23 @@ export class Block { return Buffer.from(keccak256(raw)).equals(this.header.uncleHash) } + /** + * Validates the withdrawal root + */ + async validateWithdrawalsTrie(): Promise { + if (!this._common.isActivatedEIP(4895)) { + throw new Error('EIP 4895 is not activated') + } + const trie = new Trie() + let index = 0 + for (const withdrawal of this.withdrawals!) { + const withdrawalRLP = RLP.encode(this.withdrawalToBufferArray(withdrawal)) + await trie.put(Buffer.from('0x' + index.toString(16)), arrToBufArr(withdrawalRLP)) + index++ + } + return trie.root().equals(this.header.withdrawalsRoot!) + } + /** * Consistency checks for uncles included in the block, if any. * diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index f99e584208d..b96969ceb15 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -51,6 +51,7 @@ export class BlockHeader { public readonly mixHash: Buffer public readonly nonce: Buffer public readonly baseFeePerGas?: bigint + public readonly withdrawalsRoot?: Buffer public readonly _common: Common @@ -154,6 +155,7 @@ export class BlockHeader { mixHash: zeros(32), nonce: zeros(8), baseFeePerGas: undefined, + withdrawalsRoot: undefined, } const parentHash = toType(headerData.parentHash, TypeOutput.Buffer) ?? defaults.parentHash @@ -176,6 +178,8 @@ export class BlockHeader { const nonce = toType(headerData.nonce, TypeOutput.Buffer) ?? defaults.nonce let baseFeePerGas = toType(headerData.baseFeePerGas, TypeOutput.BigInt) ?? defaults.baseFeePerGas + const withdrawalsRoot = + toType(headerData.withdrawalsRoot, TypeOutput.Buffer) ?? defaults.withdrawalsRoot const hardforkByBlockNumber = options.hardforkByBlockNumber ?? false if (hardforkByBlockNumber || options.hardforkByTTD !== undefined) { @@ -198,6 +202,18 @@ export class BlockHeader { } } + if (this._common.isActivatedEIP(4895)) { + if (withdrawalsRoot === defaults.withdrawalsRoot) { + throw new Error('invalid header. withdrawalsRoot should be provided') + } + } else { + if (withdrawalsRoot !== undefined) { + throw new Error( + 'A withdrawalsRoot for a header can only be provied with EIP4895 being activated' + ) + } + } + this.parentHash = parentHash this.uncleHash = uncleHash this.coinbase = coinbase @@ -214,6 +230,7 @@ export class BlockHeader { this.mixHash = mixHash this.nonce = nonce this.baseFeePerGas = baseFeePerGas + this.withdrawalsRoot = withdrawalsRoot this._genericFormatValidation() this._validateDAOExtraData() @@ -310,6 +327,19 @@ export class BlockHeader { } } } + + if (this._common.isActivatedEIP(4895) === true) { + if (this.withdrawalsRoot === undefined) { + const msg = this._errorMsg('EIP4895 block has no withdrawalsRoot field') + throw new Error(msg) + } + if (this.withdrawalsRoot?.length !== 32) { + const msg = this._errorMsg( + `withdrawalsRoot must be 32 bytes, received ${this.withdrawalsRoot!.length} bytes` + ) + throw new Error(msg) + } + } } /** diff --git a/packages/block/src/helpers.ts b/packages/block/src/helpers.ts index 1c05c394949..e37a821a586 100644 --- a/packages/block/src/helpers.ts +++ b/packages/block/src/helpers.ts @@ -37,9 +37,10 @@ export function valuesArrayToHeaderData(values: BlockHeaderBuffer): HeaderData { mixHash, nonce, baseFeePerGas, + withdrawalsRoot, ] = values - if (values.length > 16) { + if (values.length > 17) { throw new Error('invalid header. More values than expected were received') } if (values.length < 15) { @@ -63,6 +64,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBuffer): HeaderData { mixHash, nonce, baseFeePerGas, + withdrawalsRoot, } } diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 56c67d37592..f00628d5471 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -95,6 +95,14 @@ export interface HeaderData { mixHash?: BufferLike nonce?: BufferLike baseFeePerGas?: BigIntLike + withdrawalsRoot?: BufferLike +} + +export type Withdrawal = { + index: BigIntLike + validatorIndex: BigIntLike + address: AddressLike + amount: BigIntLike } /** @@ -107,16 +115,20 @@ export interface BlockData { header?: HeaderData transactions?: Array uncleHeaders?: Array + withdrawals?: Array } -export type BlockBuffer = [BlockHeaderBuffer, TransactionsBuffer, UncleHeadersBuffer] +export type BlockBuffer = + | [BlockHeaderBuffer, TransactionsBuffer, UncleHeadersBuffer] + | [BlockHeaderBuffer, TransactionsBuffer, UncleHeadersBuffer, WithdrawalBuffer] export type BlockHeaderBuffer = Buffer[] -export type BlockBodyBuffer = [TransactionsBuffer, UncleHeadersBuffer] +export type BlockBodyBuffer = [TransactionsBuffer, UncleHeadersBuffer, WithdrawalBuffer?] /** * TransactionsBuffer can be an array of serialized txs for Typed Transactions or an array of Buffer Arrays for legacy transactions. */ export type TransactionsBuffer = Buffer[][] | Buffer[] export type UncleHeadersBuffer = Buffer[][] +export type WithdrawalBuffer = Buffer[][] /** * An object with the block's data represented as strings. @@ -128,6 +140,7 @@ export interface JsonBlock { header?: JsonHeader transactions?: JsonTx[] uncleHeaders?: JsonHeader[] + withdrawals?: JsonRpcWithdrawal[] } /** @@ -150,6 +163,14 @@ export interface JsonHeader { mixHash?: string nonce?: string baseFeePerGas?: string + withdrawalsRoot?: string +} + +export interface JsonRpcWithdrawal { + index: string // QUANTITY - bigint 8 bytes + validatorIndex: string // QUANTITY - bigint 8 bytes + address: string // DATA, 20 Bytes address to withdraw to + amount: string // QUANTITY - bigint amount in wei 32 bytes } /* @@ -177,4 +198,5 @@ export interface JsonRpcBlock { transactions: Array // Array of transaction objects, or 32 Bytes transaction hashes depending on the last given parameter. uncles: string[] // Array of uncle hashes baseFeePerGas?: string // If EIP-1559 is enabled for this block, returns the base fee per gas + withdrawals?: Array // If EIP-4895 is enabled for this block, array of withdrawals } diff --git a/packages/block/test/eip4895block.spec.ts b/packages/block/test/eip4895block.spec.ts new file mode 100644 index 00000000000..34be593738f --- /dev/null +++ b/packages/block/test/eip4895block.spec.ts @@ -0,0 +1,182 @@ +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { Address, KECCAK256_RLP } from '@ethereumjs/util' +import * as tape from 'tape' + +import { Block } from '../src/block' +import { BlockHeader } from '../src/header' + +import type { Withdrawal } from '../src' + +const common = new Common({ + eips: [4895], + chain: Chain.Mainnet, + hardfork: Hardfork.Merge, +}) + +// Small hack to hack in the activation block number +// (Otherwise there would be need for a custom chain only for testing purposes) +common.hardforkBlock = function (hardfork: string | undefined) { + if (hardfork === 'london') { + return BigInt(1) + } else if (hardfork === 'dao') { + // Avoid DAO HF side-effects + return BigInt(99) + } + return BigInt(0) +} + +tape('EIP1559 tests', function (t) { + t.test('Header tests', function (st) { + const earlyCommon = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Istanbul }) + st.throws(() => { + BlockHeader.fromHeaderData( + { + withdrawalsRoot: Buffer.from('00'.repeat(32), 'hex'), + }, + { + common: earlyCommon, + } + ) + }, 'should throw when setting withdrawalsRoot with EIP4895 not being activated') + st.throws(() => { + BlockHeader.fromHeaderData( + {}, + { + common, + } + ) + }, 'should throw when withdrawalsRoot is undefined with EIP4895 being activated') + st.doesNotThrow(() => { + BlockHeader.fromHeaderData( + { + withdrawalsRoot: Buffer.from('00'.repeat(32), 'hex'), + }, + { + common, + } + ) + }, 'correctly instantiates an EIP4895 block header') + st.end() + }) + t.test('Block tests', async function (st) { + const earlyCommon = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Istanbul }) + st.throws(() => { + Block.fromBlockData( + { + withdrawals: [], + }, + { + common: earlyCommon, + } + ) + }, 'should throw when setting withdrawals with EIP4895 not being activated') + st.throws(() => { + Block.fromBlockData( + {}, + { + common, + } + ) + }, 'should throw when withdrawals is undefined with EIP4895 being activated') + st.doesNotThrow(() => { + Block.fromBlockData( + { + header: { + withdrawalsRoot: Buffer.from('00'.repeat(32), 'hex'), + }, + withdrawals: [], + }, + { + common, + } + ) + }) + const block = Block.fromBlockData( + { + header: { + withdrawalsRoot: Buffer.from('00'.repeat(32), 'hex'), + }, + withdrawals: [], + }, + { + common, + } + ) + st.notOk(await block.validateWithdrawalsTrie(), 'should invalidate the withdrawals root') + const validHeader = BlockHeader.fromHeaderData( + { + withdrawalsRoot: KECCAK256_RLP, + }, + { common } + ) + const validBlock = Block.fromBlockData( + { + header: validHeader, + withdrawals: [], + }, + { + common, + } + ) + st.ok(await validBlock.validateWithdrawalsTrie(), 'should validate withdrawals root') + + const withdrawal = { + index: BigInt(0), + validatorIndex: BigInt(0), + address: new Address(Buffer.from('20'.repeat(20), 'hex')), + amount: BigInt(1000), + } + + const validBlockWithWithdrawal = Block.fromBlockData( + { + header: { + withdrawalsRoot: Buffer.from( + '69f28913c562b0d38f8dc81e72eb0d99052444d301bf8158dc1f3f94a4526357', + 'hex' + ), + }, + withdrawals: [withdrawal], + }, + { + common, + } + ) + st.ok( + await validBlockWithWithdrawal.validateWithdrawalsTrie(), + 'should validate withdrawals root' + ) + + const withdrawal2 = { + index: BigInt(1), + validatorIndex: BigInt(11), + address: new Address(Buffer.from('30'.repeat(20), 'hex')), + amount: BigInt(2000), + } + + const validBlockWithWithdrawal2 = Block.fromBlockData( + { + header: { + withdrawalsRoot: Buffer.from( + 'cb1accdf466a644291e7b5f0374a3d490d7c5545f9a346f8652f65b3960e720e', + 'hex' + ), + }, + withdrawals: [withdrawal, withdrawal2], + }, + { + common, + } + ) + st.ok( + await validBlockWithWithdrawal2.validateWithdrawalsTrie(), + 'should validate withdrawals root' + ) + st.doesNotThrow(() => { + validBlockWithWithdrawal.hash() + }, 'hashed block with withdrawals') + st.doesNotThrow(() => { + validBlockWithWithdrawal2.hash() + }, 'hashed block with withdrawals') + st.end() + }) +}) diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index 97e98f7617f..e72992d62e2 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -144,7 +144,7 @@ tape('[Block]: Header functions', function (t) { }) t.test('Initialization -> fromValuesArray() -> error cases', function (st) { - const headerArray = Array(17).fill(Buffer.alloc(0)) + const headerArray = Array(18).fill(Buffer.alloc(0)) // mock header data (if set to zeros(0) header throws) headerArray[0] = zeros(32) //parentHash diff --git a/packages/common/src/eips/4895.json b/packages/common/src/eips/4895.json new file mode 100644 index 00000000000..0f90d586dde --- /dev/null +++ b/packages/common/src/eips/4895.json @@ -0,0 +1,13 @@ +{ + "name": "EIP-4895", + "number": 4895, + "comment": "Beacon chain push withdrawals as operations", + "url": "https://eips.ethereum.org/EIPS/eip-4895", + "status": "Draft", + "minimumHardfork": "merge", + "requiredEIPs": [], + "gasConfig": {}, + "gasPrices": {}, + "vm": {}, + "pow": {} +} diff --git a/packages/common/src/eips/index.ts b/packages/common/src/eips/index.ts index 99c1906f597..6033203882a 100644 --- a/packages/common/src/eips/index.ts +++ b/packages/common/src/eips/index.ts @@ -21,5 +21,6 @@ export const EIPs: { [key: number]: any } = { 3860: require('./3860.json'), 4345: require('./4345.json'), 4399: require('./4399.json'), + 4895: require('./4895.json'), 5133: require('./5133.json'), } diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index f476e976d73..8b6b50b01bc 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -79,6 +79,7 @@ export interface EVMOpts { * - [EIP-3855](https://eips.ethereum.org/EIPS/eip-3855) - PUSH0 instruction (`experimental`) * - [EIP-3860](https://eips.ethereum.org/EIPS/eip-3860) - Limit and meter initcode (`experimental`) * - [EIP-4399](https://eips.ethereum.org/EIPS/eip-4399) - Supplant DIFFICULTY opcode with PREVRANDAO (Merge) + * [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895) - Beacon chain push withdrawals as operations (`experimental`) * - [EIP-5133](https://eips.ethereum.org/EIPS/eip-5133) - Delaying Difficulty Bomb to mid-September 2022 * * *Annotations:* @@ -236,7 +237,7 @@ export class EVM implements EVMInterface { // Supported EIPs const supportedEIPs = [ 1153, 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3074, 3198, 3529, 3540, 3541, 3607, 3651, - 3670, 3855, 3860, 4399, 5133, + 3670, 3855, 3860, 4399, 4895, 5133, ] for (const eip of this._common.eips()) { diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 4c4437b9e64..2390eaf2b0b 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -2,7 +2,15 @@ import { Block } from '@ethereumjs/block' import { ConsensusType, Hardfork } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' import { Trie } from '@ethereumjs/trie' -import { Account, Address, bigIntToBuffer, bufArrToArr, intToBuffer, short } from '@ethereumjs/util' +import { + Account, + Address, + bigIntToBuffer, + bufArrToArr, + intToBuffer, + short, + toBuffer, +} from '@ethereumjs/util' import { debug as createDebugLogger } from 'debug' import { Bloom } from './bloom' @@ -237,6 +245,9 @@ async function applyBlock(this: VM, block: Block, opts: RunBlockOpts) { debug(`Apply transactions`) } const blockResults = await applyTransactions.bind(this)(block, opts) + if (this._common.isActivatedEIP(4895)) { + await assignWithdrawals.bind(this)(block) + } // Pay ommers and miners if (block._common.consensusType() === ConsensusType.ProofOfWork) { await assignBlockRewards.bind(this)(block) @@ -316,6 +327,19 @@ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { } } +async function assignWithdrawals(this: VM, block: Block): Promise { + const state = this.eei + const withdrawals = block.withdrawals! + for (const withdrawal of withdrawals) { + const { address: addressData, amount: amountData } = withdrawal + const address = new Address(toBuffer(addressData)) + const amount = Buffer.isBuffer(amountData) + ? BigInt('0x' + amountData.toString('hex')) + : BigInt(amountData) + await rewardAccount(state, address, amount) + } +} + /** * Calculates block rewards for miner and ommers and puts * the updated balances of their accounts to state. diff --git a/packages/vm/test/api/EIPs/eip-4895-BeaconChainWithdrawals.spec.ts b/packages/vm/test/api/EIPs/eip-4895-BeaconChainWithdrawals.spec.ts new file mode 100644 index 00000000000..6968e6d2edb --- /dev/null +++ b/packages/vm/test/api/EIPs/eip-4895-BeaconChainWithdrawals.spec.ts @@ -0,0 +1,112 @@ +import { Block } from '@ethereumjs/block' +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx' +import { Address, zeros } from '@ethereumjs/util' +import * as tape from 'tape' + +import { VM } from '../../../src/vm' + +import type { Withdrawal } from '@ethereumjs/block' + +const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Merge, +}) + +const pkey = Buffer.from('20'.repeat(32), 'hex') + +tape('EIP4895 tests', (t) => { + t.test('EIP4895: withdrawals execute as expected', async (st) => { + const vm = await VM.create({ common }) + vm._common.setEIPs([4895]) + const withdrawals = [] + const addresses = ['20'.repeat(20), '30'.repeat(20), '40'.repeat(20)] + const amounts = [BigInt(1000), BigInt(3000), BigInt(5000)] + + /* + Setup a contract at the second withdrawal address with code: + PUSH 2 + PUSH 0 + SSTORE + If code is ran, this stores "2" at slot "0". Check if withdrawal operations do not invoke this code + */ + const withdrawalCheckAddress = new Address(Buffer.from('fe'.repeat(20), 'hex')) + const withdrawalCode = Buffer.from('6002600055') + + await vm.stateManager.putContractCode(withdrawalCheckAddress, withdrawalCode) + + const contractAddress = new Address(Buffer.from('ff'.repeat(20), 'hex')) + + /* + PUSH + BALANCE // Retrieve balance of addresses[0] + PUSH 0 + MSTORE // Store balance in memory at pos 0 + PUSH 20 + PUSH 0 + RETURN // Return the balance + */ + const contract = '73' + addresses[0] + '3160005260206000F3' + await vm.stateManager.putContractCode(contractAddress, Buffer.from(contract, 'hex')) + + const transaction = FeeMarketEIP1559Transaction.fromTxData({ + to: contractAddress, + maxFeePerGas: BigInt(7), + maxPriorityFeePerGas: BigInt(0), + gasLimit: BigInt(50000), + }).sign(pkey) + + const account = await vm.stateManager.getAccount(transaction.getSenderAddress()) + account.balance = BigInt(1000000) + await vm.stateManager.putAccount(transaction.getSenderAddress(), account) + + let index = 0 + for (let i = 0; i < addresses.length; i++) { + // Just assign any number to validatorIndex as its just for CL convinience + withdrawals.push({ + index, + validatorIndex: index, + address: new Address(Buffer.from(addresses[i], 'hex')), + amount: amounts[i], + }) + index++ + } + const block = Block.fromBlockData( + { + header: { + baseFeePerGas: BigInt(7), + withdrawalsRoot: Buffer.from( + 'c6595e35232ab8ccf2d9af2a1223446c2e60a01667f348ee156608c8dab7795d', + 'hex' + ), + transactionsTrie: Buffer.from( + '9a744e8acc2886e5809ff013e3b71bf8ec97f9941cafbd7730834fc8f76391ba', + 'hex' + ), + }, + transactions: [transaction], + withdrawals, + }, + { common: vm._common } + ) + + let result: Buffer + vm.events.on('afterTx', (e) => { + result = e.execResult.returnValue + }) + + await vm.runBlock({ block, generate: true }) + + for (let i = 0; i < addresses.length; i++) { + const address = new Address(Buffer.from(addresses[i], 'hex')) + const amount = amounts[i] + const balance = (await vm.stateManager.getAccount(address)).balance + st.equals(BigInt(amount), balance, 'balance ok') + } + + st.ok(zeros(32).equals(result!), 'withdrawals happen after transactions') + + const slotValue = await vm.stateManager.getContractStorage(withdrawalCheckAddress, zeros(32)) + st.ok(zeros(0).equals(slotValue), 'withdrawals do not invoke code') + }) +}) diff --git a/packages/vm/test/api/types.spec.ts b/packages/vm/test/api/types.spec.ts index 4f5fcc5cba7..e443cb9efc5 100644 --- a/packages/vm/test/api/types.spec.ts +++ b/packages/vm/test/api/types.spec.ts @@ -21,7 +21,7 @@ tape('[Types]', function (t) { const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Berlin }) // Block - const block: Required = Block.fromBlockData({}, { common }) + const block: Omit, 'withdrawals'> = Block.fromBlockData({}, { common }) st.ok(block, 'block') // Transactions