Skip to content
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
1 change: 1 addition & 0 deletions packages/client/lib/rpc/error-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export const INVALID_REQUEST = -32600
export const METHOD_NOT_FOUND = -32601
export const INVALID_PARAMS = -32602
export const INTERNAL_ERROR = -32603
export const TOO_LARGE_REQUEST = -38004
116 changes: 115 additions & 1 deletion packages/client/lib/rpc/modules/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Withdrawal, bigIntToHex, bufferToHex, toBuffer, zeros } from '@ethereum

import { PendingBlock } from '../../miner'
import { short } from '../../util'
import { INTERNAL_ERROR, INVALID_PARAMS } from '../error-code'
import { INTERNAL_ERROR, INVALID_PARAMS, TOO_LARGE_REQUEST } from '../error-code'
import { CLConnectionManager, middleware as cmMiddleware } from '../util/CLConnectionManager'
import { middleware, validators } from '../validation'

Expand Down Expand Up @@ -102,6 +102,12 @@ type BlobsBundleV1 = {
kzgs: Bytes48[]
blobs: Blob[]
}

type ExecutionPayloadBodyV1 = {
transactions: string[]
withdrawals: WithdrawalV1[] | null
}

const EngineError = {
UnknownPayload: {
code: -32001,
Expand Down Expand Up @@ -314,6 +320,16 @@ const assembleBlock = async (
return { block }
}

const getPayloadBody = (block: Block): ExecutionPayloadBodyV1 => {
const transactions = block.transactions.map((tx) => bufferToHex(tx.serialize()))
const withdrawals = block.withdrawals?.map((wt) => wt.toJSON()) ?? null

return {
transactions,
withdrawals,
}
}

/**
* engine_* RPC module
* @memberof module:rpc/modules
Expand Down Expand Up @@ -445,6 +461,25 @@ export class Engine {
middleware(this.getBlobsBundleV1.bind(this), 1, [[validators.bytes8]]),
() => this.connectionManager.updateStatus()
)

this.getCapabilities = cmMiddleware(middleware(this.getCapabilities.bind(this), 0, []), () =>
this.connectionManager.updateStatus()
)

this.getPayloadBodiesByHashV1 = cmMiddleware(
middleware(this.getPayloadBodiesByHashV1.bind(this), 1, [
[validators.array(validators.bytes32)],
]),
() => this.connectionManager.updateStatus()
)

this.getPayloadBodiesByRangeV1 = cmMiddleware(
middleware(this.getPayloadBodiesByRangeV1.bind(this), 2, [
[validators.bytes8],
[validators.bytes8],
]),
() => this.connectionManager.updateStatus()
)
}

/**
Expand Down Expand Up @@ -989,4 +1024,83 @@ export class Engine {
blobs: bundle.blobs.map((blob) => '0x' + blob.toString('hex')),
}
}

/**
* Returns a list of engine API endpoints supported by the client
*/
private getCapabilities(_params: []): string[] {
const caps = Object.getOwnPropertyNames(Engine.prototype)
const engineMethods = caps.filter((el) => el !== 'constructor' && el !== 'getCapabilities')
return engineMethods.map((el) => 'engine_' + el)
}

/**
*
* @param params a list of block hashes as hex prefixed strings
* @returns an array of ExecutionPayloadBodyV1 objects or null if a given execution payload isn't stored locally
*/
private async getPayloadBodiesByHashV1(
params: [[Bytes32]]
): Promise<(ExecutionPayloadBodyV1 | null)[]> {
if (params[0].length > 32) {
throw {
code: TOO_LARGE_REQUEST,
message: 'More than 32 execution payload bodies requested',
}
}
const hashes = params[0].map((hash) => toBuffer(hash))
const blocks: (ExecutionPayloadBodyV1 | null)[] = []
for (const hash of hashes) {
try {
const block = await this.chain.getBlock(hash)
const payloadBody = getPayloadBody(block)
blocks.push(payloadBody)
} catch {
blocks.push(null)
}
}
return blocks
}

/**
*
* @param params an array of 2 parameters
* 1. start: Bytes8 - the first block in the range
* 2. count: Bytes8 - the number of blocks requested
* @returns an array of ExecutionPayloadBodyV1 objects or null if a given execution payload isn't stored locally
*/
private async getPayloadBodiesByRangeV1(
params: [Bytes8, Bytes8]
): Promise<(ExecutionPayloadBodyV1 | null)[]> {
const start = BigInt(params[0])
let count = BigInt(params[1])
if (count > BigInt(32)) {
throw {
code: TOO_LARGE_REQUEST,
message: 'More than 32 execution payload bodies requested',
}
}

if (count < BigInt(1) || start < BigInt(1)) {
throw {
code: INVALID_PARAMS,
message: 'Start and Count parameters cannot be less than 1',
}
}
const currentChainHeight = this.chain.headers.height
if (start + count > currentChainHeight) {
count = count - currentChainHeight
}
const blocks = await this.chain.getBlocks(start, Number(count))
const payloads: (ExecutionPayloadBodyV1 | null)[] = []
for (const block of blocks) {
try {
const payloadBody = getPayloadBody(block)
payloads.push(payloadBody)
} catch {
payloads.push(null)
}
}
return payloads
}
}
1 change: 0 additions & 1 deletion packages/client/lib/rpc/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export function middleware(method: any, requiredParamsCount: number, validators:
}
return reject(error)
}

for (let i = 0; i < validators.length; i++) {
if (validators[i] !== undefined) {
for (let j = 0; j < validators[i].length; j++) {
Expand Down
20 changes: 20 additions & 0 deletions packages/client/test/rpc/engine/getCapabilities.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as tape from 'tape'

import { baseRequest, baseSetup, params } from '../helpers'

const method = 'engine_getCapabilities'

tape(`${method}: call with invalid payloadId`, async (t) => {
const { server } = baseSetup({ engine: true })

const req = params(method, [])
const expectRes = (res: any) => {
t.ok(res.body.result.length > 0, 'got more than 1 engine capability')
t.equal(
res.body.result.findIndex((el: string) => el === 'engine_getCapabilities'),
-1,
'should not include engine_getCapabilities in response'
)
}
await baseRequest(t, server, req, 200, expectRes)
})
208 changes: 208 additions & 0 deletions packages/client/test/rpc/engine/getPayloadBodiesByHashV1.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { Block, BlockHeader } from '@ethereumjs/block'
import { Hardfork } from '@ethereumjs/common'
import { DefaultStateManager } from '@ethereumjs/statemanager'
import { TransactionFactory } from '@ethereumjs/tx'
import { Address } from '@ethereumjs/util'
import { randomBytes } from 'crypto'
import * as tape from 'tape'

import { TOO_LARGE_REQUEST } from '../../../lib/rpc/error-code'
import genesisJSON = require('../../testdata/geth-genesis/eip4844.json')
import preShanghaiGenesisJson = require('../../testdata/geth-genesis/post-merge.json')
import { baseRequest, baseSetup, params, setupChain } from '../helpers'
import { checkError } from '../util'

const method = 'engine_getPayloadBodiesByHashV1'

tape(`${method}: call with too many hashes`, async (t) => {
const { server } = baseSetup({ engine: true, includeVM: true })
const tooManyHashes: string[] = []
for (let x = 0; x < 35; x++) {
tooManyHashes.push('0x' + randomBytes(32).toString('hex'))
}
const req = params(method, [tooManyHashes])
const expectRes = checkError(
t,
TOO_LARGE_REQUEST,
'More than 32 execution payload bodies requested'
)
await baseRequest(t, server, req, 200, expectRes)
})

tape(`${method}: call with valid parameters`, async (t) => {
// Disable stateroot validation in TxPool since valid state root isn't available
const originalSetStateRoot = DefaultStateManager.prototype.setStateRoot
const originalStateManagerCopy = DefaultStateManager.prototype.copy
DefaultStateManager.prototype.setStateRoot = function (): any {}
DefaultStateManager.prototype.copy = function () {
return this
}
const { chain, service, server, common } = await setupChain(genesisJSON, 'post-merge', {
engine: true,
hardfork: Hardfork.ShardingForkDev,
})
common.setHardfork(Hardfork.ShardingForkDev)
const pkey = Buffer.from(
'9c9996335451aab4fc4eac58e31a8c300e095cdbcee532d53d09280e83360355',
'hex'
)
const address = Address.fromPrivateKey(pkey)
const account = await service.execution.vm.stateManager.getAccount(address)

account.balance = 0xfffffffffffffffn
await service.execution.vm.stateManager.putAccount(address, account)
const tx = TransactionFactory.fromTxData(
{
type: 0x01,
maxFeePerDataGas: 1n,
maxFeePerGas: 10000000000n,
maxPriorityFeePerGas: 100000000n,
gasLimit: 30000000n,
},
{ common }
).sign(pkey)
const tx2 = TransactionFactory.fromTxData(
{
type: 0x01,
maxFeePerDataGas: 1n,
maxFeePerGas: 10000000000n,
maxPriorityFeePerGas: 100000000n,
gasLimit: 30000000n,
nonce: 1n,
},
{ common }
).sign(pkey)
const block = Block.fromBlockData(
{
transactions: [tx],
header: BlockHeader.fromHeaderData(
{ parentHash: chain.genesis.hash(), number: 1n },
{ common, skipConsensusFormatValidation: true }
),
},
{ common, skipConsensusFormatValidation: true }
)
const block2 = Block.fromBlockData(
{
transactions: [tx2],
header: BlockHeader.fromHeaderData(
{ parentHash: block.hash(), number: 2n },
{ common, skipConsensusFormatValidation: true }
),
},
{ common, skipConsensusFormatValidation: true }
)

await chain.putBlocks([block, block2], true)

const req = params(method, [
[
'0x' + block.hash().toString('hex'),
'0x' + randomBytes(32).toString('hex'),
'0x' + block2.hash().toString('hex'),
],
])
const expectRes = (res: any) => {
t.equal(
res.body.result[0].transactions[0],
'0x' + tx.serialize().toString('hex'),
'got expected transaction from first payload'
)
t.equal(res.body.result[1], null, 'got null for block not found in chain')
t.equal(res.body.result.length, 3, 'length of response matches number of block hashes sent')
}
await baseRequest(t, server, req, 200, expectRes)
// Restore setStateRoot
DefaultStateManager.prototype.setStateRoot = originalSetStateRoot
DefaultStateManager.prototype.copy = originalStateManagerCopy
})

tape(`${method}: call with valid parameters on pre-Shanghai block`, async (t) => {
// Disable stateroot validation in TxPool since valid state root isn't available
const originalSetStateRoot = DefaultStateManager.prototype.setStateRoot
const originalStateManagerCopy = DefaultStateManager.prototype.copy
DefaultStateManager.prototype.setStateRoot = function (): any {}
DefaultStateManager.prototype.copy = function () {
return this
}
const { chain, service, server, common } = await setupChain(
preShanghaiGenesisJson,
'post-merge',
{
engine: true,
hardfork: Hardfork.London,
}
)
common.setHardfork(Hardfork.London)
const pkey = Buffer.from(
'9c9996335451aab4fc4eac58e31a8c300e095cdbcee532d53d09280e83360355',
'hex'
)
const address = Address.fromPrivateKey(pkey)
const account = await service.execution.vm.stateManager.getAccount(address)

account.balance = 0xfffffffffffffffn
await service.execution.vm.stateManager.putAccount(address, account)
const tx = TransactionFactory.fromTxData(
{
type: 0x01,
maxFeePerDataGas: 1n,
maxFeePerGas: 10000000000n,
maxPriorityFeePerGas: 100000000n,
gasLimit: 30000000n,
},
{ common }
).sign(pkey)
const tx2 = TransactionFactory.fromTxData(
{
type: 0x01,
maxFeePerDataGas: 1n,
maxFeePerGas: 10000000000n,
maxPriorityFeePerGas: 100000000n,
gasLimit: 30000000n,
nonce: 1n,
},
{ common }
).sign(pkey)
const block = Block.fromBlockData(
{
transactions: [tx],
header: BlockHeader.fromHeaderData(
{ parentHash: chain.genesis.hash(), number: 1n },
{ common, skipConsensusFormatValidation: true }
),
},
{ common, skipConsensusFormatValidation: true }
)
const block2 = Block.fromBlockData(
{
transactions: [tx2],
header: BlockHeader.fromHeaderData(
{ parentHash: block.hash(), number: 2n },
{ common, skipConsensusFormatValidation: true }
),
},
{ common, skipConsensusFormatValidation: true }
)

await chain.putBlocks([block, block2], true)

const req = params(method, [
[
'0x' + block.hash().toString('hex'),
'0x' + randomBytes(32).toString('hex'),
'0x' + block2.hash().toString('hex'),
],
])
const expectRes = (res: any) => {
t.equal(
res.body.result[0].withdrawals,
null,
'got null for withdrawals field on pre-Shanghai block'
)
}
await baseRequest(t, server, req, 200, expectRes)
// Restore setStateRoot
DefaultStateManager.prototype.setStateRoot = originalSetStateRoot
DefaultStateManager.prototype.copy = originalStateManagerCopy
})
Loading