From 0eba2ce45e8e803c6198ffcff11922fb93b1d53e Mon Sep 17 00:00:00 2001 From: ether-wan Date: Fri, 18 Apr 2025 21:20:50 +0200 Subject: [PATCH 1/5] Client: make JSON-RPC payload limits configurable for eth and engine servers --- packages/client/bin/repl.ts | 2 ++ packages/client/bin/startRPC.ts | 7 +++++++ packages/client/bin/utils.ts | 10 ++++++++++ packages/client/src/config.ts | 20 +++++++++++++++++++- packages/client/src/types.ts | 2 ++ packages/client/src/util/rpc.ts | 7 ++++--- packages/client/test/util/rpc.spec.ts | 2 ++ 7 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/client/bin/repl.ts b/packages/client/bin/repl.ts index b69019a652d..eb7bc3a35c1 100644 --- a/packages/client/bin/repl.ts +++ b/packages/client/bin/repl.ts @@ -42,6 +42,8 @@ const setupClient = async ( jwtSecret: '', rpcEngineAuth: false, rpcCors: '', + rpcEthMaxPayload: args.rpcEthMaxPayload ?? '5mb', + rpcEngineMaxPayload: args.rpcEngineMaxPayload ?? '15mb', }) return { client, executionRPC: servers[0], engineRPC: servers[1] } diff --git a/packages/client/bin/startRPC.ts b/packages/client/bin/startRPC.ts index 5ec7d91ab81..a183aace988 100644 --- a/packages/client/bin/startRPC.ts +++ b/packages/client/bin/startRPC.ts @@ -37,6 +37,8 @@ export type RPCArgs = { jwtSecret?: string rpcEngineAuth: boolean rpcCors: string + rpcEthMaxPayload: string + rpcEngineMaxPayload: string } /** @@ -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 = @@ -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( @@ -191,6 +197,7 @@ export function startRPCServers(client: EthereumClient, args: RPCArgs) { jwtSecret, } : undefined, + maxPayload: rpcEngineMaxPayload, }) rpcHttpServer.listen(rpcEnginePort, rpcEngineAddr) logger.info( diff --git a/packages/client/bin/utils.ts b/packages/client/bin/utils.ts index d80c10c7d16..2a6a8524f31 100644 --- a/packages/client/bin/utils.ts +++ b/packages/client/bin/utils.ts @@ -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: '5mb', + }) + .option('rpcEngineMaxPayload', { + describe: 'Define max JSON payload size for engine RPC requests', + string: true, + default: '15mb', + }) .option('jwtSecret', { describe: 'Provide a file containing a hex encoded jwt secret for Engine RPC server', coerce: (arg: string) => (arg ? path.resolve(arg) : undefined), diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index 1a0223c15fb..82043ff46b9 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -346,6 +346,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 { @@ -356,7 +366,7 @@ export class Config { public readonly events: EventEmitter public static readonly CHAIN_DEFAULT = Mainnet - public static readonly SYNCMODE_DEFAULT = SyncMode.Full + public static readonly SYNCMODE_DEFAULT = SyncMode.None public static readonly DATADIR_DEFAULT = `./datadir` public static readonly PORT_DEFAULT = 30303 public static readonly MAXPERREQUEST_DEFAULT = 100 @@ -365,6 +375,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 @@ -458,6 +470,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 */ @@ -563,6 +578,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 diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index fa13e3f595a..8d97436ca50 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -148,6 +148,8 @@ export interface ClientOpts { skipEngineExec?: boolean ignoreStatelessInvalidExecs?: boolean useJsCrypto?: boolean + rpcEthMaxPayload?: string + rpcEngineMaxPayload?: string } export type PrometheusMetrics = { diff --git a/packages/client/src/util/rpc.ts b/packages/client/src/util/rpc.ts index 3ebf507622f..2ba2b9fe425 100644 --- a/packages/client/src/util/rpc.ts +++ b/packages/client/src/util/rpc.ts @@ -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] @@ -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 diff --git a/packages/client/test/util/rpc.spec.ts b/packages/client/test/util/rpc.spec.ts index 1f207b49395..ce577fd8bd2 100644 --- a/packages/client/test/util/rpc.spec.ts +++ b/packages/client/test/util/rpc.spec.ts @@ -32,6 +32,7 @@ describe('[Util/RPC]', () => { const httpServer = createRPCServerListener({ server, withEngineMiddleware: { jwtSecret: new Uint8Array(32) }, + maxPayload: '15mb', }) const wsServer = createWsRPCServerListener({ server, @@ -74,6 +75,7 @@ describe('[Util/RPC]', () => { const httpServer = createRPCServerListener({ server, withEngineMiddleware: { jwtSecret: new Uint8Array(32) }, + maxPayload: '15mb', }) const wsServer = createWsRPCServerListener({ server, From dd46b414a8928d4049a5b174296eb62ab7f3a71d Mon Sep 17 00:00:00 2001 From: ether-wan Date: Sat, 19 Apr 2025 03:34:54 +0200 Subject: [PATCH 2/5] Client: add tests and config references for JSON-RPC payload limits --- packages/client/bin/repl.ts | 6 +- packages/client/bin/utils.ts | 4 +- packages/client/src/config.ts | 2 +- packages/client/test/util/rpc.spec.ts | 94 ++++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 8 deletions(-) diff --git a/packages/client/bin/repl.ts b/packages/client/bin/repl.ts index eb7bc3a35c1..dab7154b1cc 100644 --- a/packages/client/bin/repl.ts +++ b/packages/client/bin/repl.ts @@ -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' @@ -42,8 +42,8 @@ const setupClient = async ( jwtSecret: '', rpcEngineAuth: false, rpcCors: '', - rpcEthMaxPayload: args.rpcEthMaxPayload ?? '5mb', - rpcEngineMaxPayload: args.rpcEngineMaxPayload ?? '15mb', + rpcEthMaxPayload: args.rpcEthMaxPayload ?? Config.RPC_ETH_MAXPAYLOAD_DEFAULT, + rpcEngineMaxPayload: args.rpcEngineMaxPayload ?? Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT, }) return { client, executionRPC: servers[0], engineRPC: servers[1] } diff --git a/packages/client/bin/utils.ts b/packages/client/bin/utils.ts index 2a6a8524f31..0d5f9f7ea4f 100644 --- a/packages/client/bin/utils.ts +++ b/packages/client/bin/utils.ts @@ -199,12 +199,12 @@ export function getArgs(): ClientOpts { .option('rpcEthMaxPayload', { describe: 'Define max JSON payload size for eth/debug RPC requests', string: true, - default: '5mb', + default: Config.RPC_ETH_MAXPAYLOAD_DEFAULT, }) .option('rpcEngineMaxPayload', { describe: 'Define max JSON payload size for engine RPC requests', string: true, - default: '15mb', + default: Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT, }) .option('jwtSecret', { describe: 'Provide a file containing a hex encoded jwt secret for Engine RPC server', diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index 82043ff46b9..fe062ca58ff 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -366,7 +366,7 @@ export class Config { public readonly events: EventEmitter public static readonly CHAIN_DEFAULT = Mainnet - public static readonly SYNCMODE_DEFAULT = SyncMode.None + public static readonly SYNCMODE_DEFAULT = SyncMode.Full public static readonly DATADIR_DEFAULT = `./datadir` public static readonly PORT_DEFAULT = 30303 public static readonly MAXPERREQUEST_DEFAULT = 100 diff --git a/packages/client/test/util/rpc.spec.ts b/packages/client/test/util/rpc.spec.ts index ce577fd8bd2..fe7e4f5a122 100644 --- a/packages/client/test/util/rpc.spec.ts +++ b/packages/client/test/util/rpc.spec.ts @@ -32,7 +32,7 @@ describe('[Util/RPC]', () => { const httpServer = createRPCServerListener({ server, withEngineMiddleware: { jwtSecret: new Uint8Array(32) }, - maxPayload: '15mb', + maxPayload: Config.RPC_ETH_MAXPAYLOAD_DEFAULT, }) const wsServer = createWsRPCServerListener({ server, @@ -75,7 +75,7 @@ describe('[Util/RPC]', () => { const httpServer = createRPCServerListener({ server, withEngineMiddleware: { jwtSecret: new Uint8Array(32) }, - maxPayload: '15mb', + maxPayload: Config.RPC_ENGINE_MAXPAYLOAD_DEFAULT, }) const wsServer = createWsRPCServerListener({ server, @@ -86,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 () => { From c0af2038ce13df6ff520d8c904f732e7f27ab990 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Tue, 20 May 2025 21:20:14 -0400 Subject: [PATCH 3/5] spellcheck --- config/cspell-ts.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config/cspell-ts.json b/config/cspell-ts.json index abda1bfccda..bae5c61b981 100644 --- a/config/cspell-ts.json +++ b/config/cspell-ts.json @@ -24,6 +24,7 @@ } ], "words": [ + "MAXPAYLOAD", "immediates", "unerasable", "bytelist", From c5defc687cbef30f221d467356595d21e61d36eb Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Tue, 20 May 2025 21:23:37 -0400 Subject: [PATCH 4/5] fix types --- packages/client/src/util/rpc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/util/rpc.ts b/packages/client/src/util/rpc.ts index 2ba2b9fe425..8813743e7fd 100644 --- a/packages/client/src/util/rpc.ts +++ b/packages/client/src/util/rpc.ts @@ -33,7 +33,7 @@ type CreateRPCServerListenerOpts = { withEngineMiddleware?: WithEngineMiddleware } type CreateWSServerOpts = CreateRPCServerListenerOpts & { httpServer?: jayson.HttpServer } -type CreateHTTPServerOpts = CreateRPCServerListenerOpts & { maxPayload: string } +type CreateHTTPServerOpts = CreateRPCServerListenerOpts & { maxPayload?: string } type WithEngineMiddleware = { jwtSecret: Uint8Array; unlessFn?: (req: IncomingMessage) => boolean } export type MethodConfig = (typeof MethodConfig)[keyof typeof MethodConfig] From 95aae7c3599ae72841063ed459d81bbafc1438b1 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 21 May 2025 08:09:14 +0200 Subject: [PATCH 5/5] Update packages/client/src/config.ts --- packages/client/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index 3a808b79615..9ace55de5f0 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -374,7 +374,7 @@ 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_ETH_MAXPAYLOAD_DEFAULT = '11mb' public static readonly RPC_ENGINE_MAXPAYLOAD_DEFAULT = '15mb' public static readonly EXECUTION = true public static readonly NUM_BLOCKS_PER_ITERATION = 100