|
| 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