Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
4 changes: 3 additions & 1 deletion packages/client/bin/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { startRPCServers } from './startRPC.ts'
import { generateClientConfig, getArgs } from './utils.ts'

import type { Common, GenesisState } from '@ethereumjs/common'
import type { Config } from '../src/config.ts'
import { Config } from '../src/config.ts'
import type { EthereumClient } from '../src/index.ts'
import type { ClientOpts } from '../src/types.ts'

Expand Down Expand Up @@ -42,6 +42,8 @@ const setupClient = async (
jwtSecret: '',
rpcEngineAuth: false,
rpcCors: '',
rpcEthMaxPayload: args.rpcEthMaxPayload ?? Config.RPC_ETH_MAXPAYLOAD_DEFAULT,
rpcEngineMaxPayload: args.rpcEngineMaxPayload ?? Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT,
})

return { client, executionRPC: servers[0], engineRPC: servers[1] }
Expand Down
7 changes: 7 additions & 0 deletions packages/client/bin/startRPC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export type RPCArgs = {
jwtSecret?: string
rpcEngineAuth: boolean
rpcCors: string
rpcEthMaxPayload: string
rpcEngineMaxPayload: string
}

/**
Expand Down Expand Up @@ -97,7 +99,10 @@ export function startRPCServers(client: EthereumClient, args: RPCArgs) {
rpcCors,
rpcDebug,
rpcDebugVerbose,
rpcEthMaxPayload,
rpcEngineMaxPayload,
} = args

const manager = new RPCManager(client, config)
const { logger } = config
const jwtSecret =
Expand Down Expand Up @@ -136,6 +141,7 @@ export function startRPCServers(client: EthereumClient, args: RPCArgs) {
: req.body.method.includes('engine_') === false,
}
: undefined,
maxPayload: rpcEthMaxPayload,
})
rpcHttpServer.listen(rpcPort, rpcAddr)
logger?.info(
Expand Down Expand Up @@ -191,6 +197,7 @@ export function startRPCServers(client: EthereumClient, args: RPCArgs) {
jwtSecret,
}
: undefined,
maxPayload: rpcEngineMaxPayload,
})
rpcHttpServer.listen(rpcEnginePort, rpcEngineAddr)
logger?.info(
Expand Down
10 changes: 10 additions & 0 deletions packages/client/bin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ export function getArgs(): ClientOpts {
boolean: true,
default: true,
})
.option('rpcEthMaxPayload', {
describe: 'Define max JSON payload size for eth/debug RPC requests',
string: true,
default: Config.RPC_ETH_MAXPAYLOAD_DEFAULT,
})
.option('rpcEngineMaxPayload', {
describe: 'Define max JSON payload size for engine RPC requests',
string: true,
default: Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT,
})
.option('jwtSecret', {
describe: 'Provide a file containing a hex encoded jwt secret for Engine RPC server',
coerce: (arg: string) => (arg ? path.resolve(arg) : undefined),
Expand Down
18 changes: 18 additions & 0 deletions packages/client/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,16 @@ export interface ConfigOptions {
* Enables Prometheus Metrics that can be collected for monitoring client health
*/
prometheusMetrics?: PrometheusMetrics

/**
* Max JSON payload size for eth/eth RPC requests (untrusted)
*/
rpcEthMaxPayload?: string

/*
* Max JSON payload size for engine RPC requests (trusted)
*/
rpcEngineMaxPayload?: string
}

export class Config {
Expand All @@ -364,6 +374,8 @@ export class Config {
public static readonly MINPEERS_DEFAULT = 1
public static readonly MAXPEERS_DEFAULT = 25
public static readonly DNSADDR_DEFAULT = '8.8.8.8'
public static readonly RPC_ETH_MAXPAYLOAD_DEFAULT = '5mb'
public static readonly RPC_ENGINE_MAXPAYLOAD_DEFAULT = '15mb'
public static readonly EXECUTION = true
public static readonly NUM_BLOCKS_PER_ITERATION = 100
public static readonly ACCOUNT_CACHE = 400000
Expand Down Expand Up @@ -457,6 +469,9 @@ export class Config {

public readonly blobsAndProofsCacheBlocks: number

public readonly rpcEthMaxPayload: string
public readonly rpcEngineMaxPayload: string

public synchronized: boolean
public lastSynchronized?: boolean
/** lastSyncDate in ms */
Expand Down Expand Up @@ -562,6 +577,9 @@ export class Config {
this.blobsAndProofsCacheBlocks =
options.blobsAndProofsCacheBlocks ?? Config.BLOBS_AND_PROOFS_CACHE_BLOCKS

this.rpcEthMaxPayload = options.rpcEthMaxPayload ?? Config.RPC_ETH_MAXPAYLOAD_DEFAULT
this.rpcEngineMaxPayload = options.rpcEngineMaxPayload ?? Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT

this.discDns = this.getDnsDiscovery(options.discDns)
this.discV4 = options.discV4 ?? true

Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ export interface ClientOpts {
skipEngineExec?: boolean
ignoreStatelessInvalidExecs?: boolean
useJsCrypto?: boolean
rpcEthMaxPayload?: string
rpcEngineMaxPayload?: string
}

export type PrometheusMetrics = {
Expand Down
7 changes: 4 additions & 3 deletions packages/client/src/util/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type CreateRPCServerListenerOpts = {
withEngineMiddleware?: WithEngineMiddleware
}
type CreateWSServerOpts = CreateRPCServerListenerOpts & { httpServer?: jayson.HttpServer }
type CreateHTTPServerOpts = CreateRPCServerListenerOpts & { maxPayload: string }
type WithEngineMiddleware = { jwtSecret: Uint8Array; unlessFn?: (req: IncomingMessage) => boolean }

export type MethodConfig = (typeof MethodConfig)[keyof typeof MethodConfig]
Expand Down Expand Up @@ -182,13 +183,13 @@ function checkHeaderAuth(req: any, jwtSecret: Uint8Array): void {
}
}

export function createRPCServerListener(opts: CreateRPCServerListenerOpts): jayson.HttpServer {
const { server, withEngineMiddleware, RPCCors } = opts
export function createRPCServerListener(opts: CreateHTTPServerOpts): jayson.HttpServer {
const { server, withEngineMiddleware, RPCCors, maxPayload } = opts

const app = Connect()
if (typeof RPCCors === 'string') app.use(cors({ origin: RPCCors }))
// GOSSIP_MAX_SIZE_BELLATRIX is proposed to be 10MiB
app.use(JSONParser({ limit: '11mb' }))
app.use(JSONParser({ limit: maxPayload }))

if (withEngineMiddleware) {
const { jwtSecret, unlessFn } = withEngineMiddleware
Expand Down
92 changes: 92 additions & 0 deletions packages/client/test/util/rpc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('[Util/RPC]', () => {
const httpServer = createRPCServerListener({
server,
withEngineMiddleware: { jwtSecret: new Uint8Array(32) },
maxPayload: Config.RPC_ETH_MAXPAYLOAD_DEFAULT,
})
const wsServer = createWsRPCServerListener({
server,
Expand Down Expand Up @@ -74,6 +75,7 @@ describe('[Util/RPC]', () => {
const httpServer = createRPCServerListener({
server,
withEngineMiddleware: { jwtSecret: new Uint8Array(32) },
maxPayload: Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT,
})
const wsServer = createWsRPCServerListener({
server,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test here to verify that for both engine/eth the config is handled correctly? You can likely just set it to some low limit and then try to push more data over the RPC than the limit to verify that it fails.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

Expand All @@ -84,6 +86,96 @@ describe('[Util/RPC]', () => {
'should return http and ws servers',
)
})
it('should reject oversized RPC payloads', async () => {
const config = new Config({
accountCache: 10000,
storageCache: 1000,
rpcEthMaxPayload: '1kb',
rpcEngineMaxPayload: '10mb',
})
const client = await EthereumClient.create({ config, metaDB: new MemoryLevel() })

const manager = new RPCManager(client, config)
const { logger } = config
const methodConfig = Object.values(MethodConfig)[0]
const { server } = createRPCServer(manager, {
methodConfig,
rpcDebug: 'eth',
logger,
rpcDebugVerbose: undefined as any,
})

const ethHttpServer = createRPCServerListener({
server,
withEngineMiddleware: undefined,
maxPayload: config.rpcEthMaxPayload,
})

const engineHttpServer = createRPCServerListener({
server,
withEngineMiddleware: undefined,
maxPayload: config.rpcEngineMaxPayload,
})

const ethPort = 8545
const enginePort = 8551

ethHttpServer.listen(ethPort)
engineHttpServer.listen(enginePort)

const oversizedEthPayload = JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_getBlockByNumber',
params: ['latest', true],
data: 'eth'.repeat(2500),
})

const oversizedEnginePayload = JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'engine_newPayloadV2',
params: [
{
baseFeePerGas: '0x',
blockHash: '0x',
blockNumber: '0x',
extraData: '0x'.repeat(2500),
feeRecipient: '0x',
gasLimit: '0x',
gasUsed: '0x',
logsBloom: '0x',
parentHash: '0x',
prevRandao: '0x',
receiptsRoot: '0x',
stateRoot: '0x',
timestamp: '0x',
transactions: [],
},
],
})

const resEth = await fetch(`http://localhost:${ethPort}`, {
method: 'POST',
body: oversizedEthPayload,
headers: { 'Content-Type': 'application/json' },
})

const resEngine = await fetch(`http://localhost:${enginePort}`, {
method: 'POST',
body: oversizedEnginePayload,
headers: {
'Content-Type': 'application/json',
},
})

assert.strictEqual(
resEth.status,
413,
'ETH server should reject oversized payload with 413 status',
)
assert.strictEqual(resEngine.status, 200, 'ENGINE server should accept oversized payload')
})
})

describe('[Util/RPC/Engine eth methods]', async () => {
Expand Down
Loading