Get Started
Become a LightChain Mainnet Validator
Production guide for running an external validator on the LightChain mainnet from a Linux server with a public IP.Audience. This doc assumes a dedicated Linux VM or bare-metal box — the standard target for any serious validator. Running a real mainnet validator from a laptop is strongly discouraged — every sleep cycle costs missed attestations against real economic value.
Network parameters
Public peer endpoints
LightChain mainnet runs two public sentries. Use both for redundancy — single-sentry config works but loses peers entirely if that one sentry is down or rotates its keys. EL bootnodes (devp2p enodes):CodeHTML
CodeHTML
EL pubkey rotation. A copy of the EL enode pubkey is also published athttps://storage.googleapis.com/lightchain-mainnet-public/chain-config/enode-pubkey.txt, but at the time of writing that file is stale and will fail discv5 handshake — its pubkey starts952786e9...while the live process advertises79ea19ff.... The published file consistently lags the live process across chain resets. Use the inlined value above. If it also fails (e.g. after a chain reset), see Troubleshooting → "Geth has 0 peers".
Hardware
Anything Hetzner / OVH / Latitude / Equinix would sell as a small dedicated server, or a 4-core cloud VM (AWSc6i.xlarge, GCP e2-standard-4, etc.) is plenty.
Server prerequisites
- OS: Ubuntu 22.04 LTS or Debian 12 (other distros work; commands assume
apt/ufw) - Docker Engine: install from docs.docker.com/engine/install — not Docker Desktop
- Foundry:
castfor the deposit transaction - Standard tools:
jq,curl,openssl,git
CodeBASH
Networking
The server needs a public IP and the following ports open inbound:CodeBASH
CodeBASH
Genesis artifacts
Three files are needed in this working directory before launching the node:genesis.json— EL (geth) genesis, used withgeth initgenesis.ssz— CL (Prysm) genesis state, SSZ-serializedconfig.yml— CL chain config (slot times, fork versions, deposit contract)
CodeBASH
9200, fork 0x10000089):
CodeBASH
Container images
Mainnet uses immutable commit-hash tags — there is no:latest. Pin the currently-deployed versions explicitly:
CodeHTML
Run the node
Working directory
CodeBASH
genesis.json, genesis.ssz, config.yml) into this directory if you fetched them elsewhere.
One-time setup
Bundle the runtime env vars into a.env file so they survive new shell sessions — every section below depends on these:
CodeBASH
At the top of every new shell session for the rest of this guide: cd /opt/lightchain-validator && source .env
JWT shared between EL and CL:
CodeBASH
Verify$FEE_RECIPIENTis set before launching containers. Empty or malformed values cause Prysm to crash withFATAL --suggested-fee-recipient is not a valid Ethereum addressand enter a restart loop.CodeBASH
Initialize geth from genesis (one-time)
CodeBASH
Run geth (EL)
--network host gives geth direct access to the server's network stack — no Docker NAT, no port mapping, lower latency.
CodeBASH
--authrpc.addr=127.0.0.1— Engine API only reachable from the same host (the local prysm container)--http.addr=127.0.0.1— JSON-RPC only reachable locally; never expose this publicly without an auth proxy--nat=extip:$PUBLIC_IP— geth advertises this IP to peers, so they can dial back
Run prysm beacon (CL)
Uses the public Beacon API for checkpoint sync — finalized state downloaded over HTTPS in seconds, then live P2P from genesis is unnecessary.CodeBASH
Verify both clients are healthy
CodeBASH
is_syncing: falseandis_optimistic: falseandel_offline: false- Local geth's block number matches the public RPC's
Become a validator
Step 1 produces unrecoverable losses if done wrong — a deposit signed against the wrong fork version will be rejected by the LightChain consensus client and the funds will be permanently stuck in the deposit contract. Do not substitute tools without verifying the producedfork_versionmatches0x10000089.
Step 1 — Generate validator keys
LightChain key generation usesprotolambda/eth2-val-tools, which accepts the custom fork version and deposit amount that LightChain requires.
The widely-knownSet up directories and a wallet password:staking-deposit-clidoes not work for LightChain — its--chainparameter accepts only predefined networks (mainnet, sepolia, holesky, …) and hardcodes both the fork version and the 32 ETH deposit amount.
CodeBASH
CodeBASH
CodeBASH
CodeBASH
0x01 (execution-layer) withdrawal credentials:
CodeBASH
CodeBASH
pubkey— your validator's BLS pubkey (48 bytes)withdrawal_credentials—0x01+ 11 zero bytes + your withdrawal addressvalue: 500000000000000(= 500k LCAI in gwei)signatureanddeposit_data_root
validator-keys/mnemonic.txtvalidator-keys/wallet-password.txtvalidator-keys/deposit_data.json
Step 2 — Submit the deposit transaction
This sends 500k LCAI from your funder EOA to the deposit contract. It is irreversible — once the transaction lands, those funds are staked and can only be recovered via a validator exit.CodeBASH
CodeBASH
500000 LCAI plus a small gas buffer.
Send the deposit:
CodeBASH
status: 1 (success) in the receipt.
Step 3 — Run the validator client
CodeBASH
Tail the logs:--chain-config-fileis required. Without it, the validator client falls back to mainnet Ethereum defaults (12s slots, not LightChain's 6s) and computes the wrong current slot — you'll seeError getting validator duties: start slot N is smaller than the minimum valid start slot Mand the validator never attests. The startup log lineRunning on custom Ethereum network specified in a chain configuration YAML fileconfirms the flag is honored.
CodeBASH
Opened validator wallet keymanagerKind=directProposer settings loaded ... suggested-fee-recipient=0x...Initialized gRPC connection provider endpoints=[127.0.0.1:4000]Health status changed current=true previous=false url=127.0.0.1:4000Validator deposited, entering activation queue after finalization pubkey=0x... status=DEPOSITEDNo active validator keys provided. Waiting until next epoch to check again...
Line 6 reads alarming but is expected. Prysm's wording suggests the wallet is empty; what it actually means is "I have your key loaded, but it's not yet eligible to attest". The pairing of #5 (which prints your pubkey) and #6 in successive lines is the healthy pre-activation heartbeat. If your keys were genuinely missing, you'd see #6 alone with no Validator deposited ... pubkey=0x... above it.
The last line repeats every epoch (~36 s) until your validator becomes active_ongoing. Once it transitions, you'll see:
Validator activated index=N pubkey=0x... status=ACTIVESchedule for epoch X attesterCount=1 proposerCount=0Submitted new attestations slot=...(every epoch when assigned)
Step 4 — Watch for activation
You can query the public Beacon API directly:CodeBASH
CodeBASH
CodeHTML
active_ongoing, typically 45–65 min, dominated by EPOCHS_PER_ETH1_VOTING_PERIOD = 64 (≈ 38 min per voting period). The chain applies your deposit's eth1_data only at the end of a voting period with ≥50% supermajority, so the wait depends on where in the period the deposit lands:
- Deposit lands early in a period → may catch the same period's vote → ~30–40 min
- Deposit lands mid/late → misses the boundary → has to wait the full next period → ~60–75 min
block.body.deposits[], the validator joins state.validators[] with pending_initialized, and 1–2 epochs later it transitions through pending_queued to active_ongoing.
Sanity check while waiting: if state.eth1_data.deposit_count is still the old value but state.eth1_data_votes shows entries for the new value, the chain is mid-voting-period and you're on track:
CodeBASH
Stale-period gotcha — don't panic if every vote isOnce"0". During the first ~38 min after your deposit lands, you may observeeth1_data_votesaccumulate entirely fordeposit_count: "0"even though your deposit is already on the EL and cached by Prysm. This is the in-progress voting period that started before your deposit was visible to proposers — its votes were already locked in for the pre-deposit eth1 state. The pattern resolves at the next period boundary, when a fresh period starts and proposers begin voting for your deposit's count. Watch for theunique_votesset to flip from["0"]to["1"](or higher); once ≥50% of slots in a period vote for the new count and the period boundary passes,eth1_data.deposit_countupdates and your validator joins the registry within 1–2 epochs. Concretely, the worst-case path is: deposit lands mid-period → that period closes with all-zeros votes → next period opens and accumulates new-count votes → that period closes and applies the new eth1_data → 1–2 epochs topending_initialized→ 1–2 epochs toactive_ongoing. End-to-end this can take up to ~80 min. Don't intervene unless that exceeds.
active_ongoing, the validator client log emits every epoch (~36 s):
CodeHTML
Troubleshooting
Validator stuck at DEPOSITED for >1 hour
The chain processes deposits at the end of each eth1 voting period (EPOCHS_PER_ETH1_VOTING_PERIOD = 64, ≈ 38 min). One full period after the deposit lands, your validator should appear as pending_initialized and progress to active_ongoing within minutes.
If after two voting periods (≈ 80 min) the public Beacon API still returns Unknown validator, open an issue with the deposit transaction hash and the output of the health checks below.
Quick health check:
CodeBASH
Geth has 0 peers / Bad discv5 packet
First, verify your firewall actually opened 30303 TCP+UDP — ufw status verbose should list both. If both rules are present and your IP is reachable from the public internet, the bootnode enode pubkey may be stale (pubkeys can rotate after chain resets, and inlined values in this doc may briefly lag).
Re-pull this doc to make sure the EL_BOOTNODES and CL_PEER_* values in your .env match the latest enodes / multiaddrs in the "Public peer endpoints" section above. If they differ, update .env, then recreate geth (chain data on disk is preserved — only the runtime flag changes):
CodeBASH
.env already match the doc and peers are still 0 after 10+ minutes, open an issue with the output of:
CodeBASH
Validator log: start slot N is smaller than the minimum valid start slot M
Validator client is computing slots with the wrong SECONDS_PER_SLOT. Cause: missing --chain-config-file. Add --chain-config-file=/work/config.yml and recreate the container.
Validator log: FATAL --suggested-fee-recipient is not a valid Ethereum address
$FEE_RECIPIENT was empty or malformed when the container started. Set it to your funded EOA (full 0x + 40 hex chars) and recreate the container.
Beacon stays is_optimistic: true indefinitely
The CL is processing blocks but the EL hasn't validated their execution payloads. Geth is either still syncing or has lost peers.
CodeBASH
Operations
Stop everything
CodeBASH
./geth/, ./beacon/, and ./validator-keys/. Restarting the containers picks up where they left off.
Tear down completely
CodeBASH
Keep validator-keys/mnemonic.txt somewhere safe before this — it's the only path to recover the validator after teardown if you didn't exit it.
Voluntary exit
Usewealdtech/ethdo to generate a signed SignedVoluntaryExit against the public beacon, then broadcast it via the Beacon REST API.
Prysm's ownPrerequisite: validator must have beenvalidator accounts voluntary-exitandprysmctl validator exitproduce signatures with the wrong fork version on LightChain (the signing-domain code path silently uses mainnet Ethereum defaults regardless of--chain-config-file). The public beacon rejects withInvalid exit: signature did not verify. Until that's patched upstream, ethdo is the working path on this chain.
active_ongoing for at least SHARD_COMMITTEE_PERIOD (256) epochs (~2.5 hours at 36 s/epoch) before it's eligible to exit. Pre-eligible exits are accepted into the pool but never included.
-
Generate the signed exit (uses your validator mnemonic, fetches chain config from the public beacon's
/eth/v1/config/specendpoint):Output is aCodeBASHSignedVoluntaryExitJSON object:CodeJSON -
Broadcast via the public beacon's REST API:
Expected:CodeBASH
HTTP 200with empty body. (HTTP 400 withsignature did not verifymeans ethdo couldn't reach a beacon configured with LightChain's chain config — verify the--connectionURL.) -
Verify the exit was included in a block (within ~1 epoch):
Status progression — each transition triggered by an epoch boundary:CodeBASH
-
Predict the withdrawal time once status is
active_exiting:CodeBASHwithdrawable_epoch(=exit_epoch + 256) is when your stake becomes sweep-eligible. Multiply by36 s/epochand add the chain's genesis time to get the wall-clock timestamp. Query genesis time dynamically (don't hardcode — it changes on chain resets):CodeBASH -
Confirm the stake landed on the EL after status flips to
withdrawal_done:The withdrawal address inherits the full 500,000 LCAI (less any prior partial-withdrawal sweeps ifCodeBASHeffective_balanceever exceededMAX_EFFECTIVE_BALANCE, which on this chain is impossible because both equal 500k LCAI).
Operational notes during exit
- Keep the validator client running between broadcast and
exit_epoch. Your validator still has attestation duties through that window — stopping it early racks up missed attestations and inactivity penalties that reduce the exit balance. - The 256-epoch withdrawal delay is fixed by
MIN_VALIDATOR_WITHDRAWABILITY_DELAY. There is no expedited path. Plan for ~2.5 h betweenexit_epochand the EL credit landing. - Withdrawal sweep ordering is round-robin by validator index. With ~66 validators on the chain and
MAX_WITHDRAWALS_PER_PAYLOAD = 16per block, every validator gets visited within ~5 blocks of becoming withdrawable. Expectwithdrawal_donewithin 30–60 s ofwithdrawable_epoch. - Once
withdrawal_done, you can safely tear down the node — the validator role is finished. Use the "Tear down completely" recipe above.
withdrawal_done, the full stake lands at the EL address encoded in your withdrawal credentials — no manual claim required for 0x01-prefix credentials.
Related guides
- Mainnet Overview — chain basics, public endpoints, and how to connect.
- Mainnet Contract Addresses — protocol contracts, service EOAs, and whitelisted models.
- Mainnet Governance — Governor, Timelock, and proposal lifecycle.
- Run a Worker on Mainnet — the worker role (separate from validator).