Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client: Extend newPayloadV3 for blob versioned hashes checks #2716

Merged
merged 5 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 77 additions & 6 deletions packages/client/lib/rpc/modules/engine.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Block } from '@ethereumjs/block'
import { Hardfork } from '@ethereumjs/common'
import { BlobEIP4844Transaction } from '@ethereumjs/tx'
import {
bigIntToHex,
bytesToHex,
Expand Down Expand Up @@ -348,6 +349,7 @@ export class Engine {
validators.object(executionPayloadV3FieldValidators)
),
],
[validators.optional(validators.array(validators.bytes32))],
]),
([payload], response) => this.connectionManager.lastNewPayload({ payload, response })
)
Expand Down Expand Up @@ -449,8 +451,10 @@ export class Engine {
* valid block in the branch defined by payload and its ancestors
* 3. validationError: String|null - validation error message
*/
private async newPayload(params: [ExecutionPayload]): Promise<PayloadStatusV1> {
const [payload] = params
private async newPayload(
params: [ExecutionPayload, (Bytes32[] | null)?]
): Promise<PayloadStatusV1> {
const [payload, versionedHashes] = params
if (this.config.synchronized) {
this.connectionManager.newPayloadLog()
}
Expand All @@ -467,6 +471,53 @@ export class Engine {
return response
}

if (block._common.isActivatedEIP(4844)) {
let validationError: string | null = null
if (versionedHashes === undefined || versionedHashes === null) {
validationError = `Error verifying versionedHashes: received none`
} else {
// Collect versioned hashes in the flat array `txVersionedHashes` to match with received
const txVersionedHashes = []
for (const tx of block.transactions) {
if (tx instanceof BlobEIP4844Transaction) {
for (const vHash of tx.versionedHashes) {
g11tech marked this conversation as resolved.
Show resolved Hide resolved
txVersionedHashes.push(vHash)
}
}
}

if (versionedHashes.length !== txVersionedHashes.length) {
validationError = `Error verifying versionedHashes: expected=${txVersionedHashes.length} received=${versionedHashes.length}`
} else {
// match individual hashes
for (let vIndex = 0; vIndex < versionedHashes.length; vIndex++) {
// if mismatch, record error and break
if (
!equalsBytes(hexStringToBytes(versionedHashes[vIndex]), txVersionedHashes[vIndex])
) {
validationError = `Error verifying versionedHashes: mismatch at index=${vIndex} expected=${short(
txVersionedHashes[vIndex]
)} received=${short(versionedHashes[vIndex])}`
break
}
}
}
}

// if there was a validation error return invalid
if (validationError !== null) {
this.config.logger.debug(validationError)
const latestValidHash = await validHash(hexStringToBytes(payload.parentHash), this.chain)
const response = { status: Status.INVALID, latestValidHash, validationError }
return response
}
} else if (versionedHashes !== undefined && versionedHashes !== null) {
const validationError = `Invalid versionedHashes before EIP-4844 is activated`
const latestValidHash = await validHash(hexStringToBytes(payload.parentHash), this.chain)
const response = { status: Status.INVALID, latestValidHash, validationError }
return response
}

this.connectionManager.updatePayloadStats(block)

const hardfork = block._common.hardfork()
Expand Down Expand Up @@ -608,10 +659,30 @@ export class Engine {
}

async newPayloadV3(
params: [ExecutionPayloadV3 | ExecutionPayloadV2 | ExecutionPayloadV1]
params: [ExecutionPayloadV3 | ExecutionPayloadV2 | ExecutionPayloadV1, (Bytes32[] | null)?]
): Promise<PayloadStatusV1> {
const shanghaiTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Shanghai)
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
) {
throw {
code: INVALID_PARAMS,
message: 'Recieved versionedHashes 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 {
Expand All @@ -626,14 +697,14 @@ export class Engine {
if (!('extraDataGas' in params[0])) {
throw {
code: INVALID_PARAMS,
message: 'ExecutionPayloadV2 MUST be used if Shanghai is activated and EIP-4844 is not',
message: 'ExecutionPayloadV2 MUST be used if Shanghai is activated and Cancun is not',
}
}
} else if (eip4844Timestamp === null || parseInt(params[0].timestamp) >= eip4844Timestamp) {
if (!('extraData' in params[0])) {
throw {
code: INVALID_PARAMS,
message: 'ExecutionPayloadV3 MUST be used after EIP-4844 is activated',
message: 'ExecutionPayloadV3 MUST be used after Cancun is activated',
}
}
}
Expand Down
135 changes: 135 additions & 0 deletions packages/client/test/rpc/engine/newPayloadV3VersionedHashes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { BlockHeader } from '@ethereumjs/block'
import * as tape from 'tape'
import * as td from 'testdouble'

import { INVALID_PARAMS } from '../../../lib/rpc/error-code'
import blocks = require('../../testdata/blocks/beacon.json')
import genesisJSON = require('../../testdata/geth-genesis/eip4844.json')
import { baseRequest, params, setupChain } from '../helpers'
import { checkError } from '../util'

import type { HttpServer } from 'jayson'
type Test = tape.Test

const method = 'engine_newPayloadV3'

const [blockData] = blocks

const originalValidate = BlockHeader.prototype._consensusFormatValidation

export const batchBlocks = async (t: Test, server: HttpServer) => {
for (let i = 0; i < 3; i++) {
const req = params(method, [blocks[i], []])
const expectRes = (res: any) => {
t.equal(res.body.result.status, 'VALID')
}
await baseRequest(t, server, req, 200, expectRes, false)
}
}

tape(`${method}: Cancun validations`, (v1) => {
v1.test(`${method}: versionedHashes`, async (t) => {
const { server } = await setupChain(genesisJSON, 'post-merge', { engine: true })

const blockDataExtraVersionedHashes = [
{
...blockData,
parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858',
blockHash: '0x5493df0b38523c8e61cd7dd72ac21b023dc5357a5f297ff8db95a03f8a9c4179',
},
['0x3434', '0x2334'],
]
let req = params(method, blockDataExtraVersionedHashes)
let expectRes = (res: any) => {
t.equal(res.body.result.status, 'INVALID')
t.equal(
res.body.result.validationError,
'Error verifying versionedHashes: expected=0 received=2'
)
}

await baseRequest(t, server, req, 200, expectRes, false)

const txString =
'0x03f87c01808405f5e1008502540be4008401c9c380808080c001e1a001317228841f747eac2b4987a0225753a4f81688b31b21192ad2d2a3f5d252c580a01146addbda4889ddeaa8e4d74baae37c55f9796ab17030c762260faa797ca33ea0555a673397ea115d81c390a560ab77d3f63e93a59270b1b8d12cd2a1fb8b9b11'
const txVersionedHashesString = [
'0x01317228841f747eac2b4987a0225753a4f81688b31b21192ad2d2a3f5d252c5',
]

const blockDataNoneHashes = [
{
...blockData,
parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858',
blockHash: '0x701f665755524486783d70ea3808f6d013ddfcd03972bd87eace1f29a44a83e8',
// two blob transactions but no versioned hashes
transactions: [txString, txString],
},
]
req = params(method, blockDataNoneHashes)
expectRes = checkError(t, INVALID_PARAMS, 'Missing versionedHashes after Cancun is activated')
await baseRequest(t, server, req, 200, expectRes, false)

const blockDataExtraMissingHashes1 = [
{
...blockData,
parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858',
blockHash: '0x701f665755524486783d70ea3808f6d013ddfcd03972bd87eace1f29a44a83e8',
// two blob transactions but missing versioned hash of second
transactions: [txString, txString],
},
txVersionedHashesString,
]
req = params(method, blockDataExtraMissingHashes1)
expectRes = (res: any) => {
t.equal(res.body.result.status, 'INVALID')
t.equal(
res.body.result.validationError,
'Error verifying versionedHashes: expected=2 received=1'
)
}
await baseRequest(t, server, req, 200, expectRes, false)

const blockDataExtraMisMatchingHashes1 = [
{
...blockData,
parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858',
blockHash: '0x701f665755524486783d70ea3808f6d013ddfcd03972bd87eace1f29a44a83e8',
// two blob transactions but mismatching versioned hash of second
transactions: [txString, txString],
},
[...txVersionedHashesString, '0x3456'],
]
req = params(method, blockDataExtraMisMatchingHashes1)
expectRes = (res: any) => {
t.equal(res.body.result.status, 'INVALID')
t.equal(
res.body.result.validationError,
'Error verifying versionedHashes: mismatch at index=1 expected=0x0131…52c5 received=0x3456…'
)
}
await baseRequest(t, server, req, 200, expectRes, false)

const blockDataMatchingVersionedHashes = [
{
...blockData,
parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858',
blockHash: '0x701f665755524486783d70ea3808f6d013ddfcd03972bd87eace1f29a44a83e8',
// two blob transactions with matching versioned hashes
transactions: [txString, txString],
},
[...txVersionedHashesString, ...txVersionedHashesString],
]
req = params(method, blockDataMatchingVersionedHashes)
expectRes = (res: any) => {
t.equal(res.body.result.status, 'ACCEPTED')
}
await baseRequest(t, server, req, 200, expectRes)
})

v1.test(`reset TD`, (t) => {
BlockHeader.prototype._consensusFormatValidation = originalValidate
td.reset()
t.end()
})
v1.end()
})