From 3a7fd73b0c0f1cc7cda2cc6a0b7e30ec0714c6e7 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Mon, 22 Sep 2025 20:35:19 +0200 Subject: [PATCH 1/7] Beacon node as library Minimal support for running the beacon node as part of another application, for example `nimbus`. * move copyright out of version (set it per binary) * avoid allocations / logging in signal handler * expose `run` and `main` for others to call --- beacon_chain/buildinfo.nim | 18 +- beacon_chain/nimbus_beacon_node.nim | 227 ++++++++---------- beacon_chain/nimbus_binary_common.nim | 82 ++++--- beacon_chain/nimbus_light_client.nim | 10 +- beacon_chain/nimbus_signing_node.nim | 16 +- beacon_chain/nimbus_validator_client.nim | 16 +- .../spec/eth2_apis/rest_keymanager_calls.nim | 6 +- beacon_chain/spec/keystore.nim | 6 +- beacon_chain/winservice.nim | 6 +- research/fakeee.nim | 2 +- tests/test_keymanager_api.nim | 10 +- vendor/nim-chronicles | 2 +- vendor/nim-confutils | 2 +- 13 files changed, 216 insertions(+), 187 deletions(-) diff --git a/beacon_chain/buildinfo.nim b/beacon_chain/buildinfo.nim index ffa2139761..a50b5f89a9 100644 --- a/beacon_chain/buildinfo.nim +++ b/beacon_chain/buildinfo.nim @@ -25,9 +25,7 @@ proc gitFolderExists(path: string): bool {.compileTime.} = false const - compileYear = CompileDate[0 ..< 4] # YYYY-MM-DD (UTC) - copyrights* = - "Copyright (c) 2019-" & compileYear & " Status Research & Development GmbH" + compileYear* = CompileDate[0 ..< 4] # YYYY-MM-DD (UTC) GitRevisionOverride {.strdefine.} = "" @@ -40,8 +38,7 @@ template generateGitRevision*(repoPath: string): untyped = when GitRevisionOverride.len > 0: static: doAssert( - GitRevisionOverride.len == 8, - "GitRevisionOverride must consist of 8 characters", + GitRevisionOverride.len == 8, "GitRevisionOverride must consist of 8 characters" ) doAssert( GitRevisionOverride.allIt(it in HexDigits), @@ -53,14 +50,12 @@ template generateGitRevision*(repoPath: string): untyped = if gitFolderExists(repoPath): # only using git if the parent dir is a git repo. strip( - staticExec( - "git -C " & strutils.escape(repoPath) & " rev-parse --short=8 HEAD" - ) + staticExec("git -C " & strutils.escape(repoPath) & " rev-parse --short=8 HEAD") ) else: # otherwise we use revision number given by build system. # e.g. user download from release tarball, or Github zip download. - "00000000" + "00000000" func getNimGitHash(): string = const gitPrefix = "git hash: " @@ -79,5 +74,6 @@ func nimBanner*(): string = else: tmp[0] -declareGauge nimVersionGauge, "Nim version info", ["version", "nim_commit"], name = "nim_version" -nimVersionGauge.set(1, labelValues=[NimVersion, getNimGitHash()]) +declareGauge nimVersionGauge, + "Nim version info", ["version", "nim_commit"], name = "nim_version" +nimVersionGauge.set(1, labelValues = [NimVersion, getNimGitHash()]) diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 6d58875c48..6380c64fb2 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -8,7 +8,8 @@ {.push raises: [].} import - std/[os, random, terminal, times, exitprocs], + system/ansi_c, + std/[os, random, terminal, times], chronos, chronicles, metrics, metrics/chronos_httpserver, stew/[byteutils, io2], @@ -25,12 +26,9 @@ import ./sync/[sync_protocol, light_client_protocol, sync_overseer, validator_custody], ./validators/[keystore_management, beacon_validators], ./[ - beacon_node, beacon_node_light_client, deposits, + beacon_node, beacon_node_light_client, buildinfo, deposits, nimbus_binary_common, process_state, statusbar, trusted_node_sync, wallets] -when defined(posix): - import system/ansi_c - from std/algorithm import sort from std/sequtils import filterIt, mapIt, toSeq from libp2p/protocols/pubsub/gossipsub import @@ -706,16 +704,17 @@ const SlashingDbName = "slashing_protection" # changing this requires physical file rename as well or history is lost. -proc init*(T: type BeaconNode, - rng: ref HmacDrbgContext, - config: BeaconNodeConf, - metadata: Eth2NetworkMetadata): Future[BeaconNode] - {.async.} = +proc init*( + T: type BeaconNode, + rng: ref HmacDrbgContext, + config: BeaconNodeConf, + metadata: Eth2NetworkMetadata, + taskpool: Taskpool, +): Future[BeaconNode] {.async.} = var genesisState: ref ForkedHashedBeaconState = nil template cfg: auto = metadata.cfg - template eth1Network: auto = metadata.eth1Network if not(isDir(config.databaseDir)): # If database directory missing, we going to use genesis state to check @@ -747,21 +746,6 @@ proc init*(T: type BeaconNode, altair_fork_epoch = metadata.cfg.ALTAIR_FORK_EPOCH quit 1 - let taskpool = - try: - if config.numThreads < 0: - fatal "The number of threads --num-threads cannot be negative." - quit QuitFailure - elif config.numThreads == 0: - Taskpool.new(numThreads = min(countProcessors(), 16)) - else: - Taskpool.new(numThreads = config.numThreads) - except CatchableError as e: - fatal "Cannot start taskpool", err = e.msg - quit QuitFailure - - info "Threadpool started", numThreads = taskpool.numThreads - if metadata.genesis.kind == BakedIn: if config.genesisState.isSome: warn "The --genesis-state option has no effect on networks with built-in genesis state" @@ -838,9 +822,6 @@ proc init*(T: type BeaconNode, reindex = false, genesisState) - if config.finalizedCheckpointBlock.isSome: - warn "--finalized-checkpoint-block has been deprecated, ignoring" - let checkpointState = if config.finalizedCheckpointState.isSome: let checkpointStatePath = config.finalizedCheckpointState.get.string let tmp = try: @@ -971,10 +952,7 @@ proc init*(T: type BeaconNode, dag.checkWeakSubjectivityCheckpoint( config.weakSubjectivityCheckpoint.get, beaconClock) - let elManager = ELManager.new(engineApiUrls, eth1Network) - - if config.rpcEnabled.isSome: - warn "Nimbus's JSON-RPC server has been removed. This includes the --rpc, --rpc-port, and --rpc-address configuration options. https://nimbus.guide/rest-api.html shows how to enable and configure the REST Beacon API server which replaces it." + let elManager = ELManager.new(engineApiUrls, metadata.eth1Network) let restServer = if config.restEnabled: RestServerRef.init(config.restAddress, config.restPort, @@ -2466,7 +2444,51 @@ proc stop(node: BeaconNode) = node.db.close() notice "Databases closed" -proc run(node: BeaconNode) {.raises: [CatchableError].} = +proc initializeNetworking(node: BeaconNode) {.async.} = + node.installMessageValidators() + + info "Listening to incoming network requests" + await node.network.startListening() + + let addressFile = node.config.dataDir / "beacon_node.enr" + writeFile(addressFile, node.network.announcedENR.toURI) + + await node.network.start() + +type StopFuture = Future[void].Raising([CancelledError]) + +proc run*(node: BeaconNode, stopper: StopFuture) {.raises: [CatchableError].} = + let + head = node.dag.head + finalizedHead = node.dag.finalizedHead + genesisTime = node.beaconClock.fromNow(start_beacon_time(Slot 0)) + + notice "Starting beacon node", + version = fullVersionStr, + nimVersion = NimVersion, + enr = node.network.announcedENR.toURI, + peerId = $node.network.switch.peerInfo.peerId, + timeSinceFinalization = + node.beaconClock.now() - finalizedHead.slot.start_beacon_time(), + head = shortLog(head), + justified = shortLog(getStateField( + node.dag.headState, current_justified_checkpoint)), + finalized = shortLog(getStateField( + node.dag.headState, finalized_checkpoint)), + finalizedHead = shortLog(finalizedHead), + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + SPEC_VERSION, + dataDir = node.config.dataDir.string, + validators = node.attachedValidators[].count + + if genesisTime.inFuture: + notice "Waiting for genesis", genesisIn = genesisTime.offset + + waitFor node.initializeNetworking() + + node.elManager.start() + ProcessState.notifyRunning() if not isNil(node.restServer): @@ -2506,61 +2528,13 @@ proc run(node: BeaconNode) {.raises: [CatchableError].} = asyncSpawn runQueueProcessingLoop(node.blockProcessor) asyncSpawn runKeystoreCachePruningLoop(node.keystoreCache) - while not ProcessState.stopIt(notice("Shutting down", reason = it)): + while not ProcessState.stopIt(notice("Shutting down", reason = it)) and + (stopper == nil or not stopper.finished): poll() # time to say goodbye node.stop() -var gPidFile: string -proc createPidFile(filename: string) {.raises: [IOError].} = - writeFile filename, $os.getCurrentProcessId() - gPidFile = filename - addExitProc proc {.noconv.} = discard io2.removeFile(gPidFile) - -proc initializeNetworking(node: BeaconNode) {.async.} = - node.installMessageValidators() - - info "Listening to incoming network requests" - await node.network.startListening() - - let addressFile = node.config.dataDir / "beacon_node.enr" - writeFile(addressFile, node.network.announcedENR.toURI) - - await node.network.start() - -proc start*(node: BeaconNode) {.raises: [CatchableError].} = - let - head = node.dag.head - finalizedHead = node.dag.finalizedHead - genesisTime = node.beaconClock.fromNow(start_beacon_time(Slot 0)) - - notice "Starting beacon node", - version = fullVersionStr, - nimVersion = NimVersion, - enr = node.network.announcedENR.toURI, - peerId = $node.network.switch.peerInfo.peerId, - timeSinceFinalization = - node.beaconClock.now() - finalizedHead.slot.start_beacon_time(), - head = shortLog(head), - justified = shortLog(getStateField( - node.dag.headState, current_justified_checkpoint)), - finalized = shortLog(getStateField( - node.dag.headState, finalized_checkpoint)), - finalizedHead = shortLog(finalizedHead), - SLOTS_PER_EPOCH, - SECONDS_PER_SLOT, - SPEC_VERSION, - dataDir = node.config.dataDir.string, - validators = node.attachedValidators[].count - - if genesisTime.inFuture: - notice "Waiting for genesis", genesisIn = genesisTime.offset - - waitFor node.initializeNetworking() - - node.elManager.start() - node.run() func formatGwei(amount: Gwei): string = # TODO This is implemented in a quite a silly way. @@ -2705,7 +2679,13 @@ when not defined(windows): asyncSpawn statusBarUpdatesPollingLoop() -proc doRunBeaconNode(config: var BeaconNodeConf, rng: ref HmacDrbgContext) {.raises: [CatchableError].} = +proc doRunBeaconNode*( + config: var BeaconNodeConf, + rng: ref HmacDrbgContext, + taskpool: Taskpool, + stopper: StopFuture, +) {.raises: [CatchableError], gcsafe.} = + doAssert taskpool != nil info "Launching beacon node", version = fullVersionStr, bls_backend = $BLS_BACKEND, @@ -2713,18 +2693,6 @@ proc doRunBeaconNode(config: var BeaconNodeConf, rng: ref HmacDrbgContext) {.rai cmdParams = commandLineParams(), config - template ignoreDeprecatedOption(option: untyped): untyped = - if config.option.isSome: - warn "Config option is deprecated", - option = config.option.get - ignoreDeprecatedOption requireEngineAPI - ignoreDeprecatedOption safeSlotsToImportOptimistically - ignoreDeprecatedOption terminalTotalDifficultyOverride - ignoreDeprecatedOption optimistic - ignoreDeprecatedOption validatorMonitorTotals - ignoreDeprecatedOption web3ForcePolling - ignoreDeprecatedOption finalizedDepositTreeSnapshot - config.createDumpDirs() # There are no managed event loops in here, to do a graceful shutdown, but @@ -2749,16 +2717,14 @@ proc doRunBeaconNode(config: var BeaconNodeConf, rng: ref HmacDrbgContext) {.rai if ProcessState.stopIt(notice("Shutting down", reason = it)): return - let node = waitFor BeaconNode.init(rng, config, metadata) - - let metricsServer = (waitFor config.initMetricsServer()).valueOr: - return + let node = waitFor BeaconNode.init(rng, config, metadata, taskpool) # Nim GC metrics (for the main thread) will be collected in onSecond(), but # we disable piggy-backing on other metrics here. setSystemMetricsAutomaticUpdate(false) - node.metricsServer = metricsServer + node.metricsServer = (waitFor config.initMetricsServer()).valueOr: + return if ProcessState.stopIt(notice("Shutting down", reason = it)): return @@ -2769,9 +2735,9 @@ proc doRunBeaconNode(config: var BeaconNodeConf, rng: ref HmacDrbgContext) {.rai initStatusBar(node) if node.nickname != "": - dynamicLogScope(node = node.nickname): node.start() + dynamicLogScope(node = node.nickname): node.run(stopper) else: - node.start() + node.run(stopper) proc doRecord(config: BeaconNodeConf, rng: var HmacDrbgContext) {. raises: [CatchableError].} = @@ -2871,7 +2837,25 @@ proc handleStartUpCmd(config: var BeaconNodeConf) {.raises: [CatchableError].} = createPidFile(config.dataDir.string / "beacon_node.pid") ProcessState.setupStopHandlers() - doRunBeaconNode(config, rng) + if config.rpcEnabled.isSome: + warn "Nimbus's JSON-RPC server has been removed. This includes the --rpc, --rpc-port, and --rpc-address configuration options. https://nimbus.guide/rest-api.html shows how to enable and configure the REST Beacon API server which replaces it." + + template ignoreDeprecatedOption(option: untyped): untyped = + if config.option.isSome: + warn "Ignoring deprecated configuration option", + option = config.option.get + ignoreDeprecatedOption requireEngineAPI + ignoreDeprecatedOption safeSlotsToImportOptimistically + ignoreDeprecatedOption terminalTotalDifficultyOverride + ignoreDeprecatedOption optimistic + ignoreDeprecatedOption validatorMonitorTotals + ignoreDeprecatedOption web3ForcePolling + ignoreDeprecatedOption finalizedDepositTreeSnapshot + ignoreDeprecatedOption finalizedCheckpointBlock + + let taskpool = setupTaskpool(config.numThreads) + + doRunBeaconNode(config, rng, taskpool, nil) of BNStartUpCmd.deposits: doDeposits(config, rng[]) of BNStartUpCmd.wallets: doWallets(config, rng[]) of BNStartUpCmd.record: doRecord(config, rng[]) @@ -2899,8 +2883,13 @@ proc handleStartUpCmd(config: var BeaconNodeConf) {.raises: [CatchableError].} = db.close() # noinline to keep it in stack traces -proc main() {.noinline, raises: [CatchableError].} = - var config = makeBannerAndConfig(clientId, BeaconNodeConf) +proc main*() {.noinline, raises: [CatchableError].} = + const copyright = + "Copyright (c) 2019-" & compileYear & " Status Research & Development GmbH" + + var config = BeaconNodeConf.loadWithBanners(clientId, copyright, [specBanner]).valueOr: + stderr.writeLine error # Logging not yet set up + quit QuitFailure setupLogging(config.logLevel, config.logStdout, config.logFile) setupFileLimits() @@ -2910,36 +2899,30 @@ proc main() {.noinline, raises: [CatchableError].} = # permissions are insecure. quit QuitFailure - ## This Ctrl+C handler exits the program in non-graceful way. ## It's responsible for handling Ctrl+C in sub-commands such ## as `wallets *` and `deposits *`. In a regular beacon node ## run, it will be overwritten later with a different handler ## performing a graceful exit. proc exitImmediatelyOnCtrlC() {.noconv.} = - when defined(windows): - # workaround for https://github.com/nim-lang/Nim/issues/4057 - setupForeignThreadGc() - # in case a password prompt disabled echoing - resetStdin() - echo "" # If we interrupt during an interactive prompt, this - # will move the cursor to the next line - notice "Shutting down after having received SIGINT" - quit 0 + # No allocations in signal handler + cstdout.rawWrite("Shutting down after having received SIGINT / ctrl-c") + quit QuitSuccess setControlCHook(exitImmediatelyOnCtrlC) + # equivalent SIGTERM handler - when defined(posix): + when declared(ansi_c.SIGTERM): proc exitImmediatelyOnSIGTERM(signal: cint) {.noconv.} = - notice "Shutting down after having received SIGTERM" - quit 0 + # No allocations in signal handler + cstdout.rawWrite("Shutting down after having received SIGTERM") + quit QuitSuccess c_signal(ansi_c.SIGTERM, exitImmediatelyOnSIGTERM) when defined(windows): if config.runAsService: proc exitService() = ProcessState.scheduleStop("exitService") - establishWindowsService(clientId, - ["Ethereum consensus spec v" & SPEC_VERSION], + establishWindowsService(clientId, copyright, [specBanner], "nimbus_beacon_node", BeaconNodeConf, handleStartUpCmd, exitService) else: diff --git a/beacon_chain/nimbus_binary_common.nim b/beacon_chain/nimbus_binary_common.nim index 01f9e6eb58..558d0e4468 100644 --- a/beacon_chain/nimbus_binary_common.nim +++ b/beacon_chain/nimbus_binary_common.nim @@ -11,17 +11,23 @@ import # Standard library - std/[os, tables, terminal, typetraits], + std/[cpuinfo, exitprocs, os, tables, terminal, typetraits], # Nimble packages chronos, confutils, presto, toml_serialization, metrics, chronicles, chronicles/helpers as chroniclesHelpers, chronicles/topics_registry, - stew/io2, metrics/chronos_httpserver, + stew/io2, metrics/chronos_httpserver, taskpools, # Local modules ./spec/keystore, ./buildinfo +from ./spec/datatypes/base import SPEC_VERSION + +const specBanner* = "Ethereum consensus spec v" & SPEC_VERSION + +from system/ansi_c import c_malloc + when defaultChroniclesStream.outputs.type.arity == 2: from ./filepath import secureCreatePath @@ -153,18 +159,40 @@ proc setupLogging*( updateLogLevel(logLevel) except ValueError as err: try: - stderr.write "Invalid value for --log-level. " & err.msg + stderr.writeLine "Invalid value for --log-level. " & err.msg except IOError: echo "Invalid value for --log-level. " & err.msg quit 1 -proc makeBannerAndConfig*( - helpBanner: string, versions: openArray[string], - environment: openArray[string], +proc setupTaskpool*(numThreads: int): Taskpool = + let taskpool = + try: + if numThreads < 0: + fatal "The number of threads --num-threads cannot be negative." + quit QuitFailure + elif numThreads == 0: + Taskpool.new(numThreads = min(countProcessors(), 16)) + else: + Taskpool.new(numThreads = numThreads) + except CatchableError as e: + fatal "Cannot start taskpool", err = e.msg + quit QuitFailure + + info "Taskpool started", numThreads = taskpool.numThreads + + taskpool + +proc loadWithBanners*( ConfType: type, + helpBanner, copyright: string, + versions: openArray[string], + ignoreUnknown = false, + environment: openArray[string] = [], ): Result[ConfType, string] = let - version = helpBanner & "\p" & copyrights & "\p\p" & (@versions & @[nimBanner()]).join("\p") + version = + [helpBanner, copyright].join("\p") & "\p\p" & + (@versions & @[nimBanner()]).join("\p") cmdLine = if len(environment) == 0: @@ -179,6 +207,7 @@ proc makeBannerAndConfig*( version = version, # what --version outputs copyrightBanner = helpBanner, # what is shown on top of --help cmdLine = cmdLine, + ignoreUnknown = ignoreUnknown, secondarySources = proc( config: ConfType, sources: ref SecondarySources ) {.raises: [ConfigurationError], gcsafe.} = @@ -205,16 +234,6 @@ proc makeBannerAndConfig*( {.pop.} ok(config) -template makeBannerAndConfig*(helpBanner: string, ConfType: type): auto = - makeBannerAndConfig( - helpBanner, ["Ethereum consensus spec v" & SPEC_VERSION], [], ConfType - ).valueOr: - try: - stderr.write(error) - except IOError: - discard - quit QuitFailure - proc checkIfShouldStopAtEpoch*(scheduledSlot: Slot, stopAtEpoch: uint64): bool = # Offset backwards slightly to allow this epoch's finalization check to occur @@ -237,17 +256,14 @@ proc resetStdin*() = attrs.c_lflag = attrs.c_lflag or Cflag(ECHO) discard fd.tcSetAttr(TCSANOW, attrs.addr) -proc runKeystoreCachePruningLoop*(cache: KeystoreCacheRef) {.async.} = - while true: - let exitLoop = - try: - await sleepAsync(60.seconds) - false - except CatchableError: - cache.clear() - true - if exitLoop: break - cache.pruneExpiredKeys() +proc runKeystoreCachePruningLoop*(cache: KeystoreCacheRef) {.async: (raises: []).} = + try: + while true: + await sleepAsync(60.seconds) + cache.pruneExpiredKeys() + except CancelledError: + discard + cache.clear() proc sleepAsync*(t: TimeDiff): Future[void] {. async: (raises: [CancelledError], raw: true).} = @@ -434,3 +450,13 @@ proc defaultDataDir*(namespace, network: string): string = dir = dir / network dir + +proc createPidFile*(filename: string) {.raises: [IOError].} = + var pidFile {.global.}: cstring # avoid gc + doAssert pidFile.len == 0, "PID file must only be created once" + + writeFile filename, $os.getCurrentProcessId() + pidFile = cast[cstring](c_malloc(csize_t(filename.len + 1))) + copyMem(pidFile, cstring(filename), filename.len + 1) + + addExitProc proc {.noconv.} = discard io2.removeFile($pidFile) diff --git a/beacon_chain/nimbus_light_client.nim b/beacon_chain/nimbus_light_client.nim index 8dd8793e99..059c4b7005 100644 --- a/beacon_chain/nimbus_light_client.nim +++ b/beacon_chain/nimbus_light_client.nim @@ -27,9 +27,15 @@ from ./gossip_processing/eth2_processor import toValidationResult # noinline to keep it in stack traces proc main() {.noinline, raises: [CatchableError].} = ProcessState.setupStopHandlers() + const + banner = "Nimbus light client " & fullVersionStr + copyright = + "Copyright (c) 2022-" & compileYear & " Status Research & Development GmbH" + + var config = LightClientConf.loadWithBanners(banner, copyright, [specBanner]).valueOr: + stderr.writeLine error # Logging not yet set up + quit QuitFailure - var config = makeBannerAndConfig( - "Nimbus light client " & fullVersionStr, LightClientConf) setupLogging(config.logLevel, config.logStdout, config.logFile) notice "Launching light client", diff --git a/beacon_chain/nimbus_signing_node.nim b/beacon_chain/nimbus_signing_node.nim index 56efae642f..bb553bba1a 100644 --- a/beacon_chain/nimbus_signing_node.nim +++ b/beacon_chain/nimbus_signing_node.nim @@ -1,5 +1,5 @@ # nimbus_signing_node -# Copyright (c) 2018-2025 Status Research & Development GmbH +# Copyright (c) 2021-2025 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -16,7 +16,7 @@ import "."/spec/datatypes/[base, altair, phase0], "."/spec/[crypto, digest, network, signatures, forks], "."/spec/eth2_apis/[rest_types, eth2_rest_serialization], "."/rpc/rest_constants, - "."/[conf, version, nimbus_binary_common], + "."/[buildinfo, conf, version, nimbus_binary_common], "."/validators/[keystore_management, validator_pool] const @@ -495,9 +495,17 @@ proc runSigningNode(config: SigningNodeConf) {.async: (raises: []).} = # noinline to keep it in stack traces proc main() {.noinline, raises: [CatchableError].} = - let config = - makeBannerAndConfig("Nimbus signing node " & fullVersionStr, SigningNodeConf) + const + banner = "Nimbus signing node " & fullVersionStr + copyright = + "Copyright (c) 2021-" & compileYear & " Status Research & Development GmbH" + + let config = SigningNodeConf.loadWithBanners(banner, copyright, [specBanner]).valueOr: + stderr.writeLine error # Logging not yet set up + quit QuitFailure + setupLogging(config.logLevel, config.logStdout, config.logFile) + waitFor runSigningNode(config) when isMainModule: diff --git a/beacon_chain/nimbus_validator_client.nim b/beacon_chain/nimbus_validator_client.nim index 1ef30fcb73..8f3825a272 100644 --- a/beacon_chain/nimbus_validator_client.nim +++ b/beacon_chain/nimbus_validator_client.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2018-2025 Status Research & Development GmbH +# Copyright (c) 2020-2025 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -12,7 +12,8 @@ import ./rpc/rest_key_management_api, ./validator_client/[ common, fallback_service, duties_service, fork_service, block_service, - doppelganger_service, attestation_service, sync_committee_service] + doppelganger_service, attestation_service, sync_committee_service], + ./buildinfo const PREGENESIS_EPOCHS_COUNT = 1 @@ -638,9 +639,15 @@ proc runValidatorClient*( # noinline to keep it in stack traces proc main() {.noinline, raises: [CatchableError].} = + const + banner = "Nimbus validator client " & fullVersionStr + copyright = + "Copyright (c) 2020-" & compileYear & " Status Research & Development GmbH" + let - config = makeBannerAndConfig("Nimbus validator client " & fullVersionStr, - ValidatorClientConf) + config = ValidatorClientConf.loadWithBanners(banner, copyright, [specBanner]).valueOr: + stderr.writeLine error # Logging not yet set up + quit QuitFailure # Single RNG instance for the application - will be seeded on construction # and avoid using system resources (such as urandom) after that @@ -648,6 +655,7 @@ proc main() {.noinline, raises: [CatchableError].} = setupLogging(config.logLevel, config.logStdout, config.logFile) setupFileLimits() + waitFor runValidatorClient(config, rng) when isMainModule: diff --git a/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim b/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim index a1fb047361..3f033e7fb1 100644 --- a/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim +++ b/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim @@ -16,9 +16,9 @@ import export client, rest_types, eth2_rest_serialization, rest_keymanager_types -UUID.serializesAsBaseIn RestJson -KeyPath.serializesAsBaseIn RestJson -WalletName.serializesAsBaseIn RestJson +UUID.serializesAsBase RestJson +KeyPath.serializesAsBase RestJson +WalletName.serializesAsBase RestJson proc raiseKeymanagerGenericError*(resp: RestPlainResponse) {. noreturn, raises: [RestError].} = diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index 631894fbb0..beee403234 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -264,9 +264,9 @@ const KeystoreCachePruningTime* = 5.minutes -UUID.serializesAsBaseIn Json -KeyPath.serializesAsBaseIn Json -WalletName.serializesAsBaseIn Json +UUID.serializesAsBase Json +KeyPath.serializesAsBase Json +WalletName.serializesAsBase Json ChecksumFunctionKind.serializesAsTextInJson CipherFunctionKind.serializesAsTextInJson diff --git a/beacon_chain/winservice.nim b/beacon_chain/winservice.nim index 4f2b626d51..415dde75ae 100644 --- a/beacon_chain/winservice.nim +++ b/beacon_chain/winservice.nim @@ -106,7 +106,7 @@ when defined(windows): proc reportServiceStatusSuccess*() = reportServiceStatus(SERVICE_RUNNING, NO_ERROR, 0) - template establishWindowsService*(argHelpBanner: string, + template establishWindowsService*(argHelpBanner, argCopyright: string, argVersions: openArray[string], argServiceName: string, argConfigType: untyped, @@ -148,8 +148,8 @@ when defined(windows): reportServiceStatus(SERVICE_STOPPED, ERROR_INVALID_PARAMETER, 0) quit QuitFailure - var config = makeBannerAndConfig(argHelpBanner, argVersions, - environment, argConfigType).valueOr: + var config = loadWithBanners(argConfigType, argHelpBanner, argCopyright, + argVersions, false, environment).valueOr: reportServiceStatus(SERVICE_STOPPED, ERROR_BAD_CONFIGURATION, 0) quit QuitFailure diff --git a/research/fakeee.nim b/research/fakeee.nim index d6280a07ec..b4d2509277 100644 --- a/research/fakeee.nim +++ b/research/fakeee.nim @@ -36,7 +36,7 @@ proc setupEngineAPI*(server: RpcServer) = ) # https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.4/src/engine/paris.md#engine_getpayloadv1 - server.rpc("engine_getPayloadV1") do(payloadId: PayloadID) -> ExecutionPayloadV1: + server.rpc("engine_getPayloadV1") do(payloadId: Bytes8) -> ExecutionPayloadV1: info "engine_getPayloadV1", id = payloadId.toHex diff --git a/tests/test_keymanager_api.nim b/tests/test_keymanager_api.nim index f0a9ebc788..b643114372 100644 --- a/tests/test_keymanager_api.nim +++ b/tests/test_keymanager_api.nim @@ -365,9 +365,11 @@ proc initBeaconNode(basePort: int): Future[BeaconNode] {.async: (raises: []).} = raiseAssert exc.msg try: - let metadata = - loadEth2NetworkMetadata(dataDir).expect("Metadata is compatible") - await BeaconNode.init(rng, runNodeConf, metadata) + let + metadata = loadEth2NetworkMetadata(dataDir).expect("Metadata is compatible") + taskpool = Taskpool.new() + + await BeaconNode.init(rng, runNodeConf, metadata, taskpool) except CatchableError as exc: raiseAssert exc.msg @@ -2054,7 +2056,7 @@ proc main(basePort: int) {.async.} = asyncSpawn delayedTests(basePort, node.attachedValidators, node.keymanagerHost) - node.start() + node.run(nil) let basePortStr = os.getEnv("NIMBUS_TEST_KEYMANAGER_BASE_PORT", $defaultBasePort) diff --git a/vendor/nim-chronicles b/vendor/nim-chronicles index 14cb79f470..67da11265f 160000 --- a/vendor/nim-chronicles +++ b/vendor/nim-chronicles @@ -1 +1 @@ -Subproject commit 14cb79f470f9eec6fbdcd4f572b437a547b7622c +Subproject commit 67da11265f8defa5b2c7d2f17ab5b1637c6a1d41 diff --git a/vendor/nim-confutils b/vendor/nim-confutils index 7a256534e6..eec84f4b5e 160000 --- a/vendor/nim-confutils +++ b/vendor/nim-confutils @@ -1 +1 @@ -Subproject commit 7a256534e60bbd69921c44c4c45679d1b3f6f494 +Subproject commit eec84f4b5e8ea322f42b9c4008e549825c310a8d From a2ea8af73c25bd1a43617ba012c7172d6521d0d0 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Wed, 24 Sep 2025 18:48:15 +0200 Subject: [PATCH 2/7] fix missing logging setup in windows service --- beacon_chain/nimbus_beacon_node.nim | 2 +- beacon_chain/nimbus_binary_common.nim | 2 +- beacon_chain/winservice.nim | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 6380c64fb2..e4d07e9c83 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -2894,7 +2894,7 @@ proc main*() {.noinline, raises: [CatchableError].} = setupLogging(config.logLevel, config.logStdout, config.logFile) setupFileLimits() - if not(checkAndCreateDataDir(string(config.dataDir))): + if not (checkAndCreateDataDir(string(config.dataDir))): # We are unable to access/create data folder or data folder's # permissions are insecure. quit QuitFailure diff --git a/beacon_chain/nimbus_binary_common.nim b/beacon_chain/nimbus_binary_common.nim index 558d0e4468..ce7f226d51 100644 --- a/beacon_chain/nimbus_binary_common.nim +++ b/beacon_chain/nimbus_binary_common.nim @@ -85,7 +85,7 @@ proc setupFileLimits*() = warn "Cannot increase open file limit", err = osErrorMsg(error) proc setupLogging*( - logLevel: string, stdoutKind: StdoutLogKind, logFile: Option[OutFile]) = + logLevel: string, stdoutKind: StdoutLogKind, logFile = none(OutFile)) = # In the cfg file for nimbus, we create two formats: textlines and json. # Here, we either write those logs to an output, or not, depending on the # given configuration. diff --git a/beacon_chain/winservice.nim b/beacon_chain/winservice.nim index 415dde75ae..3208ea1ccf 100644 --- a/beacon_chain/winservice.nim +++ b/beacon_chain/winservice.nim @@ -153,6 +153,8 @@ when defined(windows): reportServiceStatus(SERVICE_STOPPED, ERROR_BAD_CONFIGURATION, 0) quit QuitFailure + setupLogging(config.logLevel, config.logStdout, config.logFile) + try: argEntryPoint(config) info "Service thread stopped" From 71e0f7f532705e8eef8755bf94c7a988e8813428 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Thu, 25 Sep 2025 09:36:02 +0200 Subject: [PATCH 3/7] nicer poll loop --- beacon_chain/nimbus_beacon_node.nim | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index e4d07e9c83..6b8b899b75 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -2528,9 +2528,14 @@ proc run*(node: BeaconNode, stopper: StopFuture) {.raises: [CatchableError].} = asyncSpawn runQueueProcessingLoop(node.blockProcessor) asyncSpawn runKeystoreCachePruningLoop(node.keystoreCache) - while not ProcessState.stopIt(notice("Shutting down", reason = it)) and - (stopper == nil or not stopper.finished): - poll() + while true: + if (let reason = ProcessState.stopping(); reason.isSome()): + notice "Shutting down", reason = reason[] + break + if stopper != nil or stopper.finished(): + break + + chronos.poll() # time to say goodbye node.stop() From 1e6237603320097fb5305fcee961f55c1b129b93 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Thu, 25 Sep 2025 10:02:22 +0200 Subject: [PATCH 4/7] writePanicLine helper --- beacon_chain/nimbus_beacon_node.nim | 2 +- beacon_chain/nimbus_binary_common.nim | 10 ++++++++++ beacon_chain/nimbus_light_client.nim | 2 +- beacon_chain/nimbus_signing_node.nim | 2 +- beacon_chain/nimbus_validator_client.nim | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 6b8b899b75..6c51bbdfad 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -2893,7 +2893,7 @@ proc main*() {.noinline, raises: [CatchableError].} = "Copyright (c) 2019-" & compileYear & " Status Research & Development GmbH" var config = BeaconNodeConf.loadWithBanners(clientId, copyright, [specBanner]).valueOr: - stderr.writeLine error # Logging not yet set up + writePanicLine error # Logging not yet set up quit QuitFailure setupLogging(config.logLevel, config.logStdout, config.logFile) diff --git a/beacon_chain/nimbus_binary_common.nim b/beacon_chain/nimbus_binary_common.nim index ce7f226d51..fcfbcb1454 100644 --- a/beacon_chain/nimbus_binary_common.nim +++ b/beacon_chain/nimbus_binary_common.nim @@ -84,6 +84,16 @@ proc setupFileLimits*() = setMaxOpenFiles2(16384).isOkOr: warn "Cannot increase open file limit", err = osErrorMsg(error) +proc writePanicLine*(v: varargs[string, `$`]) = + ## Attempt writing text to stderr, ignoring errors if it fails - useful when + ## logging has not yet been set up + try: + for s in v: + stderr.write(s) + s.write("\p") + except IOError: + discard # Nothing to do.. + proc setupLogging*( logLevel: string, stdoutKind: StdoutLogKind, logFile = none(OutFile)) = # In the cfg file for nimbus, we create two formats: textlines and json. diff --git a/beacon_chain/nimbus_light_client.nim b/beacon_chain/nimbus_light_client.nim index 059c4b7005..31276d19f6 100644 --- a/beacon_chain/nimbus_light_client.nim +++ b/beacon_chain/nimbus_light_client.nim @@ -33,7 +33,7 @@ proc main() {.noinline, raises: [CatchableError].} = "Copyright (c) 2022-" & compileYear & " Status Research & Development GmbH" var config = LightClientConf.loadWithBanners(banner, copyright, [specBanner]).valueOr: - stderr.writeLine error # Logging not yet set up + writePanicLine error # Logging not yet set up quit QuitFailure setupLogging(config.logLevel, config.logStdout, config.logFile) diff --git a/beacon_chain/nimbus_signing_node.nim b/beacon_chain/nimbus_signing_node.nim index bb553bba1a..438f9542ce 100644 --- a/beacon_chain/nimbus_signing_node.nim +++ b/beacon_chain/nimbus_signing_node.nim @@ -501,7 +501,7 @@ proc main() {.noinline, raises: [CatchableError].} = "Copyright (c) 2021-" & compileYear & " Status Research & Development GmbH" let config = SigningNodeConf.loadWithBanners(banner, copyright, [specBanner]).valueOr: - stderr.writeLine error # Logging not yet set up + writePanicLine error # Logging not yet set up quit QuitFailure setupLogging(config.logLevel, config.logStdout, config.logFile) diff --git a/beacon_chain/nimbus_validator_client.nim b/beacon_chain/nimbus_validator_client.nim index 8f3825a272..c894a7fc76 100644 --- a/beacon_chain/nimbus_validator_client.nim +++ b/beacon_chain/nimbus_validator_client.nim @@ -646,7 +646,7 @@ proc main() {.noinline, raises: [CatchableError].} = let config = ValidatorClientConf.loadWithBanners(banner, copyright, [specBanner]).valueOr: - stderr.writeLine error # Logging not yet set up + writePanicLine error # Logging not yet set up quit QuitFailure # Single RNG instance for the application - will be seeded on construction From 6ae8b4d2f2f0a5bb162bb3333e35aacaa07a863a Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Thu, 25 Sep 2025 10:53:23 +0200 Subject: [PATCH 5/7] oops --- beacon_chain/nimbus_binary_common.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_chain/nimbus_binary_common.nim b/beacon_chain/nimbus_binary_common.nim index fcfbcb1454..720624fb18 100644 --- a/beacon_chain/nimbus_binary_common.nim +++ b/beacon_chain/nimbus_binary_common.nim @@ -90,7 +90,7 @@ proc writePanicLine*(v: varargs[string, `$`]) = try: for s in v: stderr.write(s) - s.write("\p") + stderr.write("\p") except IOError: discard # Nothing to do.. From e6cde0e54f44bc7be0996e046367b6a844077d63 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Thu, 25 Sep 2025 12:36:10 +0200 Subject: [PATCH 6/7] oops --- beacon_chain/nimbus_beacon_node.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 6c51bbdfad..648218b879 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -2532,7 +2532,7 @@ proc run*(node: BeaconNode, stopper: StopFuture) {.raises: [CatchableError].} = if (let reason = ProcessState.stopping(); reason.isSome()): notice "Shutting down", reason = reason[] break - if stopper != nil or stopper.finished(): + if stopper != nil and stopper.finished(): break chronos.poll() From 5256a7614931940f52d5a9a7e84d43959ffa9178 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Thu, 25 Sep 2025 14:13:06 +0200 Subject: [PATCH 7/7] don't leak exceptions --- beacon_chain/nimbus_binary_common.nim | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beacon_chain/nimbus_binary_common.nim b/beacon_chain/nimbus_binary_common.nim index 720624fb18..500c205f18 100644 --- a/beacon_chain/nimbus_binary_common.nim +++ b/beacon_chain/nimbus_binary_common.nim @@ -206,7 +206,10 @@ proc loadWithBanners*( cmdLine = if len(environment) == 0: - commandLineParams() + try: + commandLineParams() + except OSError as exc: + return err(exc.msg) else: @environment