Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8a4c2cb
Add Lean Ethereum consensus pipeline (ethlambda + ream/zeam stubs)
ilitteri May 13, 2026
0d894ba
Fix Lean integration: Starlark string syntax, sanity_check, mounts
ilitteri May 13, 2026
160081e
Wire lean-only mode end-to-end and fix Lean pipeline runtime issues
ilitteri May 13, 2026
4c87631
Load XMSS pubkey manifest with yaml.BaseLoader
ilitteri May 13, 2026
09f8fdd
Wire zeam end-to-end via static busybox + genesis_bootnode
ilitteri May 13, 2026
0559f18
Bump hash-sig keygen plan.run_sh wait to 30m
ilitteri May 13, 2026
5f97800
Add Prometheus + Grafana to the Lean Kurtosis pipeline
ilitteri May 13, 2026
9413bac
Pin metrics services to deterministic host ports (3000 + 9090)
ilitteri May 13, 2026
99e45da
Add launchers for qlean, lantern, grandine, lighthouse, gean
ilitteri May 14, 2026
21cf078
Fix lantern and lighthouse launcher CLI surfaces
ilitteri May 14, 2026
c2d8a06
Point DEFAULT_LEAN_IMAGES at :latest, not devnet4
ilitteri May 14, 2026
aebde26
Reframe Lean as "client-only today, EL pairing later"
ilitteri May 14, 2026
2a4bdf4
Catch remaining "standalone" framing in package_io comments
ilitteri May 14, 2026
1052920
Follow examples convention: drop prose docs/, add .github/tests/ args
ilitteri May 14, 2026
5f9de98
Enable the `admin` JSON-RPC namespace on the ethrex EL launcher so that
ilitteri May 20, 2026
70da979
Detach Lean client binary launches via `setsid -f` instead of
ilitteri May 20, 2026
11631a9
Wire every Lean Ethereum consensus client as a `cl_type:` value inside
ilitteri May 20, 2026
116506b
Guard `fuzz_target` against empty `all_el_contexts`. When every
ilitteri May 20, 2026
92ecc5b
Replace underscores with hyphens when building the Kurtosis service name
ilitteri May 20, 2026
c16f421
Add args file for the all-ELs experiment: 8 EL clients × 2 each, every
ilitteri May 20, 2026
0f0f7d2
Drop ethereumjs from the all-ELs ethlambda experiment — its container
ilitteri May 20, 2026
ac90c38
Narrow the all-ELs ethlambda experiment to 6 EL clients: ethrex,
ilitteri May 20, 2026
d52f13b
Fix Lean participant count being applied twice. The standard input
ilitteri May 20, 2026
adaa9c7
Drop dora from the all-ELs experiment. Dora requires at least one Eth1
ilitteri May 20, 2026
e85ff53
Bump genesis_delay to 180s in the all-ELs ethlambda experiment. With
ilitteri May 20, 2026
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
74 changes: 74 additions & 0 deletions .github/tests/ethlambda-el-all-clients.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Cross-client EL × ethlambda experiment: 6 EL clients × 2 each, every
# node paired with ethlambda via Engine API (12 EL+CL pairs total).
# One ethlambda is the aggregator; the remaining 11 are non-aggregators.
#
# Client set: ethrex, nethermind, geth, erigon, nimbus-el (nimbus-eth1),
# besu. ethereumjs and reth omitted from this experiment.
#
# The ethlambda image is built locally from lambdaclass/ethlambda#367 —
# see the PR description for the build command.
#
# Dora intentionally omitted — it requires at least one Eth1 beacon
# endpoint to start, and Lean cl_types don't expose one yet. Re-enable
# once ethlambda ships Beacon API compatibility stubs.
participants:
# First ethrex+ethlambda pair is the network aggregator.
- el_type: ethrex
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 1
validator_count: 1
is_aggregator: true
# Second ethrex+ethlambda — non-aggregator peer.
- el_type: ethrex
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 1
validator_count: 1
is_aggregator: false
# 2× nethermind
- el_type: nethermind
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 2
validator_count: 1
is_aggregator: false
# 2× geth
- el_type: geth
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 2
validator_count: 1
is_aggregator: false
# 2× erigon
- el_type: erigon
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 2
validator_count: 1
is_aggregator: false
# 2× nimbus-eth1
- el_type: nimbus
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 2
validator_count: 1
is_aggregator: false
# 2× besu
- el_type: besu
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 2
validator_count: 1
is_aggregator: false

lean_network_params:
# 180s gives the 12 EL containers time to warm up before the Lean
# chain starts producing blocks. With the default 60s, several early
# slots missed because proposers couldn't get a payload back from
# their paired EL within the 4s slot window during EL JIT/cache
# warm-up; those early gaps then blocked 3SF-mini's `delta ≤ 5`
# finalization rule.
genesis_delay: 180

additional_services: []
23 changes: 23 additions & 0 deletions .github/tests/ethlambda-el-pair-2node.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Two ethrex + ethlambda pairs. One aggregator, one not.
#
# - Two `participants:` entries, each pairs an ethrex EL with an ethlambda CL.
# - Lean genesis pipeline runs once across both participants (single XMSS keyset,
# shared validator-config) — see src/lean/lean_launcher.star.
# - Each ethlambda gets its own EL endpoint, JWT, and EL genesis block hash.
# - Lean libp2p QUIC mesh peers ethlambda_0 <-> ethlambda_1 directly via the
# nodes.yaml the genesis pipeline emits.
participants:
- el_type: ethrex
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 1
validator_count: 1
is_aggregator: true
- el_type: ethrex
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 1
validator_count: 1
is_aggregator: false

additional_services: []
21 changes: 21 additions & 0 deletions .github/tests/ethlambda-el-pair.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Single ethrex + ethlambda pair, smoke test for Engine API pairing.
#
# - ethlambda runs as a CL inside `participants:` (cl_type: ethlambda).
# - Its launcher is wired in src/lean/ethlambda/ethlambda_launcher.star; the
# standard CL dispatcher in src/cl/cl_launcher.star skips Lean cl_types,
# then main.star routes them into the Lean pipeline with the paired
# el_context and the network JWT.
# - The `:engine-api-integration` image is built from
# lambdaclass/ethlambda#367 locally (no registry yet).
#
# Other Lean clients live in `participants:` too with `el_type: none`
# (no Engine API yet) — see `lean-devnet4.yaml` and `lean-smoke.yaml`.
participants:
- el_type: ethrex
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
count: 1
validator_count: 1
is_aggregator: true

additional_services: []
64 changes: 64 additions & 0 deletions .github/tests/lean-devnet4.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# 14-node Lean devnet4. Every participant pins the devnet4 image
# explicitly via `cl_image:`; DEFAULT_CL_IMAGES point at `:latest` so
# the defaults don't rot when a new devnet generation ships.
#
# Single aggregator on the first ethlambda. lean_lighthouse is omitted:
# the published `hopinheimer/lighthouse:latest` image is still on the
# single-key (devnet3) GENESIS_VALIDATORS layout and won't reach
# consensus with the devnet4 nodes.
participants:
- el_type: none
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:devnet4
count: 1
validator_count: 1
is_aggregator: true
- el_type: none
cl_type: ethlambda
cl_image: ghcr.io/lambdaclass/ethlambda:devnet4
count: 1
validator_count: 1
is_aggregator: false
- el_type: none
cl_type: ream
cl_image: ghcr.io/reamlabs/ream:latest-devnet4
count: 2
validator_count: 1
is_aggregator: false
- el_type: none
cl_type: zeam
cl_image: blockblaz/zeam:devnet4
count: 2
validator_count: 1
is_aggregator: false
- el_type: none
cl_type: qlean
cl_image: qdrvm/qlean-mini:devnet-4-amd64
count: 2
validator_count: 1
is_aggregator: false
- el_type: none
cl_type: lantern
cl_image: piertwo/lantern:v0.0.4
count: 2
validator_count: 1
is_aggregator: false
- el_type: none
cl_type: lean_grandine
cl_image: sifrai/lean:devnet-4
count: 2
validator_count: 1
is_aggregator: false
- el_type: none
cl_type: gean
cl_image: ghcr.io/geanlabs/gean:devnet4
count: 2
validator_count: 1
is_aggregator: false

lean_network_params:
genesis_delay: 180
active_epoch: 18
attestation_committee_count: 1

additional_services: []
20 changes: 20 additions & 0 deletions .github/tests/lean-smoke.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Smallest reproducible Lean devnet: one aggregator + one peer, both
# ethlambda, no EL pairing. Comes up in ~3 min including hash-sig
# keygen. Use this as a sanity check before reaching for
# `lean-devnet4.yaml` or `ethlambda-el-pair*.yaml`.
participants:
- el_type: none
cl_type: ethlambda
count: 1
validator_count: 1
is_aggregator: true
- el_type: none
cl_type: ethlambda
count: 1
validator_count: 1
is_aggregator: false
lean_network_params:
genesis_delay: 60
active_epoch: 18
attestation_committee_count: 1
additional_services: []
35 changes: 35 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,41 @@ Once CL genesis data and keys have been created, the CL client nodes are started

There are only two major difference between CL client and EL client launchers. First, the `cl_client_launcher.launch` method also consumes an `el_context`, because each CL client is connected in a 1:1 relationship with an EL client. Second, because CL clients have keys, the keystore files are passed in to the `launch` function as well.

## Lean Ethereum participants

Lean Ethereum consensus clients live in the standard `participants:`
block with a Lean `cl_type` — one of `ethlambda`, `ream`, `zeam`,
`qlean`, `lantern`, `gean`, `lean_grandine`, `lean_lighthouse`. The
standard CL dispatcher in [`src/cl/cl_launcher.star`](../src/cl/cl_launcher.star)
skips these; `main.star` then routes them to a parallel pipeline under
[`src/lean/`](../src/lean) that runs its own genesis and starts the
client binary.

`ethlambda` is the only Lean client that implements Engine API today
([lambdaclass/ethlambda#367](https://github.com/lambdaclass/ethlambda/pull/367));
it can be paired with any EL the way standard CLs are
(`el_type: ethrex, cl_type: ethlambda` etc.). The other Lean clients
run client-only with `el_type: none` until Engine API ships on their
side too. Lean-only deployments (every participant `el_type: none`)
relax the package's first-participant-must-have-EL guard.

Genesis uses
[`eth-beacon-genesis leanchain`](https://github.com/ethpandaops/eth-beacon-genesis)
for the chain bundle and
[`blockblaz/hash-sig-cli`](https://hub.docker.com/r/blockblaz/hash-sig-cli)
for XMSS validator keypairs (lives in
[`src/prelaunch_data_generator/lean_genesis/`](../src/prelaunch_data_generator/lean_genesis)).
Per-client launchers translate each Lean record into the client's CLI
(see the contract documented inline in each `<client>_launcher.star`).
Metrics ride a vendored Prometheus + Grafana stack with the upstream
Lean client dashboard pre-loaded.

Canonical args examples:

- [`.github/tests/ethlambda-el-pair.yaml`](../.github/tests/ethlambda-el-pair.yaml) — single EL + ethlambda pair
- [`.github/tests/ethlambda-el-pair-2node.yaml`](../.github/tests/ethlambda-el-pair-2node.yaml) — two EL + ethlambda pairs, one aggregator + one not
- [`.github/tests/lean-devnet4.yaml`](../.github/tests/lean-devnet4.yaml) — devnet4 multi-client (all `el_type: none`)

## Auxiliary Services

After the Ethereum network is up and running, this package starts several auxiliary containers to make it easier to work with the Ethereum network. At time of writing, these are:
Expand Down
74 changes: 70 additions & 4 deletions main.star
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
input_parser = import_module("./src/package_io/input_parser.star")
constants = import_module("./src/package_io/constants.star")
participant_network = import_module("./src/participant_network.star")
lean_launcher = import_module("./src/lean/lean_launcher.star")
shared_utils = import_module("./src/shared_utils/shared_utils.star")
static_files = import_module("./src/static_files/static_files.star")
genesis_constants = import_module(
Expand Down Expand Up @@ -329,6 +330,64 @@ def run(plan, args={}):
)
all_xatu_sentry_contexts.append(participant.xatu_sentry_context)

# Launch Lean Ethereum consensus participants. Lean clients are
# `participants:` entries whose `cl_type` is in
# constants.LEAN_CL_TYPES. The cl_launcher dispatcher already
# skipped them (None cl_context); here we build equivalent Lean
# records from each participant and hand them — together with the
# paired el_context (None when el_type is `none`) and the network
# JWT — to src/lean/lean_launcher.launch. ethlambda is the only
# Lean client that wires Engine API today (lambdaclass/ethlambda#367);
# the others run with `el_type: none`.
lean_records = []
for index, participant in enumerate(args_with_right_defaults.participants):
if participant.cl_type not in constants.LEAN_CL_TYPES:
continue
paired_el_context = (
all_el_contexts[index] if index < len(all_el_contexts) else None
)
lean_records.append(
{
"lean_type": participant.cl_type,
"lean_image": participant.cl_image,
# The standard input parser already expanded `count: N` into
# N separate `participants:` entries (input_parser.star:1260),
# so we synthesize one Lean record per expanded participant
# and hardcode count=1 here — otherwise the Lean launcher's
# own count expansion would multiply N×N.
"count": 1,
"validator_count": participant.validator_count
or args_with_right_defaults.lean_network_params[
"num_validator_keys_per_node"
],
"is_aggregator": participant.is_aggregator,
"lean_extra_params": participant.cl_extra_params,
"lean_extra_env_vars": participant.cl_extra_env_vars,
"lean_extra_labels": participant.cl_extra_labels,
"lean_log_level": participant.cl_log_level,
"lean_min_cpu": participant.cl_min_cpu,
"lean_max_cpu": participant.cl_max_cpu,
"lean_min_mem": participant.cl_min_mem,
"lean_max_mem": participant.cl_max_mem,
"node_selectors": participant.node_selectors,
"tolerations": participant.tolerations,
"prometheus_config": {
"scrape_interval": participant.prometheus_config.scrape_interval,
"labels": participant.prometheus_config.labels or {},
},
# Underscore-prefixed fields are internal hand-offs to
# the Lean launcher.
"_el_context": paired_el_context,
}
)

all_lean_contexts = lean_launcher.launch(
plan,
lean_records,
args_with_right_defaults.lean_network_params,
jwt_file=jwt_file,
)

# Generate validator ranges
validator_ranges_config_template = read_file(
static_files.VALIDATOR_RANGES_CONFIG_TEMPLATE_FILEPATH
Expand All @@ -340,10 +399,17 @@ def run(plan, args={}):
args_with_right_defaults.participants,
)

fuzz_target = "http://{0}:{1}".format(
all_el_contexts[0].ip_addr,
all_el_contexts[0].rpc_port_num,
)
# `fuzz_target` is only consumed by additional services that talk to an
# Eth1 EL (tx-fuzz, rakoon, broadcaster, custom_flood). For all-Lean
# deployments (every participant `el_type: none`) there is no EL to
# point at; leave it empty and let the additional-service handlers
# guard their own use.
fuzz_target = ""
if len(all_el_contexts) > 0 and all_el_contexts[0] != None:
fuzz_target = "http://{0}:{1}".format(
all_el_contexts[0].ip_addr,
all_el_contexts[0].rpc_port_num,
)

# Broadcaster forwards requests, sent to it, to all nodes in parallel
if "broadcaster" in args_with_right_defaults.additional_services:
Expand Down
30 changes: 30 additions & 0 deletions network_params.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,33 @@ port_publisher:
# #!/bin/bash
# echo "Hello"
extra_files: {}

# Lean Ethereum knobs (post-quantum signatures, XMSS validator keys,
# leanchain genesis). Lean clients themselves live in the standard
# `participants:` block above with a Lean cl_type (ethlambda, ream, zeam,
# qlean, lantern, gean, lean_grandine, lean_lighthouse). ethlambda is the
# only Lean client that wires Engine API today
# (lambdaclass/ethlambda#367); the rest run with `el_type: none`.
#
# Example - 4 ethlambda nodes paired with ethrex:
# participants:
# - el_type: ethrex
# cl_type: ethlambda
# cl_image: ghcr.io/lambdaclass/ethlambda:engine-api-integration
# count: 4
# validator_count: 1
# is_aggregator: true
lean_network_params:
# Seconds added to "now" to compute GENESIS_TIME when genesis_time is 0.
genesis_delay: 60
# Explicit Unix timestamp; 0 = derive from genesis_delay.
genesis_time: 0
# leanSpec ATTESTATION_COMMITTEE_COUNT (1 = single committee per slot).
attestation_committee_count: 1
# log_2(active epochs) for the XMSS hash-sig scheme.
active_epoch: 18
# Default validator keys per node (overridable per participant).
num_validator_keys_per_node: 1
# Override the Lean genesis tooling images. Empty = pinned default.
genesis_generator_image: ""
hash_sig_cli_image: ""
13 changes: 13 additions & 0 deletions src/cl/cl_launcher.star
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ def launch(
for index, participant in enumerate(args_with_right_defaults.participants):
cl_type = participant.cl_type
el_type = participant.el_type

# Lean cl_types are launched by the Lean pipeline (src/lean/),
# not by the standard CL dispatcher. We append `None` to the
# per-index context lists so downstream `all_cl_contexts[index]`
# lookups stay aligned with `args_with_right_defaults.participants`;
# main.star then runs the Lean launcher with the right el_contexts
# and jwt_file. Mirrors the EL_TYPE.none / cl-only path the package
# already supports for `consensoor` etc.
if cl_type in constants.LEAN_CL_TYPES:
all_cl_contexts.append(None)
all_snooper_el_engine_contexts.append(None)
continue

node_selectors = input_parser.get_client_node_selectors(
participant.node_selectors,
global_node_selectors,
Expand Down
Loading
Loading