Skip to content

Commit f2586bf

Browse files
authored
Merge branch 'main' into build/leanvm-track-main
2 parents 5ed24c3 + e16f477 commit f2586bf

29 files changed

Lines changed: 15839 additions & 0 deletions

.claude/skills/multi-server-devnet/SKILL.md

Lines changed: 519 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Multi-Client Conversion Reference
2+
3+
How to run zeam / ream / qlean / gean / lantern / grandine validators in place of
4+
ethlambda on a devnet, and current interop status. `scripts/convert.sh` implements
5+
all of this; this file explains the why and the gotchas.
6+
7+
## Core principle: keep the node identity, rename only the container
8+
9+
All genesis artifacts are keyed by **node identity** `node_N`
10+
(`annotated_validators.yaml`, `validator-config.yaml`, `node_N.key`,
11+
`hash-sig-keys/validator_N_*`, data dir `data/node_N`). So any client can take over
12+
a validator slot **without touching identity or data layout**: keep `--node-id
13+
node_N` and the key `/config/node_N.key`.
14+
15+
What *does* change is the **container/display name**, which becomes `<client>_N`
16+
(e.g. `zeam_8`) so cAdvisor's `name` label and promtail's `node` label — and hence
17+
Grafana — show which client is running. Scripts locate a node's existing container
18+
by the `_N$` suffix, so the prefix can differ from run to run.
19+
20+
Conversion = swap image + CLI shape + wipe data + checkpoint sync. Never keep-DB
21+
across a client switch (wrong-format data) — wipe and checkpoint-sync from a
22+
healthy **same-devnet** node.
23+
24+
## Vote math constraint
25+
26+
Finality threshold is `ceil(2/3 · NODES)` aligned votes on that devnet. Keep
27+
aggregators on ethlambda and keep the ethlambda node count above the threshold, so
28+
finality survives any single non-eth cohort failing. Do NOT convert so many nodes
29+
that ethlambda drops below the threshold unless every other client is proven to
30+
reliably contribute justification votes. Convert a **canary** of each client type
31+
first and confirm `vote_count` rises before mass conversion.
32+
33+
## Images (devnet5 — current)
34+
35+
| Client | Image | Notes |
36+
|--------|-------|-------|
37+
| ethlambda | `ghcr.io/lambdaclass/ethlambda:devnet5` | |
38+
| zeam | `blockblaz/zeam:devnet5` | needs `--security-opt seccomp=unconfined` |
39+
| ream | `ghcr.io/reamlabs/ream:latest-devnet5` | |
40+
| qlean | `qdrvm/qlean-mini:devnet-5-amd64` | arch-specific tag (`-amd64`/`-arm64`) |
41+
| gean | `ghcr.io/geanlabs/gean:devnet5` | |
42+
| lantern | `bitminemavan/lantern:devnet5` | `v0.0.X` == devnetX; see mirror note |
43+
| grandine | `sifrai/lean:devnet-5` | |
44+
45+
**`bitminemavan/lantern` is lantern's current repo** — it is what upstream
46+
lean-quickstart's ansible pulls and where new builds land (including
47+
`devnet5-leanvm-main` for chains on leanVM `main`). `piertwo/lantern` is the older
48+
repo: same tag names, but it stopped tracking (its `devnet5` predates
49+
bitminemavan's). Use `bitminemavan`; the scripts default to it.
50+
51+
devnet4 images (older): `blockblaz/zeam:devnet4`,
52+
`ghcr.io/reamlabs/ream:latest-devnet4`, `qdrvm/qlean-mini:devnet-4-amd64`,
53+
`sifrai/lean:devnet-4`, `bitminemavan/lantern:v0.0.4`.
54+
55+
Image tag must match the chain's leanVM/proof format. After a leanVM bump, other
56+
clients must ship a matching build or signature aggregation deserialization fails.
57+
**Re-canary every client after any bump.**
58+
59+
## CLI shapes (per node)
60+
61+
Placeholders below: `N` = node index, `G`/`A`/`M` = gossip/api/metrics ports
62+
(`9000+N`/`5052+N`/`9200+N`), `ACC` = the devnet's `SUBNETS`
63+
(`ATTESTATION_COMMITTEE_COUNT`), `URL` = `http://127.0.0.1:<CS_PORT>/lean/v0/states/finalized`.
64+
Aggregator flags in `[...]` are added only for an aggregator node, and it covers a
65+
**single** subnet: `--aggregate-subnet-ids $((N % ACC))`.
66+
67+
zeam — `--validator-config genesis_bootnode` (NOT a file path; the file form fails
68+
with `NotDir`). Reads the genesis dir via `--custom-genesis`. The log flags come
69+
before the `node` subcommand; `--console_log_level info` is what puts zeam's own
70+
duty publishes on stdout where the duty-timing profiler can read them (the file
71+
sink stays at warn):
72+
```
73+
--log_file_active_level warn --console_log_level info node \
74+
--custom-genesis /config --validator-config genesis_bootnode --data-dir /data \
75+
--node-id node_N --node-key /config/node_N.key --metrics-enable \
76+
--api-port A --metrics-port M --attestation-committee-count ACC \
77+
[--is-aggregator --aggregate-subnet-ids <subnet>] --checkpoint-sync-url URL --db-backend lmdb
78+
```
79+
80+
ream — uses `annotated_validators.yaml`, `--socket-port` for QUIC:
81+
```
82+
--data-dir /data lean_node --network /config/config.yaml \
83+
--validator-registry-path /config/annotated_validators.yaml --bootnodes /config/nodes.yaml \
84+
--node-id node_N --node-key /config/node_N.key --socket-port G \
85+
--metrics --metrics-address 0.0.0.0 --metrics-port M --http-address 0.0.0.0 --http-port A \
86+
--attestation-committee-count ACC [--is-aggregator --aggregate-subnet-ids <subnet>] --checkpoint-sync-url URL
87+
```
88+
89+
qlean — `--genesis-dir`, multiaddr listen, NO `--aggregate-subnet-ids` (crashes
90+
`unrecognised option`); it aggregates only auto-derived subnets:
91+
```
92+
--genesis-dir /config --data-dir /data --node-id node_N --node-key /config/node_N.key \
93+
--listen-addr /ip4/0.0.0.0/udp/G/quic-v1 --metrics-host 0.0.0.0 --metrics-port M \
94+
--api-host 0.0.0.0 --api-port A --attestation-committee-count ACC [--is-aggregator] --checkpoint-sync-url URL -linfo
95+
```
96+
97+
lantern — C client. Docker entrypoint runs the binary directly when argv[0] starts
98+
with `--`. `--validator_config` is a DIR (holds both annotated_validators.yaml +
99+
validator-config.yaml); needs `--genesis-state /config/genesis.ssz`; node key via
100+
`--node-key-path`; multiaddr `--listen-address`. Reads the gossip fork digest from
101+
genesis ENRs in nodes.yaml, falling back to `--devnet NAME` as the topic segment —
102+
pass `--devnet 12345678` to match the cross-client dummy digest. Supports
103+
`--checkpoint-sync-url` (URL must include the `/lean/v0/states/finalized` path) and
104+
`--is-aggregator --aggregate-subnet-ids`:
105+
```
106+
--data-dir /data --genesis-config /config/config.yaml --nodes-path /config/nodes.yaml \
107+
--genesis-state /config/genesis.ssz --validator_config /config \
108+
--node-id node_N --node-key-path /config/node_N.key \
109+
--listen-address /ip4/0.0.0.0/udp/G/quic-v1 --http-port A --metrics-port M \
110+
--hash-sig-key-dir /config/hash-sig-keys --attestation-committee-count ACC \
111+
--devnet 12345678 [--is-aggregator --aggregate-subnet-ids <subnet>] --checkpoint-sync-url URL
112+
```
113+
114+
grandine — `--port` for QUIC, `--hash-sig-key-dir` (singular):
115+
```
116+
--genesis /config/config.yaml --validator-registry-path /config/annotated_validators.yaml \
117+
--bootnodes /config/nodes.yaml --node-id node_N --node-key /config/node_N.key \
118+
--port G --address 0.0.0.0 --http-address 0.0.0.0 --http-port A \
119+
--metrics --metrics-address 0.0.0.0 --metrics-port M --hash-sig-key-dir /config/hash-sig-keys \
120+
--attestation-committee-count ACC [--is-aggregator --aggregate-subnet-ids <subnet>] --checkpoint-sync-url URL
121+
```
122+
123+
gean — Go client. `--custom-network-config-dir` is the genesis dir; gossip port flag
124+
like ethlambda. **`--data-dir` is not optional in practice**: it defaults to a
125+
container-relative `./data` (= `/app/data`), so without it the Pebble DB lives
126+
inside the container instead of the `/data` mount — a host-side `rm -rf
127+
data/node_*` then silently wipes nothing, and the DB dies with the container:
128+
```
129+
--custom-network-config-dir /config --data-dir /data --gossipsub-port G --node-id node_N \
130+
--node-key /config/node_N.key --http-address 0.0.0.0 --api-port A --metrics-port M \
131+
--attestation-committee-count ACC [--is-aggregator --aggregate-subnet-ids <subnet>] [--checkpoint-sync-url URL]
132+
```
133+
134+
## convert.sh usage
135+
136+
```
137+
convert.sh <CS_PORT> <spec>... spec = N:client[:agg] client in zeam|ream|qlean|gean|lantern|grandine
138+
# e.g. convert.sh 5052 25:zeam 26:zeam 27:zeam
139+
# ACC=<SUBNETS> only to override what the genesis says
140+
```
141+
No BASE arg (each devnet numbers nodes from 0). `ACC` (the devnet's `SUBNETS` /
142+
`ATTESTATION_COMMITTEE_COUNT`) is read from `genesis/config.yaml` on the host, so it
143+
can't silently disagree with the chain — a wrong value puts the node's votes on a
144+
subnet nobody aggregates. CS_PORT = a healthy same-devnet node's api port.
145+
146+
Guards, both learned the hard way:
147+
148+
- **Specs are validated before anything is removed.** A typo'd client name used to
149+
be caught only after the container was gone, the data wiped and the 60s backoff
150+
slept — leaving the node down.
151+
- **Converting an aggregator away warns.** If the node currently runs with
152+
`--is-aggregator` and the new spec has no `:agg`, that subnet is left with no
153+
aggregator: votes verify, nothing is stored, `attestation_count` goes to 0 and
154+
finality dies quietly. Either keep the role (`N:client:agg`) or move it to
155+
another node first (`AGG=<subnet> cs-restart.sh <CS_PORT> <node>`).
156+
157+
Keeps the 60s backoff, memory limits, and json-log caps. After converting, relabel
158+
prometheus (see operations.md) and confirm with `host-check.sh`.

0 commit comments

Comments
 (0)