Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
23 changes: 21 additions & 2 deletions packages/client/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { LevelDB } from '../src/execution/level.ts'
import { generateVKTStateRoot } from '../src/util/vkt.ts'

import { helpRPC, startRPCServers } from './startRPC.ts'
import { generateClientConfig, getArgs } from './utils.ts'
import { generateClientConfig, generateRpcConfigs, getArgs } from './utils.ts'
Copy link
Member

Choose a reason for hiding this comment

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

generateRpcConfigs -> generateRpcConfig (so: without the s)

to align with generateClientConfig

Copy link
Member

Choose a reason for hiding this comment

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

(also further down the line there are a few Configs, can you please also rename consistently? Thanks! (we are a bit picky on naming things 🙂 ))


import type * as http from 'http'
import type { Block, BlockBytes } from '@ethereumjs/block'
Expand All @@ -24,6 +24,7 @@ import type { AbstractLevel } from 'abstract-level'
import type { Server as RPCServer } from 'jayson/promise/index.js'
import type { Config } from '../src/config.ts'
import type { Logger } from '../src/logging.ts'
import type { RpcConfig } from '../src/rpc/config.ts'
import type { FullEthereumService } from '../src/service/index.ts'
import type { ClientOpts } from '../src/types.ts'
import type { RPCArgs } from './startRPC.ts'
Expand Down Expand Up @@ -349,8 +350,26 @@ async function run() {
const { config, customGenesisState, customGenesisStateRoot, metricsServer } =
await generateClientConfig(args)

const rpcConfigs = generateRpcConfigs(config, args as RPCArgs)

logger = config.logger

await startClientAndServers(
config,
customGenesisState,
customGenesisStateRoot,
metricsServer,
rpcConfigs,
)
}

async function startClientAndServers(
config: Config,
customGenesisState: GenesisState | undefined,
customGenesisStateRoot: Uint8Array<ArrayBufferLike> | undefined,
metricsServer: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> | undefined,
rpcConfigs: RpcConfig[],
) {
// Do not wait for client to be fully started so that we can hookup SIGINT handling
// else a SIGINT before may kill the process in unclean manner
const clientStartPromise = startClient(config, {
Expand All @@ -360,7 +379,7 @@ async function run() {
.then((client) => {
const servers: (RPCServer | http.Server)[] =
args.rpc === true || args.rpcEngine === true || args.ws === true
? startRPCServers(client, args as RPCArgs)
? startRPCServers(client, rpcConfigs)
: []
if (
client.config.chainCommon.gteHardfork(Hardfork.Paris) &&
Expand Down
11 changes: 7 additions & 4 deletions packages/client/bin/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import process from 'process'

import { createInlineClient } from '../src/util/index.ts'

import { startRPCServers } from './startRPC.ts'
import { generateClientConfig, getArgs } from './utils.ts'
import { type RPCArgs, startRPCServers } from './startRPC.ts'
import { generateClientConfig, generateRpcConfigs, getArgs } from './utils.ts'

import type { Common, GenesisState } from '@ethereumjs/common'
import type { Config } from '../src/config.ts'
Expand All @@ -24,7 +24,8 @@ const setupClient = async (
args.dataDir ?? '',
true,
)
const servers = startRPCServers(client, {

const rpcArgs: RPCArgs = {
rpc: true,
rpcAddr: args.rpcAddr ?? '0.0.0.0',
rpcPort: args.rpcPort ?? 8545,
Expand All @@ -42,7 +43,9 @@ const setupClient = async (
jwtSecret: '',
rpcEngineAuth: false,
rpcCors: '',
})
}

const servers = startRPCServers(client, generateRpcConfigs(config, rpcArgs))

return { client, executionRPC: servers[0], engineRPC: servers[1] }
}
Expand Down
239 changes: 66 additions & 173 deletions packages/client/bin/startRPC.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
import {
EthereumJSErrorWithoutCode,
bytesToUnprefixedHex,
hexToBytes,
randomBytes,
} from '@ethereumjs/util'

import { RPCManager, saveReceiptsMethods } from '../src/rpc/index.ts'
import { RPCManager } from '../src/rpc/index.ts'
import * as modules from '../src/rpc/modules/index.ts'
import {
MethodConfig,
createRPCServer,
createRPCServerListener,
createWsRPCServerListener,
} from '../src/util/index.ts'

import type { Server } from 'jayson/promise/index.js'
import type { EthereumClient } from '../src/client.ts'
import type { Config } from '../src/config.ts'
import type { RpcConfig } from '../src/rpc/config.ts'

export type RPCArgs = {
rpc: boolean
Expand All @@ -39,191 +30,93 @@ export type RPCArgs = {
rpcCors: string
}

/**
* Returns a jwt secret from a provided file path, otherwise saves a randomly generated one to datadir if none already exists
*/
function parseJwtSecret(config: Config, jwtFilePath?: string): Uint8Array {
let jwtSecret: Uint8Array
const defaultJwtPath = `${config.datadir}/jwtsecret`
const usedJwtPath = jwtFilePath ?? defaultJwtPath

// If jwtFilePath is provided, it should exist
if (jwtFilePath !== undefined && !existsSync(jwtFilePath)) {
throw EthereumJSErrorWithoutCode(`No file exists at provided jwt secret path=${jwtFilePath}`)
}

if (jwtFilePath !== undefined || existsSync(defaultJwtPath)) {
const jwtSecretContents = readFileSync(jwtFilePath ?? defaultJwtPath, 'utf-8').trim()
const hexPattern = new RegExp(/^(0x|0X)?(?<jwtSecret>[a-fA-F0-9]+)$/, 'g')
const jwtSecretHex = hexPattern.exec(jwtSecretContents)?.groups?.jwtSecret
if (jwtSecretHex === undefined || jwtSecretHex.length !== 64) {
throw Error('Need a valid 256 bit hex encoded secret')
}
jwtSecret = hexToBytes(`0x${jwtSecretHex}`)
} else {
const folderExists = existsSync(config.datadir)
if (!folderExists) {
mkdirSync(config.datadir, { recursive: true })
}

jwtSecret = randomBytes(32)
writeFileSync(defaultJwtPath, bytesToUnprefixedHex(jwtSecret), {})
config.logger?.info(`New Engine API JWT token created path=${defaultJwtPath}`)
}
config.logger?.info(`Using Engine API with JWT token authentication path=${usedJwtPath}`)
return jwtSecret
}

/**
* Starts and returns enabled RPCServers
*/
export function startRPCServers(client: EthereumClient, args: RPCArgs) {
export function startRPCServers(client: EthereumClient, rpcConfigs: RpcConfig[]): Server[] {
const { config } = client
const servers: Server[] = []
const {
rpc,
rpcAddr,
rpcPort,
ws,
wsPort,
wsAddr,
rpcEngine,
rpcEngineAddr,
rpcEnginePort,
wsEngineAddr,
wsEnginePort,
jwtSecret: jwtSecretPath,
rpcEngineAuth,
rpcCors,
rpcDebug,
rpcDebugVerbose,
} = args

const manager = new RPCManager(client, config)
const { logger } = config
const jwtSecret =
rpcEngine && rpcEngineAuth ? parseJwtSecret(config, jwtSecretPath) : new Uint8Array(0)
let withEngineMethods = false

if ((rpc || rpcEngine) && !config.saveReceipts) {
logger?.warn(
`Starting client without --saveReceipts might lead to interop issues with a CL especially if the CL intends to propose blocks, omitting methods=${saveReceiptsMethods}`,
)
}

if (rpc || ws) {
let rpcHttpServer
withEngineMethods = rpcEngine && rpcEnginePort === rpcPort && rpcEngineAddr === rpcAddr

const { server, namespaces, methods } = createRPCServer(manager, {
methodConfig: withEngineMethods ? MethodConfig.WithEngine : MethodConfig.WithoutEngine,
rpcDebugVerbose,
rpcDebug,
logger,
})
servers.push(server)

if (rpc) {
rpcHttpServer = createRPCServerListener({
RPCCors: rpcCors,
server,
withEngineMiddleware:
withEngineMethods && rpcEngineAuth
? {
jwtSecret,
unlessFn: (req: any) =>
Array.isArray(req.body)
? req.body.some((r: any) => r.method.includes('engine_')) === false
: req.body.method.includes('engine_') === false,
}
: undefined,
const serverGroups: Map<string, { rpcConfig: RpcConfig; server: any }> = new Map()

for (const rpcConfig of rpcConfigs) {
// unique key for each server: eth-rpc (http & ws), engine-rpc (http & ws)
// used to create a single rpc server for each transport type
const key = `${rpcConfig.type}-${rpcConfig.methodConfig}`

let serverEntry = serverGroups.get(key)

if (!serverEntry) {
const { server, namespaces, methods } = createRPCServer(manager, {
methodConfig: rpcConfig.methodConfig,
rpcDebug: rpcConfig.debug,
rpcDebugVerbose: rpcConfig.debugVerbose,
logger: config.logger,
})
rpcHttpServer.listen(rpcPort, rpcAddr)
logger?.info(
`Started JSON RPC Server address=http://${rpcAddr}:${rpcPort} namespaces=${namespaces}${
withEngineMethods ? ' rpcEngineAuth=' + rpcEngineAuth.toString() : ''
}`,
)
logger?.debug(
`Methods available at address=http://${rpcAddr}:${rpcPort} namespaces=${namespaces} methods=${Object.keys(

servers.push(server)
serverGroups.set(key, { rpcConfig, server })
serverEntry = { rpcConfig, server }

config.logger?.info(
`Created RPCServer for type=${rpcConfig.type} methodConfig=${rpcConfig.methodConfig} namespaces=${namespaces} methods=${Object.keys(
methods,
).join(',')}`,
)
}
if (ws) {
const opts: any = {
rpcCors,

const { server } = serverEntry
// middleware for engine auth
const middleware =
rpcConfig.engineAuth && rpcConfig.jwtSecret
? {
jwtSecret: rpcConfig.jwtSecret,
unlessFn: (req: any) =>
Array.isArray(req.body)
? req.body.some((r: any) => r.method.includes('engine_')) === false
: req.body.method.includes('engine_') === false,
}
: undefined

if (rpcConfig.transport === 'http') {
const httpServer = createRPCServerListener({
RPCCors: rpcConfig.cors,
server,
withEngineMiddleware: withEngineMethods && rpcEngineAuth ? { jwtSecret } : undefined,
}
if (rpcAddr === wsAddr && rpcPort === wsPort) {
// We want to load the websocket upgrade request to the same server
opts.httpServer = rpcHttpServer
}
withEngineMiddleware: middleware,
})
httpServer.listen(rpcConfig.port, rpcConfig.address)

const rpcWsServer = createWsRPCServerListener(opts)
if (rpcWsServer) rpcWsServer.listen(wsPort)
logger?.info(
`Started JSON RPC Server address=ws://${wsAddr}:${wsPort} namespaces=${namespaces}${
withEngineMethods ? ` rpcEngineAuth=${rpcEngineAuth}` : ''
config.logger?.info(
`Started JSON RPC Server address=http://${rpcConfig.address}:${rpcConfig.port} type=${rpcConfig.type} ${
rpcConfig.engineAuth ? 'engineAuth=true' : ''
}`,
)
logger?.debug(
`Methods available at address=ws://${wsAddr}:${wsPort} namespaces=${namespaces} methods=${Object.keys(
methods,
).join(',')}`,
)
}
}

if (rpcEngine && !(rpc && rpcPort === rpcEnginePort && rpcAddr === rpcEngineAddr)) {
const { server, namespaces, methods } = createRPCServer(manager, {
methodConfig: MethodConfig.EngineOnly,
rpcDebug,
rpcDebugVerbose,
logger,
})
servers.push(server)
const rpcHttpServer = createRPCServerListener({
RPCCors: rpcCors,
server,
withEngineMiddleware: rpcEngineAuth
? {
jwtSecret,
}
: undefined,
})
rpcHttpServer.listen(rpcEnginePort, rpcEngineAddr)
logger?.info(
`Started JSON RPC server address=http://${rpcEngineAddr}:${rpcEnginePort} namespaces=${namespaces} rpcEngineAuth=${rpcEngineAuth}`,
)
logger?.debug(
`Methods available at address=http://${rpcEngineAddr}:${rpcEnginePort} namespaces=${namespaces} methods=${Object.keys(
methods,
).join(',')}`,
)

if (ws) {
const opts: any = {
rpcCors,
if (rpcConfig.transport === 'ws') {
const wsOpts: any = {
RPCCors: rpcConfig.cors,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to add typing for this instead of casting to any?

server,
withEngineMiddleware: rpcEngineAuth ? { jwtSecret } : undefined,
withEngineMiddleware: middleware,
}

if (rpcEngineAddr === wsEngineAddr && rpcEnginePort === wsEnginePort) {
// We want to load the websocket upgrade request to the same server
opts.httpServer = rpcHttpServer
// Attach to existing HTTP server for upgrades if same port/address
const httpKey = `${rpcConfig.type}-${rpcConfig.methodConfig}`
if (rpcConfig.address === rpcConfig.address && serverGroups.has(httpKey)) {
wsOpts.httpServer = serverGroups.get(httpKey)?.server
}

const rpcWsServer = createWsRPCServerListener(opts)
if (rpcWsServer) rpcWsServer.listen(wsEnginePort, wsEngineAddr)
logger?.info(
`Started JSON RPC Server address=ws://${wsEngineAddr}:${wsEnginePort} namespaces=${namespaces} rpcEngineAuth=${rpcEngineAuth}`,
)
logger?.debug(
`Methods available at address=ws://${wsEngineAddr}:${wsEnginePort} namespaces=${namespaces} methods=${Object.keys(
methods,
).join(',')}`,
)
const wsServer = createWsRPCServerListener(wsOpts)
if (wsServer) {
wsServer.listen(rpcConfig.port)
config.logger?.info(
`Started JSON RPC WS Server address=ws://${rpcConfig.address}:${rpcConfig.port} namespaces=${rpcConfig.type} ${
rpcConfig.engineAuth ? 'engineAuth=true' : ''
}`,
)
}
}
}

Expand Down
Loading
Loading