A single command line quickstart to spin up lean node(s)
- ✅ Single source of truth -
validator-config.yaml- defines everything
- Generates full genesis state (JSON + SSZ) plus config files
- add/remove nodes, modify validator count, assign IPs, ports, enr keys
- Uses PK's
eth-beacon-genesisdocker tool (not custom tooling) - Generates PQ keys based on specified configuration in
validator-config.yaml- Force regen with flag
--forceKeyGenwhen supplied withgenerateGenesis
- Force regen with flag
- ✅ Integrates zeam, ream, qlean (and more incoming...)
- ✅ 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
- Shell terminal: Preferably linux especially if you want to pop out separate new terminals for node
- Docker: Required to run PK's eth-beacon-genesis tool and hash-sig-cli for post-quantum keys
- Install from: Docker Desktop
- yq: YAML processor for automated configuration parsing
- Install on macOS:
brew install yq - Install on Linux: See yq installation guide
- Install on macOS:
# 1. Clone the repository
git clone <repo-url>
cd lean-quickstartNETWORK_DIR=local-devnet ./spin-node.sh --node all --generateGenesis --popupTerminal# Run only zeam_0 and ream_0 nodes (comma-separated)
NETWORK_DIR=local-devnet ./spin-node.sh --node zeam_0,ream_0 --generateGenesis --popupTerminal
# Run only zeam_0 and qlean_0 nodes (space-separated)
NETWORK_DIR=local-devnet ./spin-node.sh --node "zeam_0 qlean_0" --generateGenesis --popupTerminal
# Run only a single node
NETWORK_DIR=local-devnet ./spin-node.sh --node zeam_0 --generateGenesis --popupTerminal# Start all nodes with metrics enabled
NETWORK_DIR=local-devnet ./spin-node.sh --node all --generateGenesis --metrics-
NETWORK_DIRis an env to specify the network directory. Should have agenesisdirectory with genesis config. Adatafolder will be created inside thisNETWORK_DIRif not already there.genesisdirectory should have the following filesa.
validator-config.yamlwhich has node setup information for all the bootnodes b.validators.yamlwhich assigns validator indices c.nodes.yamlwhich has the enrs generated for each of the respective nodes. d.config.yamlthe actual network config -
--generateGenesisregenerate all genesis files with fresh genesis time and clean data directories -
--popupTerminalif you want to pop out new terminals to run the nodes, opens gnome terminals -
--nodespecify which node(s) you want to run:- Use
allto run all the nodes in a single go - Specify a single node name (e.g.,
zeam_0) to run just that node - Use comma-separated node names (e.g.,
zeam_0,qlean_0) to run multiple specific nodes - Use whitespace-separated node names (e.g.,
"zeam_0 ream_0") to run multiple specific nodes
The client is provided this input so as to parse the correct node configuration to startup the node.
- Use
-
--validatorConfigis the path to specify your nodesvalidator-config.yaml,validators.yaml(for which--nodeis still the node key to index) if your node is not a bootnode. If unspecified it assumes value ofgenesis_bootnodewhich is to say that your node config is to be picked fromgenesisfolder with--nodeas the node key index. This value is further provided to the client so that they can parse the correct config information. -
--metricsenables metrics collection on all nodes. When specified, each client will activate its metrics endpoint according to its implementation. Metrics ports are configured per node invalidator-config.yaml.
Current following clients are supported:
- Zeam
- Ream
- Qlean
However adding a lean client to this setup is very easy. Feel free to do the PR or reach out to the maintainers.
The quickstart includes an automated genesis generator that eliminates the need for hardcoded files and uses validator-config.yaml as the source of truth. This file is to be contained in the genesis folder of the provided NETWORK_DIR folder you want to run quickstart on. Then post genesis generation, the quickstart spins the nodes as per their respective client cmds.
The validator-config.yaml file defines the shuffle algorithm, active epoch configuration, and validator nodes specifications:
shuffle: roundrobin
config:
activeEpoch: 18 # Required: Exponent for active epochs (2^18 = 262,144 signatures)
keyType: "hash-sig" # Required: Network-wide signature scheme (hash-sig for post-quantum security)
validators: # validator nodes specification
- name: "zeam_0" # a 0rth zeam node
privkey: "bdf953adc161873ba026330c56450453f582e3c4ee6cb713644794bcfdd85fe5"
enrFields:
ip: "127.0.0.1"
quic: 9000
metricsPort: 8080
count: 1 # validator keys to be assigned to this nodeRequired Top-Level Fields:
shuffle: Validator assignment (to nodes) shuffle algorithm (e.g.,roundrobin)config.activeEpoch: Exponent for active epochs used in hash-sig key generation (2^activeEpoch signatures per active period)config.keyType: Network-wide signature scheme - must be"hash-sig"for post-quantum security
The spin-node.sh triggers genesis generator (generate-genesis.sh) which generates the following files based on validator-config.yaml:
- post-quantum secure validator keypairs in
genesis/hash-sig-keysunless already generated or forced with--forceKeyGen - config.yaml - With the updated genesis time in short future and pubkeys of the generated keypairs
- validators.yaml - Validator index assignments using round-robin distribution
- nodes.yaml - ENR (Ethereum Node Records) for peer discovery
- genesis.json - Genesis state in JSON format
- genesis.ssz - Genesis state in SSZ format
The genesis generator runs automatically when:
validators.yamlornodes.yamldon't exist, OR- You use the
--generateGenesisflag
# Regenerate genesis files with fresh genesis time
NETWORK_DIR=local-devnet ./spin-node.sh --node all --generateGenesisYou can also run the generator standalone:
./generate-genesis.sh local-devnet/genesisTool's Docker Image: HASH_SIG_CLI_IMAGE="blockblaz/hash-sig-cli:latest"
Source: https://github.com/blockblaz/hash-sig-cli
Using the above docker tool the following files are generated (unless already generated or forced via --forceKeyGen flag):
Generated files:
local-devnet/genesis/hash-sig-keys/
├── validator-keys-manifest.yaml # Metadata for all keys
├── validator_0_pk.json # Public key for validator 0
├── validator_0_sk.json # Secret key for validator 0
├── validator_1_pk.json # Public key for validator 1
├── validator_1_sk.json # Secret key for validator 1
└── ... # Keys for additional validators
Signature Scheme: The system uses the SIGTopLevelTargetSumLifetime32Dim64Base8 hash-based signature scheme, which provides:
- Post-quantum security: Resistant to attacks from quantum computers
- Active epochs: as per
config.activeEpochfor e.g. 2^18 (262,144 signatures) - Total lifetime: 2^32 (4,294,967,296 signatures)
- Stateful signatures: Uses hierarchical signature tree structure
Validator Fields:
Hash-sig key files are automatically indexed based on the validator index (first validator uses validator_0_*.json, second uses validator_1_*.json, etc.)
Tool's Docker Image: PK_DOCKER_IMAGE="ethpandaops/eth-beacon-genesis:pk910-leanchain"
Source: ethpandaops/eth-beacon-genesis#36
config.yaml is generated with the appropriate genesis time (in short future) along with the list pubkeys of the validators in the correct sequence. For e.g:
# Genesis Settings
GENESIS_TIME: 1763712794
# Key Settings
ACTIVE_EPOCH: 10
# Validator Settings
VALIDATOR_COUNT: 2
GENESIS_VALIDATORS:
- "4b3c31094bcc9b45446b2028eae5ad192b2df16778837b10230af102255c9c5f72d7ba43eae30b2c6a779f47367ebf5a42f6c959"
- "8df32a54d2fbdf3a88035b2fe3931320cb900d364d6e7c56b19c0f3c6006ce5b3ebe802a65fe1b420183f62e830a953cb33b7804"This config.yaml is consumed by the clients to directly generate the genesis in-client. Note that clients are supposed to ignore genesis.ssz and genesis.json as their formats have not been updated.
validators.yaml is generated for validator index assignments to the nodes:
zeam_0:
- 0
- 3
ream_0:
- 1
- 4
qlean_0:
- 2Recommended: annotated_validators.yaml is also generated and should be preferred by client software as it includes public keys and private key file references directly, eliminating the need for clients to derive key filenames from validator indices:
zeam_0:
- index: 0
pubkey_hex: 4b3c31094bcc9b45446b2028eae5ad192b2df16778837b10230af102255c9c5f72d7ba43eae30b2c6a779f47367ebf5a42f6c959
privkey_file: validator_0_sk.json
- index: 3
pubkey_hex: 8df32a54d2fbdf3a88035b2fe3931320cb900d364d6e7c56b19c0f3c6006ce5b3ebe802a65fe1b420183f62e830a953cb33b7804
privkey_file: validator_3_sk.json
ream_0:
- index: 1
pubkey_hex: 5b15f72f90bd655b039f9839c36951454b89c605f8c334581cfa832bdd0c994a1350094f7e22617d77607b067b0aa2439e0ead7d
privkey_file: validator_1_sk.json
- index: 4
pubkey_hex: 71bf8f73980591574de34a0db471da74f5cfd84d4731d53f47bf3023b26c2638ac5bd24993ea71492fedbd6c4afe5c299213b76b
privkey_file: validator_4_sk.json
qlean_0:
- index: 2
pubkey_hex: b87e69568a347d1aa811cc158634fb1f4e247c5509ad2b1652a8d758ec0ab0796954e307b97dd6284fbb30088c2e595546fdf663
privkey_file: validator_2_sk.jsonnodes.yaml provide enrs of all the nodes so that clients don't have to run a discovery protocol:
- enr:-IW4QMn2QUYENcnsEpITZLph3YZee8Y3B92INUje_riQUOFQQ5Zm5kASi7E_IuQoGCWgcmCYrH920Q52kH7tQcWcPhEBgmlkgnY0gmlwhH8AAAGEcXVpY4IjKIlzZWNwMjU2azGhAhMMnGF1rmIPQ9tWgqfkNmvsG-aIyc9EJU5JFo3Tegys
- enr:-IW4QDc1Hkslu0Bw11YH4APkXvSWukp5_3VdIrtwhWomvTVVAS-EQNB-rYesXDxhHA613gG9OGR_AiIyE0VeMltTd2cBgmlkgnY0gmlwhH8AAAGEcXVpY4IjKYlzZWNwMjU2azGhA5_HplOwUZ8wpF4O3g4CBsjRMI6kQYT7ph5LkeKzLgTSPost genesis generation, the quickstarts loads and calls the appropriate node's client cmd from client-cmds folder where either docker or binary cmd is picked as per the node_setup mode. (Generally binary mode is handy for local interop debugging for a client).
Client Integration: Your client implementation should read these environment variables and use the hash-sig keys for validator operations.
$item- the node name for which this cmd is being executed, index intovalidator-config.yamlfor its configuration$configDir- the abs folder housinggenesisconfiguration (same asNETWORK_DIRenv variable provided while executing shell command), already mapped to/configin the docker mode- A generic data folder is created inside config folder accessible as
$dataDirwith$dataDir/$itemto be used as the data dir for a particular node to be used for binary format, already mapped to/datain the docker mode - Variables read and available from
validator-config.yaml(use them or directly read configuration from thevalidator-config.yamlusing$itemas the index intovalidatorssection)$metricsPort$quicPort$item.keyfilename of the p2pprivkeyread and dumped into file fromvalidator-config.yamlinside config dir (so$configDir/$item.keyor/config/$item.key)
Here is an example client cmd:
#!/bin/bash
#-----------------------qlean setup----------------------
# expects "qlean" submodule or symlink inside "lean-quickstart" root directory
# https://github.com/qdrvm/qlean-mini
node_binary="$scriptDir/qlean/build/src/executable/qlean \
--modules-dir $scriptDir/qlean/build/src/modules \
--genesis $configDir/config.yaml \
--validator-registry-path $configDir/validators.yaml \
--bootnodes $configDir/nodes.yaml \
--data-dir $dataDir/$item \
--node-id $item --node-key $configDir/$privKeyPath \
--listen-addr /ip4/0.0.0.0/udp/$quicPort/quic-v1 \
--metrics-port $metricsPort"
node_docker="--platform linux/amd64 qdrvm/qlean-mini:dd67521 \
--genesis /config/config.yaml \
--validator-registry-path /config/validators.yaml \
--bootnodes /config/nodes.yaml \
--data-dir /data \
--node-id $item --node-key /config/$privKeyPath \
--listen-addr /ip4/0.0.0.0/udp/$quicPort/quic-v1 \
--metrics-port $metricsPort"
# choose either binary or docker
node_setup="docker"Each hash-sig key has a finite lifetime of 2^32 signatures. The keys are structured as:
- Active epochs: 2^18 epochs before requiring key rotation
- Total lifetime: 2^32 total signatures possible
Hash-based signatures are stateful - each signature uses a unique one-time key from the tree. Once exhausted, keys must be rotated:
# Regenerate all hash-sig keys
./generate-genesis.sh local-devnet/genesisWarning: Keep track of signature counts to avoid key exhaustion.
Secret keys are highly sensitive:
⚠️ Never commitvalidator_*_sk.jsonfiles to version control⚠️ Never share secret keys- ✅ Backup secret keys in secure, encrypted storage
- ✅ Restrict permissions on key files (e.g.,
chmod 600)
The .gitignore should already exclude hash-sig keys:
local-devnet/genesis/hash-sig-keys/
The manifest file (validator-keys-manifest.yaml) contains metadata about all generated keys:
# Hash-Signature Validator Keys Manifest
# Generated by hash-sig-cli
key_scheme: SIGTopLevelTargetSumLifetime32Dim64Base8
hash_function: Poseidon2
encoding: TargetSum
lifetime: 4294967296
log_num_active_epochs: 10
num_active_epochs: 1024
num_validators: 2
validators:
- index: 0
pubkey_hex: 0x4b3c31094bcc9b45446b2028eae5ad192b2df16778837b10230af102255c9c5f72d7ba43eae30b2c6a779f47367ebf5a42f6c959
privkey_file: validator_0_sk.json
- index: 1
pubkey_hex: 0x8df32a54d2fbdf3a88035b2fe3931320cb900d364d6e7c56b19c0f3c6006ce5b3ebe802a65fe1b420183f62e830a953cb33b7804
privkey_file: validator_1_sk.json
Problem: Hash-sig keys not loading during node startup
Warning: Hash-sig public key not found at genesis/hash-sig-keys/validator_0_pk.json
Solution: Run the genesis generator to create keys:
./generate-genesis.sh local-devnet/genesisProblem: Hash-sig key file not found
Warning: Hash-sig secret key not found at genesis/hash-sig-keys/validator_5_sk.json
Solution: This usually means you have more validators configured than hash-sig keys generated. Regenerate genesis files:
./generate-genesis.sh local-devnet/genesisThis quickstart includes automated configuration parsing:
- Official Genesis Generation: Uses PK's
eth-beacon-genesisdocker tool from PR #36 - Complete File Set: Generates
validators.yaml,nodes.yaml,genesis.json,genesis.ssz, and.keyfiles - QUIC Port Detection: Automatically extracts QUIC ports from
validator-config.yamlusingyq - Node Detection: Dynamically discovers available nodes from the validator configuration
- Private Key Management: Automatically extracts and creates
.keyfiles for each node - Error Handling: Provides clear error messages when nodes or ports are not found
The system reads all configuration from YAML files, making it easy to add new nodes or modify existing ones without changing any scripts.
Clients can maintain their own branches to integrated and use binay with their repos as the static targets (check git diff main zeam_repo, it has two nodes, both specified to run zeam for sim testing in zeam using the quickstart generated genesis).
And those branches can be rebased as per client convinience whenever the main code is updated.