Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
565eddd
feat: add isAggregator flag to validator configuration
ch4r10t33r Feb 6, 2026
0522c16
Merge branch 'main' of https://github.com/blockblaz/lean-quickstart
ch4r10t33r Feb 17, 2026
f307de9
Merge remote-tracking branch 'origin/main' into main
ch4r10t33r Feb 22, 2026
1522fd6
Merge branch 'main' of https://github.com/blockblaz/lean-quickstart
ch4r10t33r Mar 6, 2026
99e1d5c
Merge branch 'main' of https://github.com/blockblaz/lean-quickstart
ch4r10t33r Mar 16, 2026
53814fa
Merge branch 'main' of https://github.com/blockblaz/lean-quickstart
ch4r10t33r Mar 16, 2026
414d49c
spin-node: add --subnets flag to deploy multiple nodes per client
ch4r10t33r Mar 18, 2026
9ce84d7
Merge branch 'main' into subnets
ch4r10t33r Mar 18, 2026
32f6a28
ansible: copy only the node's own hash-sig keys to each server
ch4r10t33r Mar 18, 2026
1db5cd1
spin-node: assert exactly 1 aggregator per subnet after selection
ch4r10t33r Mar 18, 2026
a305f30
validator-config: add privkey for commented-out gean_0, lean_node_0, …
ch4r10t33r Mar 18, 2026
f035af4
spin-node: derive subnet from config 'subnet' field, not node name su…
ch4r10t33r Mar 18, 2026
4133a31
docs: add client integration guide with link from README
ch4r10t33r Mar 18, 2026
37c2c96
spin-node: honour pre-existing isAggregator: true when no --aggregato…
ch4r10t33r Mar 18, 2026
92279ea
docs: clarify touch point 1 — both configs required, separate local/a…
ch4r10t33r Mar 18, 2026
f153b38
docs: add note to contact zeam team for server IP assignment
ch4r10t33r Mar 18, 2026
fbce729
spin-node: fix associative array for bash 3.2 compatibility
ch4r10t33r Mar 18, 2026
fe4b527
validator-config: use apiPort for lantern instead of httpPort
ch4r10t33r Mar 18, 2026
6dcccf1
fix: cadvisor deploy
KatyaRyazantseva Mar 18, 2026
b785bd8
prepare: install jq alongside yq and docker
ch4r10t33r Mar 18, 2026
8dabd90
fix: grandine address flag
KatyaRyazantseva Mar 18, 2026
0b5051a
fix: grandine address flag ansible
KatyaRyazantseva Mar 18, 2026
f71c818
spin-node: skip aggregator selection when using --restart-client
ch4r10t33r Mar 18, 2026
edfea3d
Merge branch 'main' into subnets
ch4r10t33r Mar 18, 2026
09d4fc0
validator-config: enable gean_0 node
ch4r10t33r Mar 18, 2026
b9c2c1b
Merge branch 'main' into subnets
ch4r10t33r Mar 19, 2026
023eaf8
run-ansible: derive inventory groups dynamically instead of hardcoding
ch4r10t33r Mar 19, 2026
b59e331
Merge branch 'main' into subnets
ch4r10t33r Mar 19, 2026
9577f4a
validator-config: add nlean_0 node
ch4r10t33r Mar 19, 2026
827f9e7
ansible: add gean and nlean roles and wire into deploy
ch4r10t33r Mar 19, 2026
ff50c26
docs: update adding-a-new-client guide with gean and nlean
ch4r10t33r Mar 19, 2026
f2f16bc
nlean: remove --pull=always for locally-built image
ch4r10t33r Mar 19, 2026
020cb6f
nlean: use ghcr.io/nleaneth/nlean:latest as docker image
ch4r10t33r Mar 19, 2026
a20ec27
fix: enable metrics flag for nlean
KatyaRyazantseva Mar 19, 2026
5575c03
Merge branch 'main' into subnets: resolve conflicts (gean, nlean, peam)
ch4r10t33r Mar 20, 2026
888803e
Merge branch 'subnets' of github.com:blockblaz/lean-quickstart into s…
ch4r10t33r Mar 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
72 changes: 60 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A single command line quickstart to spin up lean node(s)
- Uses PK's `eth-beacon-genesis` docker tool (not custom tooling)
- Generates PQ keys based on specified configuration in `validator-config.yaml`
- Force regen with flag `--forceKeyGen` when supplied with `generateGenesis`
- ✅ Integrates zeam, ream, qlean, lantern, lighthouse, grandine, ethlambda
- ✅ Integrates zeam, ream, qlean, lantern, lighthouse, grandine, ethlambda, gean, nlean, peam
- ✅ Configure to run clients in docker or binary mode for easy development
- ✅ Linux & Mac compatible & tested
- ✅ Option to operate on single or multiple nodes or `all`
Expand Down Expand Up @@ -212,10 +212,17 @@ Every Ansible deployment automatically deploys an observability stack alongside
15. `--prepare` verify and install the software required to run lean nodes on every remote server, and open + persist the necessary firewall ports.
- **Ansible mode only** — fails with an error if `deployment_mode` is not `ansible`
- Installs: `python3` (Ansible requirement), Docker CE + Compose plugin (all clients run as containers), `yq` (required by the `common` role at every deploy)
- Opens per-node ports (`quicPort`/UDP, `metricsPort`/TCP, `apiPort`/TCP) read from `validator-config.yaml`, plus fixed observability ports (9090, 9080, 9098, 9100). Enables `ufw` with default deny incoming (persisted across reboots).
- Opens per-node ports (`quicPort`/UDP, `metricsPort`/TCP, `apiPort`/TCP) read from the active validator config, plus fixed observability ports (9090, 9080, 9098, 9100). With `--subnets N`, all N nodes' port ranges are opened per host. Enables `ufw` with default deny incoming (persisted across reboots).
- Prints a per-tool, per-host status summary (`✅ ok` / `❌ missing`) and `ufw status verbose`
- `--node` is not required and is ignored; all other flags are also ignored except `--sshKey` and `--useRoot`
- `--node` is not required; passing unsupported flags alongside `--prepare` produces a prominent error — only `--sshKey` and `--useRoot` are accepted
- Example: `NETWORK_DIR=ansible-devnet ./spin-node.sh --prepare --sshKey ~/.ssh/id_ed25519 --useRoot`
16. `--subnets N` expand the validator config to deploy N nodes of each client on the same server, where N is 1–5.
- Generates `validator-config-subnets-N.yaml` from the template (without modifying the original)
- Each subnet node gets a unique name (`{client}_0`, `{client}_1`, …), ports incremented by the subnet index, and a fresh P2P identity key for subnets > 0
- Subnet assignment rule: each server contributes **exactly one node per subnet** — nodes on the same server are never in the same subnet
- Every subnet contains the same set of client types
- `N=1` renames nodes to `{client}_0` with no port changes (useful for canonical naming)
- Example: `NETWORK_DIR=ansible-devnet ./spin-node.sh --node all --subnets 3 --sshKey ~/.ssh/id_ed25519 --useRoot`

### Preparing remote servers

Expand All @@ -237,7 +244,7 @@ NETWORK_DIR=ansible-devnet ./spin-node.sh --prepare --sshKey ~/.ssh/id_ed25519 -

**Constraints:**
- Only works in ansible mode (`deployment_mode: ansible` in your config, or `--deploymentMode ansible`)
- Any other flags (e.g., `--node`, `--generateGenesis`) are silently ignored — only `--sshKey` and `--useRoot` are used
- Passing unsupported flags (e.g. `--node`, `--generateGenesis`) alongside `--prepare` produces a prominent error — only `--sshKey` and `--useRoot` are accepted
- `--node` is not required; the playbook runs on all remote hosts in the inventory

Once preparation succeeds, proceed with the normal deploy command:
Expand All @@ -246,6 +253,43 @@ Once preparation succeeds, proceed with the normal deploy command:
NETWORK_DIR=ansible-devnet ./spin-node.sh --node all --generateGenesis --sshKey ~/.ssh/id_ed25519 --useRoot
```

### Deploying multiple subnets

Use `--subnets N` to run N independent copies of each client on the same server. This is useful for testing multi-subnet P2P scenarios without provisioning additional machines.

```sh
# Deploy 3 subnets of every client (ansible)
NETWORK_DIR=ansible-devnet ./spin-node.sh --node all --subnets 3 \
--generateGenesis --sshKey ~/.ssh/id_ed25519 --useRoot
```

**How it works:**

`--subnets N` generates `validator-config-subnets-N.yaml` from the template (the original file is never modified). For each client in the template it creates N entries:

| Subnet index | Name | quicPort | metricsPort | apiPort |
|---|---|---|---|---|
| 0 | `zeam_0` | base | base | base |
| 1 | `zeam_1` | base+1 | base+1 | base+1 |
| … | … | … | … | … |
| N-1 | `zeam_N-1` | base+N-1 | base+N-1 | base+N-1 |

**Rules enforced:**
- `N` must be between 1 and 5
- Each server contributes exactly one node per subnet (nodes on the same server are never in the same subnet)
- Every subnet contains the same set of client types
- Each node beyond subnet 0 gets a fresh P2P identity key

**Running `--prepare` with subnets:**

Always run `--prepare` with the same `--subnets N` value before deploying, so the firewall opens all N port ranges per host:

```sh
# Prepare firewall for 3 subnets
NETWORK_DIR=ansible-devnet ./spin-node.sh --prepare --subnets 3 \
--sshKey ~/.ssh/id_ed25519 --useRoot
```

### Checkpoint sync

Checkpoint sync lets you restart clients by syncing from a remote checkpoint instead of from genesis. This is useful for joining an existing network (e.g., leanpoint mainnet) without replaying the full chain.
Expand Down Expand Up @@ -278,7 +322,7 @@ NETWORK_DIR=local-devnet ./spin-node.sh --restart-client zeam_0 \
- **Local** (`NETWORK_DIR=local-devnet`): Uses Docker directly
- **Ansible** (`NETWORK_DIR=ansible-devnet`): Uses Ansible to deploy to remote hosts

**Supported clients:** zeam, ream, qlean, lantern, lighthouse, grandine, ethlambda, peam
**Supported clients:** zeam, ream, qlean, lantern, lighthouse, grandine, ethlambda, gean, nlean, peam

> **Note:** All clients accept `--checkpoint-sync-url`. Client implementations may use different parameter names internally; update client-cmd scripts if parameters change.

Expand All @@ -293,9 +337,13 @@ Current following clients are supported:
5. Lighthouse
6. Grandine
7. Ethlambda
8. Peam
8. Gean
9. Nlean
10. Peam

Adding a new client requires 6 small, well-defined steps. See the full integration guide:

However adding a lean client to this setup is very easy. Feel free to do the PR or reach out to the maintainers.
📖 **[Adding a New Client](docs/adding-a-new-client.md)**

## How It Works

Expand Down Expand Up @@ -806,7 +854,7 @@ ansible/
│ └── all.yml # Global variables
├── playbooks/
│ ├── site.yml # Main playbook (clean + copy genesis + deploy)
│ ├── prepare.yml # Bootstrap: install Docker, build-essential, yq, etc.
│ ├── prepare.yml # Bootstrap: install Docker CE, yq; open firewall ports
│ ├── clean-node-data.yml # Clean node data directories
│ ├── generate-genesis.yml # Generate genesis files
│ ├── copy-genesis.yml # Copy genesis files to remote hosts
Expand Down Expand Up @@ -846,13 +894,13 @@ The command runs `ansible/playbooks/prepare.yml` against all remote hosts in the

**Firewall rules opened (via `ufw`):**

Each host's ports are read directly from `validator-config.yaml`, so only the ports actually configured for that node are opened:
Ports are read from the active validator config (the `--subnets`-expanded file when `--subnets N` is used, or `validator-config.yaml` otherwise). Entries are matched by IP address, so all N subnet nodes on a server are found and all their ports are opened:

| Port | Protocol | Source |
|---|---|---|
| `quicPort` | UDP | Per-node — QUIC/P2P transport (e.g. 9001) |
| `metricsPort` | TCP | Per-node — Prometheus scrape endpoint (e.g. 9095) |
| `apiPort` / `httpPort` | TCP | Per-node — REST API (e.g. 5055) |
| `quicPort` … `quicPort+N-1` | UDP | Per-node — QUIC/P2P transport (e.g. 9001–9003 for N=3) |
| `metricsPort` … `metricsPort+N-1` | TCP | Per-node — Prometheus scrape endpoint |
| `apiPort`/`httpPort` … `+N-1` | TCP | Per-node — REST API |
| 9090 | TCP | Observability — Prometheus |
| 9080 | TCP | Observability — Promtail |
| 9098 | TCP | Observability — cAdvisor |
Expand Down
30 changes: 21 additions & 9 deletions ansible-devnet/genesis/validator-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ validators:
ip: "65.109.131.177"
quic: 9001
metricsPort: 9095
httpPort: 5055
apiPort: 5055
isAggregator: false
count: 1

Expand Down Expand Up @@ -97,16 +97,28 @@ validators:
isAggregator: false
count: 1

# - name: "gean_0"
# enrFields:
# ip: "204.168.134.201"
# quic: 9001
# metricsPort: 9095
# apiPort: 5055
# isAggregator: false
# count: 1
- name: "gean_0"
privkey: "df008e968231c25c3938d80fee9bcc93b4b9711312cf471c1b6f77e67ad68d08"
enrFields:
ip: "204.168.134.201"
quic: 9001
metricsPort: 9095
apiPort: 5055
isAggregator: false
count: 1

- name: "nlean_0"
privkey: "d94e3dc35e320440c891b66bd82d1aaf2079364162815b32c2633ecae009c84c"
enrFields:
ip: "95.216.164.165"
quic: 9001
metricsPort: 9095
apiPort: 5055
isAggregator: false
count: 1

# - name: "lean_node_0"
# privkey: "d94e3dc35e320440c891b66bd82d1aaf2079364162815b32c2633ecae009c84c"
# enrFields:
# ip: "95.217.19.42"
# quic: 9001
Expand Down
23 changes: 17 additions & 6 deletions ansible/playbooks/copy-genesis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,20 +153,31 @@
loop_control:
label: "{{ item }}.key"

- name: Resolve hash-sig key files for this node
vars:
_av: "{{ lookup('file', genesis_dir + '/annotated_validators.yaml') | from_yaml }}"
_assignments: "{{ _av[inventory_hostname] | default([]) }}"
_sk_files: "{{ _assignments | map(attribute='privkey_file') | list }}"
_pk_files: "{{ _sk_files | map('regex_replace', '_sk\\.ssz$', '_pk.ssz') | list }}"
set_fact:
node_hash_sig_files: "{{ _sk_files + _pk_files }}"
when: hash_sig_keys_stat.stat.exists

- name: Create hash-sig-keys directory on remote
file:
path: "{{ actual_remote_genesis_dir }}/hash-sig-keys"
state: directory
mode: '0755'
when: hash_sig_keys_stat.stat.exists
when: hash_sig_keys_stat.stat.exists and (node_hash_sig_files | default([]) | length > 0)

- name: Copy hash-sig-keys directory to remote host
- name: Copy hash-sig key files for this node only
copy:
src: "{{ genesis_dir }}/hash-sig-keys/"
dest: "{{ actual_remote_genesis_dir }}/hash-sig-keys/"
mode: '0644'
src: "{{ genesis_dir }}/hash-sig-keys/{{ item }}"
dest: "{{ actual_remote_genesis_dir }}/hash-sig-keys/{{ item }}"
mode: '0600'
force: yes
when: hash_sig_keys_stat.stat.exists
loop: "{{ node_hash_sig_files | default([]) }}"
when: hash_sig_keys_stat.stat.exists and (node_hash_sig_files | default([]) | length > 0)

- name: List files on remote genesis directory
find:
Expand Down
35 changes: 27 additions & 8 deletions ansible/playbooks/deploy-nodes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# - node key files (*.key)
# - config.yaml, validators.yaml, nodes.yaml
# - genesis.ssz, genesis.json
# - hash-sig-keys/ directory (if exists, for qlean nodes)
# - hash-sig-keys/ directory (if exists): only the sk/pk files for this node's validators

- name: Parse and validate node names
hosts: localhost
Expand Down Expand Up @@ -122,7 +122,10 @@

- name: Sync validator-config.yaml to remote host
copy:
src: "{{ local_genesis_dir }}/validator-config.yaml"
# Use the expanded subnet config when --subnets was specified; fall back
# to the standard validator-config.yaml otherwise. The destination is
# always validator-config.yaml so client roles don't need to change.
src: "{{ local_genesis_dir }}/{{ validator_config_basename | default('validator-config.yaml') }}"
dest: "{{ genesis_dir }}/validator-config.yaml"
mode: '0644'
force: yes
Expand Down Expand Up @@ -165,23 +168,37 @@
- deploy
- sync

- name: Resolve hash-sig key files for this node
vars:
_av: "{{ lookup('file', local_genesis_dir + '/annotated_validators.yaml') | from_yaml }}"
_assignments: "{{ _av[node_name] | default([]) }}"
_sk_files: "{{ _assignments | map(attribute='privkey_file') | list }}"
_pk_files: "{{ _sk_files | map('regex_replace', '_sk\\.ssz$', '_pk.ssz') | list }}"
set_fact:
node_hash_sig_files: "{{ _sk_files + _pk_files }}"
when: hash_sig_keys_local.stat.exists
tags:
- deploy
- sync

- name: Create hash-sig-keys directory on remote
file:
path: "{{ genesis_dir }}/hash-sig-keys"
state: directory
mode: '0755'
when: hash_sig_keys_local.stat.exists
when: hash_sig_keys_local.stat.exists and (node_hash_sig_files | default([]) | length > 0)
tags:
- deploy
- sync

- name: Sync hash-sig-keys directory (for qlean nodes)
- name: Copy hash-sig key files for this node only
copy:
src: "{{ local_genesis_dir }}/hash-sig-keys/"
dest: "{{ genesis_dir }}/hash-sig-keys/"
mode: '0644'
src: "{{ local_genesis_dir }}/hash-sig-keys/{{ item }}"
dest: "{{ genesis_dir }}/hash-sig-keys/{{ item }}"
mode: '0600'
force: yes
when: hash_sig_keys_local.stat.exists
loop: "{{ node_hash_sig_files | default([]) }}"
when: hash_sig_keys_local.stat.exists and (node_hash_sig_files | default([]) | length > 0)
tags:
- deploy
- sync
Expand All @@ -199,6 +216,8 @@
- lighthouse
- grandine
- ethlambda
- gean
- nlean
- peam
- deploy

Expand Down
20 changes: 18 additions & 2 deletions ansible/playbooks/helpers/deploy-single-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@
- ethlambda
- deploy

- name: Deploy Gean node
include_role:
name: gean
when: client_type == "gean"
tags:
- gean
- deploy

- name: Deploy Nlean node
include_role:
name: nlean
when: client_type == "nlean"
tags:
- nlean
- deploy

- name: Deploy Peam node
include_role:
name: peam
Expand All @@ -96,5 +112,5 @@

- name: Fail if unknown client type
fail:
msg: "Unknown client type '{{ client_type }}' for node '{{ node_name }}'. Expected: zeam, ream, qlean, lantern, lighthouse, grandine, ethlambda or peam"
when: client_type not in ["zeam", "ream", "qlean", "lantern", "lighthouse", "grandine", "ethlambda", "peam"]
msg: "Unknown client type '{{ client_type }}' for node '{{ node_name }}'. Expected: zeam, ream, qlean, lantern, lighthouse, grandine, ethlambda, gean, nlean or peam"
when: client_type not in ["zeam", "ream", "qlean", "lantern", "lighthouse", "grandine", "ethlambda", "gean", "nlean", "peam"]
Loading
Loading