Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fee7361
eth2: bump, remove holesky
arnetheduck Sep 24, 2025
2be298a
bump fixes
arnetheduck Oct 23, 2025
ea1f60b
Unified Nimbus node
arnetheduck Oct 23, 2025
ec1d3bf
fix Nix environment
0xc1c4da Oct 23, 2025
fce3143
Merge remote-tracking branch 'origin/master' into wip-nimbus
arnetheduck Oct 23, 2025
27d8882
fix missing sync horizon setting
arnetheduck Oct 23, 2025
d06d669
skip copyright year check
arnetheduck Oct 23, 2025
15e8a2c
warn on usage of engine api param
arnetheduck Oct 23, 2025
82c910d
flag updates from eth2
arnetheduck Oct 24, 2025
449336a
CL might not have head block on checkpoint sync
arnetheduck Oct 24, 2025
0d4921e
oops log is normal, after all
arnetheduck Oct 24, 2025
ad8b7e4
fix Nix environment
0xc1c4da Oct 23, 2025
632a4c3
Merge remote-tracking branch 'origin/master' into wip-nimbus
arnetheduck Oct 23, 2025
4f2f7c2
fix missing sync horizon setting
arnetheduck Oct 23, 2025
07616e1
skip copyright year check
arnetheduck Oct 23, 2025
4a5a1c0
warn on usage of engine api param
arnetheduck Oct 23, 2025
c03b44e
flag updates from eth2
arnetheduck Oct 24, 2025
fa61370
CL might not have head block on checkpoint sync
arnetheduck Oct 24, 2025
975fa09
oops log is normal, after all
arnetheduck Oct 24, 2025
ea58b6c
modern nix toolchain
0xc1c4da Oct 24, 2025
f7bf531
Merge branch 'wip-nimbus' of github.com:status-im/nimbus-eth1 into wi…
arnetheduck Oct 24, 2025
d42fc9b
ignore some more
arnetheduck Oct 24, 2025
e58a25b
add missing launch info
arnetheduck Oct 24, 2025
36d0ce8
fix launch info
arnetheduck Oct 24, 2025
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ tests/fixtures/eest_static
tests/fixtures/eest_stable
tests/fixtures/eest_develop
tests/fixtures/eest_devnet

execution_chain/nimbus
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,9 @@ nimbus_execution_client: | build deps rocksdb
check_revision: nimbus_execution_client
scripts/check_revision.sh

nimbus: nimbus_execution_client
echo "The nimbus target is deprecated and will soon change meaning, use 'nimbus_execution_client' instead"
nimbus: | build deps rocksdb
echo -e $(BUILD_MSG) "build/nimbus" && \
$(ENV_SCRIPT) nim c $(NIM_PARAMS) -d:chronicles_log_level=TRACE -o:build/nimbus "execution_chain/nimbus.nim"

# symlink
nimbus.nims:
Expand Down
40 changes: 27 additions & 13 deletions config.nims
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,20 @@ if defined(windows):
#
if defined(disableMarchNative):
if defined(i386) or defined(amd64):
switch("passC", "-mssse3")
switch("passL", "-mssse3")
if defined(macosx):
# https://support.apple.com/en-us/105113
# "macOS Sonoma is compatible with these computers" lists current oldest
# supported x86 models, all of which have Kaby Lake or newer CPUs.
switch("passC", "-march=skylake -mtune=generic")
switch("passL", "-march=skylake -mtune=generic")
else:
if defined(marchOptimized):
# https://github.com/status-im/nimbus-eth2/blob/stable/docs/cpu_features.md#bmi2--adx
switch("passC", "-march=broadwell -mtune=generic")
switch("passL", "-march=broadwell -mtune=generic")
else:
switch("passC", "-mssse3")
switch("passL", "-mssse3")
elif defined(macosx) and defined(arm64):
# Apple's Clang can't handle "-march=native" on M1: https://github.com/status-im/nimbus-eth2/issues/2758
switch("passC", "-mcpu=apple-m1")
Expand All @@ -101,33 +113,31 @@ elif defined(linux) and defined(arm64):
else:
switch("passC", "-march=native")
switch("passL", "-march=native")
if defined(windows):
if defined(i386) or defined(amd64):
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65782
# ("-fno-asynchronous-unwind-tables" breaks Nim's exception raising, sometimes)
# For non-Windows targets, https://github.com/bitcoin-core/secp256k1/issues/1623
# also suggests disabling the same flag to address Ubuntu 22.04/recent AMD CPUs.
switch("passC", "-mno-avx512f")
switch("passL", "-mno-avx512f")

# Omitting frame pointers in nim breaks the GC:
# omitting frame pointers in nim breaks the GC
# https://github.com/nim-lang/Nim/issues/10625
if not defined(windows):
# ...except on Windows where the Nim bug doesn't manifest and the option
# crashes GCC in some Mingw-w64 versions:
# https://sourceforge.net/p/mingw-w64/bugs/880/
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=86593
switch("passC", "-fno-omit-frame-pointer")
switch("passL", "-fno-omit-frame-pointer")
switch("passC", "-fno-omit-frame-pointer")
switch("passL", "-fno-omit-frame-pointer")

--threads:on
--opt:speed
--mm:refc
--excessiveStackTrace:on
# enable metric collection
--define:metrics
# for heap-usage-by-instance-type metrics and object base-type strings
--define:nimTypeNames
--styleCheck:usages
--styleCheck:error
--mm:refc

switch("define", "nim_compiler_path=" & currentDir & "env.sh nim")
switch("define", "withoutPCRE")

when not defined(disable_libbacktrace):
Expand Down Expand Up @@ -188,7 +198,6 @@ when not defined(use_system_rocksdb) and not defined(windows):
# Unfortunately this is filename based instead of path-based
# Assumes GCC

# -fomit-frame-pointer for https://github.com/status-im/nimbus-eth1/issues/2127
put("secp256k1.always", "-fno-lto -fomit-frame-pointer")

# ############################################################
Expand All @@ -206,6 +215,11 @@ put("secp256k1.always", "-fno-lto -fomit-frame-pointer")
put("server.always", "-fno-lto")
put("assembly.always", "-fno-lto")

# Secp256k1
# -fomit-frame-pointer for:
# https://github.com/status-im/nimbus-eth1/issues/2127
# https://github.com/status-im/nimbus-eth2/issues/6324
put("secp256k1.always", "-fno-lto -fomit-frame-pointer")

# BearSSL - only RNGs
put("aesctr_drbg.always", "-fno-lto")
Expand Down
10 changes: 5 additions & 5 deletions default.nix
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
let
nixpkgsFn = import (fetchTarball {
url = https://github.com/NixOS/nixpkgs/archive/85f3d86bea70fe0d76a7e3520966c58604f8e5e9.tar.gz;
sha256 = "1iacnd7ym6c5s9vizyiff0lmxjrlgh5gpya88mlavgvdppkcaiyn";
});
# nixpkgsFn = import (fetchTarball {
# url = https://github.com/NixOS/nixpkgs/archive/85f3d86bea70fe0d76a7e3520966c58604f8e5e9.tar.gz;
# sha256 = "1iacnd7ym6c5s9vizyiff0lmxjrlgh5gpya88mlavgvdppkcaiyn";
# });

# nixcrpkgs = import (fetchTarball {
# url = https://github.com/DavidEGrayson/nixcrpkgs/archive/606e5fac74204643c8ca48dd73ce239b2f821d69.tar.gz;
# sha256 = "19dn7i200xsv8s92kxymv3nd87jncmp3ki8pw77v2rxfvn8ldg34";
# }) {};

nixpkgs = nixpkgsFn {};
nixpkgs = import <nixpkgs>;

targets = {
windows = {
Expand Down
2 changes: 1 addition & 1 deletion execution_chain/beacon/api_handler/api_newpayload.nim
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ proc newPayload*(ben: BeaconEngineRef,
# If we already have the block locally, ignore the entire execution and just
# return a fake success.
if chain.haveBlockAndState(blockHash):
notice "Ignoring already known beacon payload",
debug "Ignoring already known beacon payload",
number = header.number, hash = blockHash.short
return validStatus(blockHash)

Expand Down
10 changes: 8 additions & 2 deletions execution_chain/conf.nim
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import
./common/chain_config,
./db/opts

export net, defs, jsdefs, jsnet, nimbus_binary_common, options
export net, defs, jsdefs, jsnet, nat_toml, nimbus_binary_common, options

const NimbusCopyright* =
"Copyright (c) 2018-" & compileYear & " Status Research & Development GmbH"
Expand All @@ -55,7 +55,7 @@ const
defaultMetricsServerPort = 9093
defaultHttpPort = 8545
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.4/src/engine/authentication.md#jwt-specifications
defaultEngineApiPort = 8551
defaultEngineApiPort* = 8551
defaultAdminListenAddress = (static parseIpAddress("127.0.0.1"))
defaultAdminListenAddressDesc = $defaultAdminListenAddress & ", meaning local host only"
logLevelDesc = getLogLevels()
Expand Down Expand Up @@ -472,6 +472,12 @@ type
defaultValueDesc: "\"jwt.hex\" in the data directory (see --data-dir)"
name: "jwt-secret" .}: Option[InputFile]

jwtSecretValue* {.
hidden
desc: "Hex string with jwt secret"
defaultValueDesc: "\"jwt.hex\" in the data directory (see --data-dir)"
name: "debug-jwt-secret-value" .}: Option[string]

beaconSyncTarget* {.
hidden
desc: "Manually set the initial sync target specified by its 32 byte" &
Expand Down
213 changes: 213 additions & 0 deletions execution_chain/el_sync.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# Nimbus
# Copyright (c) 2024-2025 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

## Consensus to execution syncer prototype based on nrpc

{.push raises: [].}

import
chronos,
chronicles,
web3,
web3/[engine_api, primitives, conversions],
beacon_chain/consensus_object_pools/blockchain_dag,
beacon_chain/el/[el_manager, engine_api_conversions],
beacon_chain/spec/[forks, presets, state_transition_block]

logScope:
topics = "elsync"

proc getForkedBlock(dag: ChainDAGRef, slot: Slot): Opt[ForkedTrustedSignedBeaconBlock] =
let bsi = ?dag.getBlockIdAtSlot(slot)
if bsi.isProposed():
dag.getForkedBlock(bsi.bid)
else:
Opt.none(ForkedTrustedSignedBeaconBlock)

proc blockNumber(blck: ForkedTrustedSignedBeaconBlock): uint64 =
withBlck(blck):
when consensusFork >= ConsensusFork.Bellatrix and consensusFork < ConsensusFork.Gloas:
forkyBlck.message.body.execution_payload.block_number
else:
0'u64

# Load the network configuration based on the network id
proc loadNetworkConfig(cfg: RuntimeConfig): (uint64, uint64) =
case cfg.CONFIG_NAME
of "mainnet":
(15537393'u64, 4700013'u64)
of "sepolia":
(1450408'u64, 115193'u64)
of "holesky", "hoodi":
(0'u64, 0'u64)
else:
notice "Loading custom network, assuming post-merge"
(0'u64, 0'u64)

# Slot Finding Mechanism
# First it sets the initial lower bound to `firstSlotAfterMerge` + number of blocks after Era1
# Then it iterates over the slots to find the current slot number, along with reducing the
# search space by calculating the difference between the `blockNumber` and the `block_number` from the executionPayload
# of the slot, then adding the difference to the importedSlot. This pushes the lower bound more,
# making the search way smaller
proc findSlot(
dag: ChainDAGRef,
elBlockNumber: uint64,
lastEra1Block: uint64,
firstSlotAfterMerge: uint64,
): Opt[uint64] =
var importedSlot = (elBlockNumber - lastEra1Block) + firstSlotAfterMerge + 1
debug "Finding slot number corresponding to block", elBlockNumber, importedSlot

var clNum = 0'u64
while clNum < elBlockNumber:
# Check if we can get the block id - if not, this part of the chain is not
# available from the CL
let bsi = ?dag.getBlockIdAtSlot(Slot(importedSlot))

if not bsi.isProposed:
importedSlot += 1
continue # Empty slot

let blck = dag.getForkedBlock(bsi.bid).valueOr:
return # Block unavailable

clNum = blck.blockNumber
# on the first iteration, the arithmetic helps skip the gap that has built
# up due to empty slots - for all subsequent iterations, except the last,
# we'll go one step at a time
# iteration so that we don't start at "one slot early"
importedSlot += max(elBlockNumber - clNum, 1)

Opt.some importedSlot

proc syncToEngineApi*(dag: ChainDAGRef, url: EngineApiUrl) {.async.} =
# Takes blocks from the CL and sends them to the EL - the attempt is made
# optimistically until something unexpected happens (reorg etc) at which point
# the process ends

let
# Create the client for the engine api
# And exchange the capabilities for a test communication
web3 = await url.newWeb3()
rpcClient = web3.provider
(lastEra1Block, firstSlotAfterMerge) = dag.cfg.loadNetworkConfig()

defer:
try:
await web3.close()
except:
discard

# Load the EL state detials and create the beaconAPI client
var elBlockNumber = uint64(await rpcClient.eth_blockNumber())

# Check for pre-merge situation
if elBlockNumber <= lastEra1Block:
debug "EL still pre-merge, no EL sync",
blocknumber = elBlockNumber, lastPoWBlock = lastEra1Block
return

# Load the latest state from the CL
var clBlockNumber = block:
let blck = dag.getForkedBlock(dag.head.slot).valueOr:
# When starting from a checkpoint, the CL might not yet have the head
# block in the database
debug "CL has not yet downloaded head block", head = dag.head
return
blck.blockNumber

# Check if the EL is already in sync or about to become so (ie processing a
# payload already, most likely)
if clBlockNumber in [elBlockNumber, elBlockNumber + 1]:
debug "EL in sync (or almost)", clBlockNumber, elBlockNumber
return

if clBlockNumber < elBlockNumber:
# This happens often during initial sync when the light client information
# allows the EL to sync ahead of the CL head - it can also happen during
# reorgs
debug "CL is behind EL, not activating", clBlockNumber, elBlockNumber
return

var importedSlot = findSlot(dag, elBlockNumber, lastEra1Block, firstSlotAfterMerge).valueOr:
debug "Missing slot information for sync", elBlockNumber
return

notice "Found initial slot for EL sync", importedSlot, elBlockNumber, clBlockNumber

while elBlockNumber < clBlockNumber:
var isAvailable = false
let curBlck = dag.getForkedBlock(Slot(importedSlot)).valueOr:
importedSlot += 1
continue
importedSlot += 1
let payloadResponse = withBlck(curBlck):
# Don't include blocks before bellatrix, as it doesn't have payload
when consensusFork >= ConsensusFork.Gloas:
break
elif consensusFork >= ConsensusFork.Bellatrix:
# Load the execution payload for all blocks after the bellatrix upgrade
let payload =
forkyBlck.message.body.execution_payload.asEngineExecutionPayload()

debug "Sending payload", payload

when consensusFork >= ConsensusFork.Electra:
let
# Calculate the versioned hashes from the kzg commitments
versioned_hashes =
forkyBlck.message.body.blob_kzg_commitments.asEngineVersionedHashes()
# Execution Requests for Electra
execution_requests =
forkyBlck.message.body.execution_requests.asEngineExecutionRequests()

await rpcClient.engine_newPayloadV4(
payload,
versioned_hashes,
forkyBlck.message.parent_root.to(Hash32),
execution_requests,
)
elif consensusFork >= ConsensusFork.Deneb:
# Calculate the versioned hashes from the kzg commitments
let versioned_hashes =
forkyBlck.message.body.blob_kzg_commitments.asEngineVersionedHashes()
await rpcClient.engine_newPayloadV3(
payload, versioned_hashes, forkyBlck.message.parent_root.to(Hash32)
)
elif consensusFork >= ConsensusFork.Capella:
await rpcClient.engine_newPayloadV2(payload)
else:
await rpcClient.engine_newPayloadV1(payload)
else:
return

if payloadResponse.status != PayloadExecutionStatus.valid:
if payloadResponse.status notin
[PayloadExecutionStatus.syncing, PayloadExecutionStatus.accepted]:
# This would be highly unusual since it would imply a CL-valid but
# EL-invalid block..
warn "Payload invalid",
elBlockNumber, status = payloadResponse.status, curBlck = shortLog(curBlck)
return

debug "newPayload accepted", elBlockNumber, response = payloadResponse.status

elBlockNumber += 1

if elBlockNumber mod 1024 == 0:
let curElBlock = uint64(await rpcClient.eth_blockNumber())
if curElBlock != elBlockNumber:
# If the EL starts syncing on its own, faster than we can feed it blocks
# from here, it'll run ahead and we can stop this remote-drive attempt
# TODO this happens because el-sync competes with the regular devp2p sync
# when in fact it could be collaborating such that we don't do
# redundant work
debug "EL out of sync with EL syncer", curElBlock, elBlockNumber
return
Loading