Skip to content

Commit

Permalink
client: Extend newPayloadV3 for blob versioned hashes checks
Browse files Browse the repository at this point in the history
  • Loading branch information
g11tech committed May 22, 2023
1 parent dd8afc5 commit 6cae63f
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 4 deletions.
63 changes: 59 additions & 4 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,8 @@ 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[]?]): Promise<PayloadStatusV1> {
const [payload, versionedHashes] = params
if (this.config.synchronized) {
this.connectionManager.newPayloadLog()
}
Expand All @@ -467,6 +469,48 @@ export class Engine {
return response
}

if (block._common.isActivatedEIP(4844)) {
let validationError: string | null = null
if (versionedHashes === undefined) {
validationError = `Error verifying versionedHashes: received none`
} else {
// Collect versioned hashes to match
const txVersionedHashes = []
for (const tx of block.transactions) {
if (tx instanceof BlobEIP4844Transaction) {
for (const vHash of tx.versionedHashes) {
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
}
}

this.connectionManager.updatePayloadStats(block)

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

async newPayloadV3(
params: [ExecutionPayloadV3 | ExecutionPayloadV2 | ExecutionPayloadV1]
params: [ExecutionPayloadV3 | ExecutionPayloadV2 | ExecutionPayloadV1, Bytes32[]?]
): 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
) {
throw {
code: INVALID_PARAMS,
message: 'Missing versionedHashes',
}
}

const shanghaiTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Shanghai)
if (shanghaiTimestamp === null || parseInt(params[0].timestamp) < shanghaiTimestamp) {
if ('withdrawals' in params[0]) {
throw {
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 hashesh
transactions: [txString, txString],
},
]
req = params(method, blockDataNoneHashes)
expectRes = checkError(t, INVALID_PARAMS, 'Missing versionedHashes')
await baseRequest(t, server, req, 200, expectRes, false)

const blockDataExtraMissingHashes1 = [
{
...blockData,
parentHash: '0x2559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858',
blockHash: '0x701f665755524486783d70ea3808f6d013ddfcd03972bd87eace1f29a44a83e8',
// two blob transactions but missing versioned hashes 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 hashes 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 but mismatching versioned hashes of second
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()
})

0 comments on commit 6cae63f

Please sign in to comment.