From 28f370ebcb58e16cfdc3b775eb67118c0f3f3249 Mon Sep 17 00:00:00 2001 From: harkamal Date: Tue, 18 Jul 2023 15:09:16 +0530 Subject: [PATCH 1/5] client: apply engine api changes for devnet 8 --- packages/client/src/rpc/modules/engine.ts | 173 ++++++++++++---------- 1 file changed, 91 insertions(+), 82 deletions(-) diff --git a/packages/client/src/rpc/modules/engine.ts b/packages/client/src/rpc/modules/engine.ts index 32c88e3f7a4..63bf85bdbf6 100644 --- a/packages/client/src/rpc/modules/engine.ts +++ b/packages/client/src/rpc/modules/engine.ts @@ -46,9 +46,12 @@ type Uint64 = string type Uint256 = string type WithdrawalV1 = Exclude[number] -export type ExecutionPayloadV1 = Omit -export type ExecutionPayloadV2 = ExecutionPayload & { withdrawals: WithdrawalV1[] } -export type ExecutionPayloadV3 = ExecutionPayload & { excessDataGas: Uint64; dataGasUsed: Uint64 } + +// ExecutionPayload has higher version fields as optionals to make it easy for typescript +export type ExecutionPayloadV1 = ExecutionPayload +export type ExecutionPayloadV2 = ExecutionPayloadV1 & { withdrawals: WithdrawalV1[] } +// parentBeaconBlockRoot comes separate in new payloads and needs to be added to payload data +export type ExecutionPayloadV3 = ExecutionPayloadV2 & { excessDataGas: Uint64; dataGasUsed: Uint64 } export type ForkchoiceStateV1 = { headBlockHash: Bytes32 @@ -56,14 +59,19 @@ export type ForkchoiceStateV1 = { finalizedBlockHash: Bytes32 } +// PayloadAttributes has higher version fields as optionals to make it easy for typescript type PayloadAttributes = { timestamp: Uint64 prevRandao: Bytes32 suggestedFeeRecipient: Bytes20 + // add higher version fields as optionals to make it easy for typescript withdrawals?: WithdrawalV1[] + parentBeaconBlockRoot?: Bytes32 } -type PayloadAttributesV1 = Omit -type PayloadAttributesV2 = PayloadAttributes & { withdrawals: WithdrawalV1[] } + +type PayloadAttributesV1 = Omit +type PayloadAttributesV2 = PayloadAttributesV1 & { withdrawals: WithdrawalV1[] } +type PayloadAttributesV3 = PayloadAttributesV2 & { parentBeaconBlockRoot: Bytes32 } export type PayloadStatusV1 = { status: Status @@ -139,9 +147,14 @@ const payloadAttributesFieldValidatorsV1 = { } const payloadAttributesFieldValidatorsV2 = { ...payloadAttributesFieldValidatorsV1, + // withdrawals is optional in V2 because its backward forward compatible with V1 withdrawals: validators.optional(validators.array(validators.withdrawal())), } - +const payloadAttributesFieldValidatorsV3 = { + ...payloadAttributesFieldValidatorsV1, + withdrawals: validators.array(validators.withdrawal()), + parentBeaconBlockRoot: validators.bytes32, +} /** * Formats a block to {@link ExecutionPayloadV1}. */ @@ -406,14 +419,9 @@ export class Engine { this.newPayloadV3 = cmMiddleware( middleware(this.newPayloadV3.bind(this), 1, [ - [ - validators.either( - validators.object(executionPayloadV1FieldValidators), - validators.object(executionPayloadV2FieldValidators), - validators.object(executionPayloadV3FieldValidators) - ), - ], - [validators.optional(validators.array(validators.bytes32))], + [validators.object(executionPayloadV3FieldValidators)], + [validators.array(validators.bytes32)], + [validators.bytes32], ]), ([payload], response) => this.connectionManager.lastNewPayload({ payload, response }) ) @@ -440,7 +448,6 @@ export class Engine { ]), forkchoiceUpdatedResponseCMHandler ) - this.forkchoiceUpdatedV2 = cmMiddleware( middleware(this.forkchoiceUpdatedV2.bind(this), 1, [ [validators.object(forkchoiceFieldValidators)], @@ -448,6 +455,13 @@ export class Engine { ]), forkchoiceUpdatedResponseCMHandler ) + this.forkchoiceUpdatedV3 = cmMiddleware( + middleware(this.forkchoiceUpdatedV3.bind(this), 1, [ + [validators.object(forkchoiceFieldValidators)], + [validators.optional(validators.object(payloadAttributesFieldValidatorsV3))], + ]), + forkchoiceUpdatedResponseCMHandler + ) this.getPayloadV1 = cmMiddleware( middleware(this.getPayloadV1.bind(this), 1, [[validators.bytes8]]), @@ -516,14 +530,22 @@ export class Engine { * 3. validationError: String|null - validation error message */ private async newPayload( - params: [ExecutionPayload, (Bytes32[] | null)?] + params: [ExecutionPayload, (Bytes32[] | null)?, (Bytes32 | null)?] ): Promise { - const [payload, versionedHashes] = params + const [payload, versionedHashes, parentBeaconBlockRoot] = params if (this.config.synchronized) { this.connectionManager.newPayloadLog() } const { parentHash, blockHash } = payload - const { block, error } = await assembleBlock(payload, this.chain) + // newpayloadv3 comes with parentBeaconBlockRoot out of the payload + const { block, error } = await assembleBlock( + { + ...payload, + // ExecutionPayload only handles undefined + parentBeaconBlockRoot: parentBeaconBlockRoot ?? undefined, + }, + this.chain + ) if (!block || error) { let response = error if (!response) { @@ -741,14 +763,31 @@ export class Engine { } async newPayloadV1(params: [ExecutionPayloadV1]): Promise { + const shanghaiTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Shanghai) + const ts = parseInt(params[0].timestamp) + if (shanghaiTimestamp !== null && ts >= shanghaiTimestamp) { + throw { + code: INVALID_PARAMS, + message: 'NewPayloadV2 MUST be used after Cancun is activated', + } + } + return this.newPayload(params) } async newPayloadV2(params: [ExecutionPayloadV2 | ExecutionPayloadV1]): Promise { const shanghaiTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Shanghai) + const eip4844Timestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Cancun) + const ts = parseInt(params[0].timestamp) + const withdrawals = (params[0] as ExecutionPayloadV2).withdrawals - if (shanghaiTimestamp === null || parseInt(params[0].timestamp) < shanghaiTimestamp) { + if (eip4844Timestamp !== null && ts >= eip4844Timestamp) { + throw { + code: INVALID_PARAMS, + message: 'NewPayloadV3 MUST be used after Cancun is activated', + } + } else if (shanghaiTimestamp === null || parseInt(params[0].timestamp) < shanghaiTimestamp) { if (withdrawals !== undefined && withdrawals !== null) { throw { code: INVALID_PARAMS, @@ -763,78 +802,28 @@ export class Engine { } } } - const newPayload = await this.newPayload(params) - if (newPayload.status === Status.INVALID_BLOCK_HASH) { - newPayload.status = Status.INVALID + const newPayloadRes = await this.newPayload(params) + if (newPayloadRes.status === Status.INVALID_BLOCK_HASH) { + newPayloadRes.status = Status.INVALID } - return newPayload + return newPayloadRes } - async newPayloadV3( - params: [ExecutionPayloadV3 | ExecutionPayloadV2 | ExecutionPayloadV1, (Bytes32[] | null)?] - ): Promise { + async newPayloadV3(params: [ExecutionPayloadV3, Bytes32[], Bytes32]): Promise { const eip4844Timestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Cancun) - if ( - eip4844Timestamp !== null && - parseInt(params[0].timestamp) >= eip4844Timestamp && - (params[1] === undefined || params[1] === null) - ) { - throw { - code: INVALID_PARAMS, - message: 'Missing versionedHashes after Cancun is activated', - } - } else if ( - (eip4844Timestamp === null || parseInt(params[0].timestamp) < eip4844Timestamp) && - params[1] !== undefined && - params[1] !== null - ) { + const ts = parseInt(params[0].timestamp) + if (eip4844Timestamp === null || ts < eip4844Timestamp) { throw { code: INVALID_PARAMS, - message: 'Recieved versionedHashes before Cancun is activated', + message: 'NewPayloadV{1|2} MUST be used before Cancun is activated', } } - const shanghaiTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Shanghai) - if (shanghaiTimestamp === null || parseInt(params[0].timestamp) < shanghaiTimestamp) { - if ('withdrawals' in params[0]) { - throw { - code: INVALID_PARAMS, - message: 'ExecutionPayloadV1 MUST be used before Shanghai is activated', - } - } - } else if ( - eip4844Timestamp === null || - (parseInt(params[0].timestamp) >= shanghaiTimestamp && - parseInt(params[0].timestamp) < eip4844Timestamp) - ) { - if ( - 'extraDataGas' in params[0] || - 'dataGasUsed' in params[0] || - !('withdrawals' in params[0]) - ) { - throw { - code: INVALID_PARAMS, - message: 'ExecutionPayloadV2 MUST be used if Shanghai is activated and Cancun is not', - } - } - } else if (parseInt(params[0].timestamp) >= eip4844Timestamp) { - if ( - !('extraData' in params[0]) || - !('dataGasUsed' in params[0]) || - !('withdrawals' in params[0]) - ) { - throw { - code: INVALID_PARAMS, - message: 'ExecutionPayloadV3 MUST be used after Cancun is activated', - } - } + const newPayloadRes = await this.newPayload(params) + if (newPayloadRes.status === Status.INVALID_BLOCK_HASH) { + newPayloadRes.status = Status.INVALID } - - const newPayload = await this.newPayload(params) - if (newPayload.status === Status.INVALID_BLOCK_HASH) { - newPayload.status = Status.INVALID - } - return newPayload + return newPayloadRes } /** @@ -1040,7 +1029,8 @@ export class Engine { let validResponse // If payloadAttributes is present, start building block and return payloadId if (payloadAttributes) { - const { timestamp, prevRandao, suggestedFeeRecipient, withdrawals } = payloadAttributes + const { timestamp, prevRandao, suggestedFeeRecipient, withdrawals, parentBeaconBlockRoot } = + payloadAttributes const timestampBigInt = BigInt(timestamp) if (timestampBigInt <= headBlock.header.timestamp) { @@ -1059,6 +1049,7 @@ export class Engine { timestamp, mixHash: prevRandao, coinbase: suggestedFeeRecipient, + parentBeaconBlockRoot, }, withdrawals ) @@ -1113,6 +1104,24 @@ export class Engine { return this.forkchoiceUpdated(params) } + private async forkchoiceUpdatedV3( + params: [forkchoiceState: ForkchoiceStateV1, payloadAttributes: PayloadAttributesV3 | undefined] + ): Promise { + const payloadAttributes = params[1] + if (payloadAttributes !== undefined && payloadAttributes !== null) { + const cancunTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Cancun) + const ts = BigInt(payloadAttributes.timestamp) + if (ts < cancunTimestamp!) { + throw { + code: INVALID_PARAMS, + message: 'PayloadAttributesV{1|2} MUST be used before Cancun is activated', + } + } + } + + return this.forkchoiceUpdated(params) + } + /** * Given payloadId, returns the most recent version of an execution payload * that is available by the time of the call or responds with an error. From 965097407bab597ef6fb55225bc2c80e7345f6d7 Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 21 Jul 2023 19:14:49 +0530 Subject: [PATCH 2/5] fix client spec --- packages/client/src/rpc/modules/engine.ts | 15 +- packages/client/src/rpc/validation.ts | 9 +- .../test/rpc/engine/newPayloadV3.spec.ts | 285 +++--------------- .../newPayloadV3VersionedHashes.spec.ts | 42 ++- 4 files changed, 87 insertions(+), 264 deletions(-) diff --git a/packages/client/src/rpc/modules/engine.ts b/packages/client/src/rpc/modules/engine.ts index 63bf85bdbf6..71e56a7d20f 100644 --- a/packages/client/src/rpc/modules/engine.ts +++ b/packages/client/src/rpc/modules/engine.ts @@ -418,11 +418,16 @@ export class Engine { ) this.newPayloadV3 = cmMiddleware( - middleware(this.newPayloadV3.bind(this), 1, [ - [validators.object(executionPayloadV3FieldValidators)], - [validators.array(validators.bytes32)], - [validators.bytes32], - ]), + middleware( + this.newPayloadV3.bind(this), + 3, + [ + [validators.object(executionPayloadV3FieldValidators)], + [validators.array(validators.bytes32)], + [validators.bytes32], + ], + ['executionPayload', 'versionedHashes', 'parentBeaconBlockRoot'] + ), ([payload], response) => this.connectionManager.lastNewPayload({ payload, response }) ) diff --git a/packages/client/src/rpc/validation.ts b/packages/client/src/rpc/validation.ts index 800935f8f00..4032eaed708 100644 --- a/packages/client/src/rpc/validation.ts +++ b/packages/client/src/rpc/validation.ts @@ -7,13 +7,18 @@ import { INVALID_PARAMS } from './error-code' * @param requiredParamsCount required parameters count * @param validators array of validators */ -export function middleware(method: any, requiredParamsCount: number, validators: any[] = []): any { +export function middleware( + method: any, + requiredParamsCount: number, + validators: any[] = [], + names: string[] = [] +): any { return function (params: any[] = []) { return new Promise((resolve, reject) => { if (params.length < requiredParamsCount) { const error = { code: INVALID_PARAMS, - message: `missing value for required argument ${params.length}`, + message: `missing value for required argument ${names[params.length] ?? params.length}`, } return reject(error) } diff --git a/packages/client/test/rpc/engine/newPayloadV3.spec.ts b/packages/client/test/rpc/engine/newPayloadV3.spec.ts index c77e7adabe0..73aadcc8614 100644 --- a/packages/client/test/rpc/engine/newPayloadV3.spec.ts +++ b/packages/client/test/rpc/engine/newPayloadV3.spec.ts @@ -1,13 +1,12 @@ import { BlockHeader } from '@ethereumjs/block' -import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx' -import { Address, bytesToHex, hexToBytes, zeros } from '@ethereumjs/util' +import { bigIntToHex } from '@ethereumjs/util' import * as td from 'testdouble' import { assert, describe, it } from 'vitest' import { INVALID_PARAMS } from '../../../src/rpc/error-code' import blocks from '../../testdata/blocks/beacon.json' import genesisJSON from '../../testdata/geth-genesis/post-merge.json' -import { baseRequest, baseSetup, params, setupChain } from '../helpers' +import { baseRequest, params, setupChain } from '../helpers' import { checkError } from '../util' import type { HttpServer } from 'jayson' @@ -27,276 +26,60 @@ export const batchBlocks = async (server: HttpServer) => { await baseRequest(server, req, 200, expectRes, false, false) } } +const parentBeaconBlockRoot = '0x42942949c4ed512cd85c2cb54ca88591338cbb0564d3a2bea7961a639ef29d64' describe(`${method}: call with executionPayloadV1`, () => { - it('call with invalid block hash without 0x', async () => { - const { server } = baseSetup({ engine: true, includeVM: true }) - - const blockDataWithInvalidParentHash = [ - { - ...blockData, - parentHash: blockData.parentHash.slice(2), - }, - ] - - const req = params(method, blockDataWithInvalidParentHash) - const expectRes = checkError( - INVALID_PARAMS, - "invalid argument 0 for key 'parentHash': hex string without 0x prefix" - ) - await baseRequest(server, req, 200, expectRes) - }) - - it('call with invalid hex string as block hash', async () => { - const { server } = baseSetup({ engine: true, includeVM: true }) - - const blockDataWithInvalidBlockHash = [{ ...blockData, blockHash: '0x-invalid-block-hash' }] - const req = params(method, blockDataWithInvalidBlockHash) - const expectRes = checkError( - INVALID_PARAMS, - "invalid argument 0 for key 'blockHash': invalid block hash" - ) - await baseRequest(server, req, 200, expectRes) - }) - - it('call with non existent block hash', async () => { - const { server } = await setupChain(genesisJSON, 'merge', { engine: true }) - - const blockDataNonExistentBlockHash = [ - { - ...blockData, - blockHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858', - }, - ] - const req = params(method, blockDataNonExistentBlockHash) - const expectRes = (res: any) => { - assert.equal(res.body.result.status, 'INVALID') - } - - await baseRequest(server, req, 200, expectRes) - }) - - it('call with non existent parent hash', async () => { - const { server } = await setupChain(genesisJSON, 'post-merge', { engine: true }) - - const blockDataNonExistentParentHash = [ - { - ...blockData, - parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858', - blockHash: '0xf31969a769bfcdbcc1c05f2542fdc7aa9336fc1ea9a82c4925320c035095d649', - }, - ] - const req = params(method, blockDataNonExistentParentHash) - const expectRes = (res: any) => { - assert.equal(res.body.result.status, 'ACCEPTED') - } - - await baseRequest(server, req, 200, expectRes) - }) - - it('call with unknown parent hash to store in remoteBlocks, then call valid ancestor in fcU', async () => { - const { server } = await setupChain(genesisJSON, 'post-merge', { engine: true }) - - let req = params(method, [blocks[1]]) - let expectRes = (res: any) => { - assert.equal(res.body.result.status, 'ACCEPTED') - } - await baseRequest(server, req, 200, expectRes, false, false) - - req = params(method, [blocks[0]]) - expectRes = (res: any) => { - assert.equal(res.body.result.status, 'VALID') - } - await baseRequest(server, req, 200, expectRes, false, false) - - const state = { - headBlockHash: blocks[1].blockHash, - safeBlockHash: blocks[1].blockHash, - finalizedBlockHash: blocks[0].blockHash, - } - req = params('engine_forkchoiceUpdatedV1', [state]) - expectRes = (res: any) => { - assert.equal(res.body.result.payloadStatus.status, 'VALID') - } - - await baseRequest(server, req, 200, expectRes) - }) - - it('invalid terminal block', async () => { - const genesisWithHigherTtd = { - ...genesisJSON, - config: { - ...genesisJSON.config, - terminalTotalDifficulty: 17179869185, - }, - } - - ;(BlockHeader as any).prototype._consensusFormatValidation = td.func() - td.replace('@ethereumjs/block', { BlockHeader }) - - const { server } = await setupChain(genesisWithHigherTtd, 'post-merge', { + it('invalid call before Cancun', async () => { + const { server } = await setupChain(genesisJSON, 'post-merge', { engine: true, }) - - const req = params(method, [blockData, null]) - const expectRes = (res: any) => { - assert.equal(res.body.result.status, 'INVALID') - assert.equal(res.body.result.latestValidHash, bytesToHex(zeros(32))) - } - await baseRequest(server, req, 200, expectRes) - }) - - it('call with valid data', async () => { - const { server } = await setupChain(genesisJSON, 'post-merge', { engine: true }) - - const req = params(method, [blockData]) - const expectRes = (res: any) => { - assert.equal(res.body.result.status, 'VALID') - assert.equal(res.body.result.latestValidHash, blockData.blockHash) - } - await baseRequest(server, req, 200, expectRes) - }) - - it('call with valid data but invalid transactions', async () => { - const { chain, server } = await setupChain(genesisJSON, 'post-merge', { engine: true }) - chain.config.logger.silent = true - const blockDataWithInvalidTransaction = { + // get the genesis json with current date + const validBlock = { ...blockData, - transactions: ['0x1'], + withdrawals: [], + dataGasUsed: '0x0', + excessDataGas: '0x0', } - const expectRes = (res: any) => { - assert.equal(res.body.result.status, 'INVALID') - assert.equal(res.body.result.latestValidHash, blockData.parentHash) - const expectedError = - 'Invalid tx at index 0: Error: Invalid serialized tx input: must be array' - assert.ok( - res.body.result.validationError.includes(expectedError), - `should error with - ${expectedError}` - ) - } - - const req = params(method, [blockDataWithInvalidTransaction]) - await baseRequest(server, req, 200, expectRes) - }) - - it('call with valid data & valid transaction but not signed', async () => { - const { server, common, chain } = await setupChain(genesisJSON, 'post-merge', { engine: true }) - chain.config.logger.silent = true - // Let's mock a non-signed transaction so execution fails - const tx = FeeMarketEIP1559Transaction.fromTxData( - { - gasLimit: 21_000, - maxFeePerGas: 10, - value: 1, - to: Address.fromString('0x61FfE691821291D02E9Ba5D33098ADcee71a3a17'), - }, - { common } + const req = params(method, [validBlock, [], parentBeaconBlockRoot]) + let expectRes = checkError( + INVALID_PARAMS, + 'NewPayloadV{1|2} MUST be used before Cancun is activated' ) - - const transactions = [bytesToHex(tx.serialize())] - const blockDataWithValidTransaction = { - ...blockData, - transactions, - blockHash: '0x308f490332a31fade8b2b46a8e1132cd15adeaffbb651cb523c067b3f007dd9e', - } - const expectRes = (res: any) => { - assert.equal(res.body.result.status, 'INVALID') - assert.isTrue( - res.body.result.validationError.includes('Error verifying block while running:') - ) - } - - const req = params(method, [blockDataWithValidTransaction]) await baseRequest(server, req, 200, expectRes) - }) - - it('call with valid data & valid transaction', async () => { - const accountPk = hexToBytes( - '0xe331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109' - ) - const accountAddress = Address.fromPrivateKey(accountPk) - const newGenesisJSON = { - ...genesisJSON, - alloc: { - ...genesisJSON.alloc, - [accountAddress.toString()]: { - balance: '0x1000000', - }, - }, - } - - const { server, common } = await setupChain(newGenesisJSON, 'post-merge', { engine: true }) - - const tx = FeeMarketEIP1559Transaction.fromTxData( - { - maxFeePerGas: '0x7', - value: 6, - gasLimit: 53_000, - }, - { common } - ).sign(accountPk) - const transactions = [bytesToHex(tx.serialize())] - const blockDataWithValidTransaction = { - ...blockData, - transactions, - parentHash: '0xefc1993f08864165c42195966b3f12794a1a42afa84b1047a46ab6b105828c5c', - receiptsRoot: '0xc508745f9f8b6847a127bbc58b7c6b2c0f073c7ca778b6f020138f0d6d782adf', - gasUsed: '0xcf08', - stateRoot: '0x5a7123ab8bdd4f172438671a2a3de143f2105aa1ac3338c97e5f433e8e380d8d', - blockHash: '0x625f2fd36bf278f92211376cbfe5acd7ac5da694e28f3d94d59488b7dbe213a4', - } - const expectRes = (res: any) => { + expectRes = (res: any) => { assert.equal(res.body.result.status, 'VALID') + assert.equal(res.body.result.latestValidHash, blockData.blockHash) } - const req = params(method, [blockDataWithValidTransaction]) - await baseRequest(server, req, 200, expectRes) }) - it('re-execute payload and verify that no errors occur', async () => { - const { server } = await setupChain(genesisJSON, 'post-merge', { engine: true }) - - await batchBlocks(server) - - let req = params('engine_forkchoiceUpdatedV1', [ - { - headBlockHash: blocks[2].blockHash, - finalizedBlockHash: blocks[2].blockHash, - safeBlockHash: blocks[2].blockHash, - }, - ]) + it('valid data', async () => { + // get the genesis json with current date + const cancunTime = 1689945325 + // deep copy json and add shanghai and cancun to genesis to avoid contamination + const cancunJson = JSON.parse(JSON.stringify(genesisJSON)) + cancunJson.config.shanghaiTime = cancunTime + cancunJson.config.cancunTime = cancunTime + const { server } = await setupChain(cancunJson, 'post-merge', { engine: true }) - // Let's set new head hash - const expectResFcu = (res: any) => { - assert.equal(res.body.result.payloadStatus.status, 'VALID') + const validBlock = { + ...blockData, + timestamp: bigIntToHex(BigInt(cancunTime)), + withdrawals: [], + dataGasUsed: '0x0', + excessDataGas: '0x0', + blockHash: '0x6ec6f32e6931199f8f84faf46a59bc9a1e65a23aa73ca21278b5cb48aa2d059d', + stateRoot: '0x454a9db6943b17a5f88aea507d0c3f4420d533d143b4eb5194cc7589d721b024', } - await baseRequest(server, req, 200, expectResFcu, false, false) - - // Now let's try to re-execute payload - req = params(method, [blockData]) + const req = params(method, [validBlock, [], parentBeaconBlockRoot]) const expectRes = (res: any) => { assert.equal(res.body.result.status, 'VALID') + assert.equal(res.body.result.latestValidHash, validBlock.blockHash) } await baseRequest(server, req, 200, expectRes) }) - it('parent hash equals to block hash', async () => { - const { server } = await setupChain(genesisJSON, 'post-merge', { engine: true }) - const blockDataHasBlockHashSameAsParentHash = [ - { - ...blockData, - blockHash: blockData.parentHash, - }, - ] - const req = params(method, blockDataHasBlockHashSameAsParentHash) - const expectRes = (res: any) => { - assert.equal(res.body.result.status, 'INVALID') - } - - await baseRequest(server, req, 200, expectRes) - }) - it(`reset TD`, () => { BlockHeader.prototype['_consensusFormatValidation'] = originalValidate td.reset() diff --git a/packages/client/test/rpc/engine/newPayloadV3VersionedHashes.spec.ts b/packages/client/test/rpc/engine/newPayloadV3VersionedHashes.spec.ts index 80d78b80d3c..1b3257cb726 100644 --- a/packages/client/test/rpc/engine/newPayloadV3VersionedHashes.spec.ts +++ b/packages/client/test/rpc/engine/newPayloadV3VersionedHashes.spec.ts @@ -32,16 +32,20 @@ describe(`${method}: Cancun validations`, () => { it('versionedHashes', async () => { const { server } = await setupChain(genesisJSON, 'post-merge', { engine: true }) + const parentBeaconBlockRoot = + '0x42942949c4ed512cd85c2cb54ca88591338cbb0564d3a2bea7961a639ef29d64' const blockDataExtraVersionedHashes = [ { ...blockData, parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858', - blockHash: '0x42942949c4ed512cd85c2cb54ca88591338cbb0564d3a2bea7961a639ef29d64', + blockHash: '0xb8b9607bd09f0c18bccfa4dcb6fe355f07d383c902f0fc2a1671cf20792e131c', withdrawals: [], dataGasUsed: '0x0', excessDataGas: '0x0', }, + // versioned hashes ['0x3434', '0x2334'], + parentBeaconBlockRoot, ] let req = params(method, blockDataExtraVersionedHashes) let expectRes = (res: any) => { @@ -64,20 +68,43 @@ describe(`${method}: Cancun validations`, () => { { ...blockData, parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858', - blockHash: '0x701f665755524486783d70ea3808f6d013ddfcd03972bd87eace1f29a44a83e8', + blockHash: '0x141462264b2c27594e8cfcafcadd3545e08c657af4e5882096191632dd4cfc1c', // two blob transactions but no versioned hashes transactions: [txString, txString], + withdrawals: [], + dataGasUsed: '0x40000', + excessDataGas: '0x0', }, ] req = params(method, blockDataNoneHashes) - expectRes = checkError(INVALID_PARAMS, 'Missing versionedHashes after Cancun is activated') + expectRes = checkError(INVALID_PARAMS, 'missing value for required argument versionedHashes') await baseRequest(server, req, 200, expectRes, false) - const blockDataExtraMissingHashes1 = [ + const blockDataMissingParentBeaconRoot = [ { ...blockData, parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858', blockHash: '0x141462264b2c27594e8cfcafcadd3545e08c657af4e5882096191632dd4cfc1c', + // two blob transactions but no versioned hashes + transactions: [txString, txString], + withdrawals: [], + dataGasUsed: '0x40000', + excessDataGas: '0x0', + }, + txVersionedHashesString, + ] + req = params(method, blockDataMissingParentBeaconRoot) + expectRes = checkError( + INVALID_PARAMS, + 'missing value for required argument parentBeaconBlockRoot' + ) + await baseRequest(server, req, 200, expectRes, false) + + const blockDataExtraMissingHashes1 = [ + { + ...blockData, + parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858', + blockHash: '0xeea272bb9ac158550c645a1b0666727a5fefa4a865f8d4c642a87143d2abef39', withdrawals: [], dataGasUsed: '0x40000', excessDataGas: '0x0', @@ -85,6 +112,7 @@ describe(`${method}: Cancun validations`, () => { transactions: [txString, txString], }, txVersionedHashesString, + parentBeaconBlockRoot, ] req = params(method, blockDataExtraMissingHashes1) expectRes = (res: any) => { @@ -100,7 +128,7 @@ describe(`${method}: Cancun validations`, () => { { ...blockData, parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858', - blockHash: '0x141462264b2c27594e8cfcafcadd3545e08c657af4e5882096191632dd4cfc1c', + blockHash: '0xeea272bb9ac158550c645a1b0666727a5fefa4a865f8d4c642a87143d2abef39', withdrawals: [], dataGasUsed: '0x40000', excessDataGas: '0x0', @@ -108,6 +136,7 @@ describe(`${method}: Cancun validations`, () => { transactions: [txString, txString], }, [...txVersionedHashesString, '0x3456'], + parentBeaconBlockRoot, ] req = params(method, blockDataExtraMisMatchingHashes1) expectRes = (res: any) => { @@ -123,7 +152,7 @@ describe(`${method}: Cancun validations`, () => { { ...blockData, parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858', - blockHash: '0x141462264b2c27594e8cfcafcadd3545e08c657af4e5882096191632dd4cfc1c', + blockHash: '0xeea272bb9ac158550c645a1b0666727a5fefa4a865f8d4c642a87143d2abef39', withdrawals: [], dataGasUsed: '0x40000', excessDataGas: '0x0', @@ -131,6 +160,7 @@ describe(`${method}: Cancun validations`, () => { transactions: [txString, txString], }, [...txVersionedHashesString, ...txVersionedHashesString], + parentBeaconBlockRoot, ] req = params(method, blockDataMatchingVersionedHashes) expectRes = (res: any) => { From 5342d3c4f06349f1a1dbb7a537ee8abe5e5fe40e Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 21 Jul 2023 20:07:30 +0530 Subject: [PATCH 3/5] add timestamp tests --- packages/client/src/rpc/modules/engine.ts | 2 +- .../test/rpc/engine/newPayloadV3.spec.ts | 26 +++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/client/src/rpc/modules/engine.ts b/packages/client/src/rpc/modules/engine.ts index 71e56a7d20f..931a9e6d453 100644 --- a/packages/client/src/rpc/modules/engine.ts +++ b/packages/client/src/rpc/modules/engine.ts @@ -773,7 +773,7 @@ export class Engine { if (shanghaiTimestamp !== null && ts >= shanghaiTimestamp) { throw { code: INVALID_PARAMS, - message: 'NewPayloadV2 MUST be used after Cancun is activated', + message: 'NewPayloadV2 MUST be used after Shanghai is activated', } } diff --git a/packages/client/test/rpc/engine/newPayloadV3.spec.ts b/packages/client/test/rpc/engine/newPayloadV3.spec.ts index 73aadcc8614..20ec6946eb2 100644 --- a/packages/client/test/rpc/engine/newPayloadV3.spec.ts +++ b/packages/client/test/rpc/engine/newPayloadV3.spec.ts @@ -12,7 +12,6 @@ import { checkError } from '../util' import type { HttpServer } from 'jayson' const method = 'engine_newPayloadV3' - const [blockData] = blocks const originalValidate = (BlockHeader as any).prototype._consensusFormatValidation @@ -42,15 +41,11 @@ describe(`${method}: call with executionPayloadV1`, () => { } const req = params(method, [validBlock, [], parentBeaconBlockRoot]) - let expectRes = checkError( + const expectRes = checkError( INVALID_PARAMS, 'NewPayloadV{1|2} MUST be used before Cancun is activated' ) await baseRequest(server, req, 200, expectRes) - expectRes = (res: any) => { - assert.equal(res.body.result.status, 'VALID') - assert.equal(res.body.result.latestValidHash, blockData.blockHash) - } }) it('valid data', async () => { @@ -71,9 +66,24 @@ describe(`${method}: call with executionPayloadV1`, () => { blockHash: '0x6ec6f32e6931199f8f84faf46a59bc9a1e65a23aa73ca21278b5cb48aa2d059d', stateRoot: '0x454a9db6943b17a5f88aea507d0c3f4420d533d143b4eb5194cc7589d721b024', } + let expectRes, req - const req = params(method, [validBlock, [], parentBeaconBlockRoot]) - const expectRes = (res: any) => { + const oldMethods = ['engine_newPayloadV1', 'engine_newPayloadV2'] + const expectedErrors = [ + 'NewPayloadV2 MUST be used after Shanghai is activated', + 'NewPayloadV3 MUST be used after Cancun is activated', + ] + for (let index = 0; index < oldMethods.length; index++) { + const oldMethod = oldMethods[index] + const expectedError = expectedErrors[index] + // extra params for old methods should be auto ignored + req = params(oldMethod, [validBlock, [], parentBeaconBlockRoot]) + expectRes = checkError(INVALID_PARAMS, expectedError) + await baseRequest(server, req, 200, expectRes, false, false) + } + + req = params(method, [validBlock, [], parentBeaconBlockRoot]) + expectRes = (res: any) => { assert.equal(res.body.result.status, 'VALID') assert.equal(res.body.result.latestValidHash, validBlock.blockHash) } From 3848e4ede733cca1698ba31c09e5f861c3ca17d4 Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 21 Jul 2023 20:11:34 +0530 Subject: [PATCH 4/5] fix v3 comment --- packages/client/test/rpc/engine/newPayloadV3.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/test/rpc/engine/newPayloadV3.spec.ts b/packages/client/test/rpc/engine/newPayloadV3.spec.ts index 20ec6946eb2..fd2f19cde03 100644 --- a/packages/client/test/rpc/engine/newPayloadV3.spec.ts +++ b/packages/client/test/rpc/engine/newPayloadV3.spec.ts @@ -27,7 +27,7 @@ export const batchBlocks = async (server: HttpServer) => { } const parentBeaconBlockRoot = '0x42942949c4ed512cd85c2cb54ca88591338cbb0564d3a2bea7961a639ef29d64' -describe(`${method}: call with executionPayloadV1`, () => { +describe(`${method}: call with executionPayloadV3`, () => { it('invalid call before Cancun', async () => { const { server } = await setupChain(genesisJSON, 'post-merge', { engine: true, From cf92f9f30654c245d5f73a589045c540dab559bb Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 21 Jul 2023 20:39:44 +0530 Subject: [PATCH 5/5] add fcu3 usage specs --- .../test/rpc/engine/getPayloadV3.spec.ts | 11 +++++- .../test/rpc/engine/newPayloadV3.spec.ts | 38 ++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/client/test/rpc/engine/getPayloadV3.spec.ts b/packages/client/test/rpc/engine/getPayloadV3.spec.ts index 380ac0016b2..cc2a56c562d 100644 --- a/packages/client/test/rpc/engine/getPayloadV3.spec.ts +++ b/packages/client/test/rpc/engine/getPayloadV3.spec.ts @@ -33,7 +33,14 @@ const validPayloadAttributes = { suggestedFeeRecipient: '0xaa00000000000000000000000000000000000000', } -const validPayload = [validForkChoiceState, { ...validPayloadAttributes, withdrawals: [] }] +const validPayload = [ + validForkChoiceState, + { + ...validPayloadAttributes, + withdrawals: [], + parentBeaconBlockRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, +] try { initKZG(kzg, __dirname + '/../../../src/trustedSetups/devnet6.txt') @@ -81,7 +88,7 @@ describe(method, () => { account!.balance = 0xfffffffffffffffn await service.execution.vm.stateManager.putAccount(address, account!) - let req = params('engine_forkchoiceUpdatedV2', validPayload) + let req = params('engine_forkchoiceUpdatedV3', validPayload) let payloadId let expectRes = (res: any) => { payloadId = res.body.result.payloadId diff --git a/packages/client/test/rpc/engine/newPayloadV3.spec.ts b/packages/client/test/rpc/engine/newPayloadV3.spec.ts index fd2f19cde03..88510545e59 100644 --- a/packages/client/test/rpc/engine/newPayloadV3.spec.ts +++ b/packages/client/test/rpc/engine/newPayloadV3.spec.ts @@ -18,7 +18,7 @@ const originalValidate = (BlockHeader as any).prototype._consensusFormatValidati export const batchBlocks = async (server: HttpServer) => { for (let i = 0; i < 3; i++) { - const req = params(method, [blocks[i]]) + const req = params('engine_newPayloadV1', [blocks[i]]) const expectRes = (res: any) => { assert.equal(res.body.result.status, 'VALID') } @@ -49,7 +49,7 @@ describe(`${method}: call with executionPayloadV3`, () => { }) it('valid data', async () => { - // get the genesis json with current date + // get the genesis json with late enougt date with respect to block data in batchBlocks const cancunTime = 1689945325 // deep copy json and add shanghai and cancun to genesis to avoid contamination const cancunJson = JSON.parse(JSON.stringify(genesisJSON)) @@ -90,6 +90,40 @@ describe(`${method}: call with executionPayloadV3`, () => { await baseRequest(server, req, 200, expectRes) }) + it('fcU and verify that no errors occur on new payload', async () => { + // get the genesis json with late enougt date with respect to block data in batchBlocks + const cancunTime = 1689945325 + // deep copy json and add shanghai and cancun to genesis to avoid contamination + const cancunJson = JSON.parse(JSON.stringify(genesisJSON)) + cancunJson.config.shanghaiTime = cancunTime + cancunJson.config.cancunTime = cancunTime + const { server } = await setupChain(cancunJson, 'post-merge', { engine: true }) + + await batchBlocks(server) + + let req = params('engine_forkchoiceUpdatedV3', [ + { + headBlockHash: blocks[2].blockHash, + finalizedBlockHash: blocks[2].blockHash, + safeBlockHash: blocks[2].blockHash, + }, + ]) + + // Let's set new head hash + const expectResFcu = (res: any) => { + assert.equal(res.body.result.payloadStatus.status, 'VALID') + } + await baseRequest(server, req, 200, expectResFcu, false, false) + + // use new payload v1 as blocks all belong to pre-shanghai + req = params('engine_newPayloadV1', [blockData]) + + const expectRes = (res: any) => { + assert.equal(res.body.result.status, 'VALID') + } + await baseRequest(server, req, 200, expectRes) + }) + it(`reset TD`, () => { BlockHeader.prototype['_consensusFormatValidation'] = originalValidate td.reset()