Skip to content

Commit 3978e61

Browse files
authored
Merge branch 'main' into feat/discv5-discovery
2 parents 718788c + 13bf5e2 commit 3978e61

9 files changed

Lines changed: 606 additions & 35 deletions

File tree

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

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,48 @@ B owns `k+1..N-1`). Everything below still applies with three changes, and
4040

4141
Nothing about the servers is hardcoded. Establish these from the operator:
4242

43-
- `SERVERS` — the SSH targets (any count, any names), one devnet each.
44-
- `SSH_USER` — login user. `docker` is invoked with `sudo`.
43+
- `SERVERS`: the SSH targets for the operation at hand (any count, any names),
44+
one devnet each. A working set taken FROM the inventory below, not a second
45+
list of hosts to keep in step with it.
46+
- `SSH_USER`: login user. `docker` is invoked with `sudo`.
4547
- Per-devnet `NODES` (validators on that server) and `SUBNETS`
4648
(`ATTESTATION_COMMITTEE_COUNT`); these can differ between servers.
4749
- A central host for Grafana + the federating Prometheus (often one of the
4850
servers); each server's per-host Prometheus remote-writes to it.
4951

50-
Put those values in `scripts/devnet.env` (copy `scripts/devnet.env.example`;
51-
gitignored) instead of retyping them: the operator-side scripts source it via
52+
`SERVERS`/`SSH_USER` are the caller's to pass per command: no script reads them, so
53+
which hosts a command runs against is decided at the call rather than stored. The
54+
rest goes in `scripts/devnet.env` (copy `scripts/devnet.env.example`; gitignored)
55+
instead of being retyped: the operator-side scripts source it via
5256
`scripts/devnet-env.sh`, and an env var exported in the shell still wins over the
53-
file. It is the one place a deployment's hosts, urls, and Grafana ids live.
54-
Per-devnet `NODES`/`SUBNETS` are recorded there too, but as the operator's
55-
inventory — the scripts take them as positional args, and the authority on a
56-
running devnet is always its own `genesis/config.yaml`
57-
(`ATTESTATION_COMMITTEE_COUNT`), which `start-devnet.sh` and `convert.sh` check
58-
against.
57+
file. It is the one place a deployment's urls and Grafana ids live.
58+
59+
**The servers themselves live in `scripts/devnet.inventory`** (copy
60+
`scripts/devnet.inventory.example`; gitignored): name, ip, tags, and per-devnet
61+
`NODES`/`SUBNETS`, one row per host. Query it with `scripts/inventory.sh` rather
62+
than reading it by hand:
63+
64+
```bash
65+
bash scripts/inventory.sh --tag devnet-ab --field ip # ips, for a loop
66+
bash scripts/inventory.sh --tag devnet-ab --tag aggregator # AND across tags
67+
bash scripts/inventory.sh --tag validator # derived, see below
68+
SERVERS=$(bash scripts/inventory.sh --field name) || exit # exit 2 = typo'd tag
69+
SERVERS=${SERVERS//$'\n'/ } # newlines -> spaces
70+
```
71+
72+
Tag conventions: a `devnet-*` tag names the chain a host's nodes belong to (two
73+
hosts sharing one means the split-chain model, *not* two like-named devnets), and
74+
`aggregator` is a whole-server role. `validator` is **derived**: "has a `devnet-*`
75+
tag and is not tagged `aggregator`", so it can never disagree with the aggregator
76+
tag, and writing it literally in the file is an error rather than a second opinion.
77+
An unknown or empty `--tag` exits 2 rather than returning nothing (or, worse,
78+
everything): a typo that yields an empty loop reports success while doing nothing,
79+
and `--tag "$UNSET"` would act on hosts the caller never named.
80+
81+
`NODES`/`SUBNETS` there are still the operator's inventory: the scripts take them
82+
as positional args, and the authority on a running devnet is always its own
83+
`genesis/config.yaml` (`ATTESTATION_COMMITTEE_COUNT`), which `start-devnet.sh` and
84+
`convert.sh` check against.
5985

6086
Per devnet, node `n` (0 ≤ n < NODES) on its host:
6187

@@ -130,7 +156,8 @@ redirect must run under sudo).
130156
## Workflows
131157

132158
Examples assume `SSH_USER` is set and you iterate over `SERVERS`. Per server you
133-
pass its own `NODES`/`SUBNETS`.
159+
pass its own `NODES`/`SUBNETS`. `for h in $SERVERS` splits under bash; zsh does not
160+
split parameter expansions, so there write `for h in $(echo $SERVERS)`.
134161

135162
### Pull the latest images on all servers
136163
```bash
@@ -473,7 +500,8 @@ Swap (persistent): `fallocate -l 16G /swapfile && chmod 600 && mkswap && swapon`
473500
| `promtail-config.sh` | operator | `NETWORK HOST_IP LOKI_PUSH_URL [N:client ...]` | Emit a per-host promtail.yml (docker_sd → central Loki, labels mirror prometheus, backlog guard) |
474501
| `sweep.sh` | operator | `CENTRAL_PROM_URL` | Cross-devnet audit: head/justified/finalized + client mix from the central Prometheus |
475502
| `deploy-finality-alert.sh` | operator | `[WEBHOOK_FILE]`; `METRICS_HOST`, `GRAFANA_*`, `PROM_DS_UID`; `DRY_RUN` | Render + ship the "lost finality" Slack alert to the central Grafana |
476-
| `devnet-env.sh` / `devnet.env.example` | operator | `$DEVNET_ENV`, `./devnet.env`, script dir | Load this deployment's hosts/urls/Grafana ids as defaults; exported vars win. Copy the example to `devnet.env` (gitignored) once |
503+
| `devnet-env.sh` / `devnet.env.example` | operator | `$DEVNET_ENV`, `./devnet.env`, script dir | Load this deployment's urls/Grafana ids as defaults; exported vars win. Copy the example to `devnet.env` (gitignored) once |
504+
| `inventory.sh` / `devnet.inventory.example` | operator | `[--tag T]... [--not-tag T]... [--field F] [--file P] [--count]`; `$DEVNET_INVENTORY`, `./devnet.inventory`, script dir | Select servers by tag from the fleet inventory (name, ip, tags, NODES, SUBNETS). `devnet-*` tags name the chain, `aggregator` is a server role, `validator` is derived (`devnet-*` and not `aggregator`). Unknown tag → exit 2 + known-tag list, so a typo can't masquerade as an empty fleet. Copy the example to `devnet.inventory` (gitignored) once |
477505

478506
**Grafana dashboards** (copy into the central Grafana's dashboards dir —
479507
`GRAFANA_DASHBOARDS_DIR`, *not* the provisioning tree; they auto-load in ~30s and

.claude/skills/multi-server-devnet/scripts/devnet-env.sh

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
#
55
# . "$SCRIPT_DIR/devnet-env.sh"; devnet_load_env
66
#
7+
# Also provides devnet_find_file, the file-lookup ladder both this and
8+
# inventory.sh use.
9+
#
710
# Lookup order, first hit wins: $DEVNET_ENV, ./devnet.env, <scripts dir>/devnet.env.
811
# Copy devnet.env.example -> devnet.env and fill it in (devnet.env is gitignored,
912
# since it names your hosts and may point at a webhook file).
@@ -17,21 +20,32 @@
1720
# fine. A line that looks like an assignment but whose name isn't usable is
1821
# reported on stderr rather than dropped in silence, because a config line that
1922
# goes unread is how you deploy against the wrong deployment.
20-
devnet_load_env() {
21-
local dir file line key val
23+
24+
# Resolve one of the deployment's config files. First hit wins: the path in $2 (an
25+
# env var NAME), ./<name>, <scripts dir>/<name>. Prints the path on stdout.
26+
# Returns 1 when nothing exists, and 2 when the env var names a path that doesn't
27+
# -- an explicit path that isn't there is a typo, not a licence to fall back to
28+
# another deployment's file. Shared with inventory.sh so devnet.env and
29+
# devnet.inventory can't end up with two ideas of where they live.
30+
devnet_find_file() {
31+
local name=$1 envvar=$2 dir explicit candidate
2232
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23-
file=""
24-
if [ -n "${DEVNET_ENV:-}" ]; then
25-
# An explicit path that doesn't exist is a typo, not a reason to silently fall
26-
# back to some other devnet.env and deploy against the wrong deployment.
27-
[ -f "$DEVNET_ENV" ] || { echo "DEVNET_ENV=$DEVNET_ENV does not exist" >&2; return 1; }
28-
file=$DEVNET_ENV
29-
else
30-
for candidate in "./devnet.env" "$dir/devnet.env"; do
31-
[ -f "$candidate" ] && { file=$candidate; break; }
32-
done
33+
explicit=${!envvar:-}
34+
if [ -n "$explicit" ]; then
35+
[ -f "$explicit" ] || { echo "$envvar=$explicit does not exist" >&2; return 2; }
36+
printf '%s\n' "$explicit"; return 0
3337
fi
34-
[ -n "$file" ] || return 0
38+
for candidate in "./$name" "$dir/$name"; do
39+
[ -f "$candidate" ] && { printf '%s\n' "$candidate"; return 0; }
40+
done
41+
return 1
42+
}
43+
44+
devnet_load_env() {
45+
local file line key val rc
46+
file=$(devnet_find_file devnet.env DEVNET_ENV); rc=$?
47+
[ "$rc" -eq 2 ] && return 1 # DEVNET_ENV names a missing file: reported
48+
[ "$rc" -eq 0 ] || return 0 # no env file at all is not an error
3549

3650
while IFS= read -r line || [ -n "$line" ]; do
3751
line=${line#"${line%%[![:space:]]*}"} # ltrim, so an indented line is read

.claude/skills/multi-server-devnet/scripts/devnet.env.example

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,9 @@
44
# Loaded by the operator-side scripts via devnet-env.sh; an env var already set in
55
# your shell overrides the value here.
66

7-
# --- fleet (one independent devnet per server) -------------------------------
8-
SERVERS="host-a host-b" # ssh targets, any count/names
9-
SSH_USER=user # login user; docker is invoked with sudo
10-
11-
# Per-devnet size, as INVENTORY: the scripts take NODES/SUBNETS as positional
12-
# args, and the authority for a running devnet is its own genesis/config.yaml
13-
# (ATTESTATION_COMMITTEE_COUNT). Recorded here so a restart doesn't have to guess.
14-
# host-a: NODES=32 SUBNETS=4
15-
# host-b: NODES=16 SUBNETS=2
7+
# Only what a script actually reads lives here. SERVERS/SSH_USER don't: which hosts
8+
# a command runs against is the caller's to pass per invocation, and which hosts
9+
# EXIST is devnet.inventory's record, queried with scripts/inventory.sh.
1610

1711
# --- central metrics / logs stack -------------------------------------------
1812
# sweep.sh reads CENTRAL_PROM_URL; the other two are args you pass to
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# The deployment's servers: what each one is called, where it is, what it belongs
2+
# to, and how big its devnet is. Copy to devnet.inventory (gitignored) and fill in.
3+
#
4+
# This is the operator's inventory, in the same sense devnet.env's values are: the
5+
# authority on a RUNNING devnet is always its own genesis/config.yaml
6+
# (ATTESTATION_COMMITTEE_COUNT), and the scripts still take NODES/SUBNETS as
7+
# positional args. Recorded here so a restart doesn't have to guess, and so
8+
# "which hosts are in devnet-c" has one answer instead of living in someone's head.
9+
#
10+
# Read it with inventory.sh rather than by hand:
11+
#
12+
# bash scripts/inventory.sh --tag devnet-ab --field ip # ips, for a loop
13+
# bash scripts/inventory.sh --tag devnet-ab --tag aggregator # AND: both needed
14+
# bash scripts/inventory.sh --tag validator # derived, see TAGS
15+
# SERVERS=$(bash scripts/inventory.sh --field name) || exit # exit 2 = typo'd tag
16+
# SERVERS=${SERVERS//$'\n'/ } # newlines -> spaces
17+
#
18+
# FORMAT: whitespace-separated columns, aligned however you like. Blank lines and
19+
# lines whose first non-space character is '#' are ignored. Five columns:
20+
#
21+
# name ssh target / display name. Need not equal the machine's hostname,
22+
# and often doesn't after a host is repurposed -- inventory.sh never
23+
# resolves it, so a stale name here misroutes every ssh that uses it.
24+
# ip address. The one field worth re-deriving from the fleet rather than
25+
# trusting after a rebuild.
26+
# tags COMMA-separated, NO spaces (a space would start the next column;
27+
# inventory.sh rejects the file rather than answer from shifted rows).
28+
# nodes NODES on this server, or '-' if it runs no devnet.
29+
# subnets ATTESTATION_COMMITTEE_COUNT, or '-'.
30+
#
31+
# TAGS: free-form, with two conventions inventory.sh knows about:
32+
#
33+
# devnet-* names the chain this server's nodes belong to. Two servers sharing
34+
# one tag means they share one genesis, i.e. the split-chain model
35+
# (nodes 0..N on one, N+1.. on the other) -- NOT two devnets that
36+
# happen to be named alike. Also the `network` Prometheus label.
37+
# aggregator this server runs the aggregator duty. A whole-server role, not a
38+
# per-node one.
39+
#
40+
# `validator` is DERIVED, never written here: it means "has a devnet-* tag and
41+
# is not tagged aggregator". Writing it as a literal tag would let it disagree
42+
# with the aggregator column, so inventory.sh computes it instead.
43+
#
44+
# The example below shows both supported topologies at once: host-a + host-b share
45+
# the chain `devnet-ab` (64 nodes total, 32 each), host-c runs its own independent
46+
# `devnet-c`, and metrics-1 carries the Grafana/Prometheus/Loki stack and no nodes.
47+
48+
# name ip tags nodes subnets
49+
host-a 10.0.0.1 devnet-ab,aggregator 32 8
50+
host-b 10.0.0.2 devnet-ab 32 8
51+
host-c 10.0.0.3 devnet-c 16 2
52+
metrics-1 10.0.0.4 tooling - -

0 commit comments

Comments
 (0)