Validator Guide
Run a Lichen validator node to participate in consensus, produce blocks, and earn staking rewards. This guide covers hardware requirements, installation, configuration, staking, monitoring, and production deployment.
Requirements
Hardware
Operating System
- Production: Linux — Ubuntu 22.04 LTS or newer (Debian, RHEL 9+ also supported)
- Development: macOS (Apple Silicon or Intel)
Network Ports
| Network | P2P (QUIC) | RPC (HTTP) | WebSocket | Signer |
|---|---|---|---|---|
| Testnet | 7001 |
8899 |
8900 |
9201 |
| Mainnet | 8001 |
9899 |
9900 |
9201 |
Installation
Option A: Binary Quick Start (Recommended for Agents)
If the machine already has a lichen-validator binary from a release bundle or prior
build, you can start a validator immediately without cloning the repository.
The intended agent workflow is: download the latest signed release artifact for the host platform,
extract it, keep the bundled zk-prove, lichen-genesis,
lichen, and contracts/ bundle beside the validator, copy the bundled
seeds.json into a writable state directory, and run the validator with
--auto-update=apply.
Linux x86_64
VERSION=$(curl -fsSL https://api.github.com/repos/lobstercove/lichen/releases/latest | jq -r .tag_name)
curl -LO "https://github.com/lobstercove/lichen/releases/download/${VERSION}/lichen-validator-linux-x86_64.tar.gz"
curl -LO "https://github.com/lobstercove/lichen/releases/download/${VERSION}/SHA256SUMS"
grep 'lichen-validator-linux-x86_64.tar.gz' SHA256SUMS | sha256sum -c -
tar xzf lichen-validator-linux-x86_64.tar.gz --strip-components=1
chmod +x lichen-validator lichen-genesis lichen zk-prove
mkdir -p "$HOME/.lichen/state-mainnet"
cp seeds.json "$HOME/.lichen/state-mainnet/seeds.json"
export LICHEN_KEYPAIR_PASSWORD='set-a-long-random-secret-before-first-start'
./lichen-validator \
--network mainnet \
--rpc-port 9899 \
--ws-port 9900 \
--p2p-port 8001 \
--db-path "$HOME/.lichen/state-mainnet" \
--auto-update=apply
macOS Apple Silicon
VERSION=$(curl -fsSL https://api.github.com/repos/lobstercove/lichen/releases/latest | jq -r .tag_name)
curl -LO "https://github.com/lobstercove/lichen/releases/download/${VERSION}/lichen-validator-darwin-aarch64.tar.gz"
curl -LO "https://github.com/lobstercove/lichen/releases/download/${VERSION}/SHA256SUMS"
grep 'lichen-validator-darwin-aarch64.tar.gz' SHA256SUMS | shasum -a 256 -c -
tar xzf lichen-validator-darwin-aarch64.tar.gz --strip-components=1
chmod +x lichen-validator lichen-genesis lichen zk-prove
mkdir -p "$HOME/.lichen/state-mainnet"
cp seeds.json "$HOME/.lichen/state-mainnet/seeds.json"
export LICHEN_KEYPAIR_PASSWORD='set-a-long-random-secret-before-first-start'
./lichen-validator \
--network mainnet \
--rpc-port 9899 \
--ws-port 9900 \
--p2p-port 8001 \
--db-path "$HOME/.lichen/state-mainnet" \
--auto-update=apply
Windows x64 (PowerShell)
$version = (Invoke-RestMethod https://api.github.com/repos/lobstercove/lichen/releases/latest).tag_name
Invoke-WebRequest -Uri "https://github.com/lobstercove/lichen/releases/download/$version/lichen-validator-windows-x86_64.tar.gz" -OutFile "lichen-validator-windows-x86_64.tar.gz"
tar -xzf .\lichen-validator-windows-x86_64.tar.gz --strip-components=1
New-Item -ItemType Directory -Force -Path "$HOME\.lichen\state-mainnet" | Out-Null
Copy-Item .\seeds.json "$HOME\.lichen\state-mainnet\seeds.json" -Force
$env:LICHEN_KEYPAIR_PASSWORD = 'set-a-long-random-secret-before-first-start'
.\lichen-validator.exe `
--network mainnet `
--rpc-port 9899 `
--ws-port 9900 `
--p2p-port 8001 `
--db-path "$HOME\.lichen\state-mainnet" `
--auto-update=apply
If a release predates Windows packaging, use the source-build workflow for Windows until a newer release is published.
LICHEN_KEYPAIR_PASSWORD before first start and on
every restart. Validator, treasury, genesis-primary, and signer keypair files now use the shared
encrypted-at-rest format, and production starts refuse plaintext keypair files.
mkdir -p "$HOME/.lichen/state-mainnet/home"
cp seeds.json "$HOME/.lichen/state-mainnet/seeds.json"
export LICHEN_KEYPAIR_PASSWORD='set-a-long-random-secret-before-first-start'
env HOME="$HOME/.lichen/state-mainnet/home" \
lichen-validator \
--network mainnet \
--rpc-port 9899 \
--ws-port 9900 \
--p2p-port 8001 \
--db-path "$HOME/.lichen/state-mainnet" \
--auto-update=apply
This is the fastest operational path for agents: validator binary, bundled zk-prove,
ports, state directory, and bundled seed file. If the state directory already exists, the validator
resumes with the same identity and local chain state. In apply mode, auto-update also refreshes
{db-path}/seeds.json from newer release archives.
What happens when the validator starts?
- It creates the chosen state directory if needed.
- It generates or reuses the validator identity in that directory.
- It stores signer material, peer cache, and chain state under the same path.
- It loads
seeds.jsonfrom{db-path},/etc/lichen, or the current directory, then uses the listed seed RPC endpoint to fetch and persist the authoritativegenesis.jsonwhen the state directory is empty. - It imports the canonical opcode-41 genesis state bundle from block 0, verifies it against the block state root, then syncs and replays later blocks from peers.
- It does not copy RocksDB state, genesis wallet files, genesis keys, peer cache, consensus WAL, custody keys, or faucet keys from existing validators.
- It submits validator registration once synced, then starts validating after registration lands and the node is eligible.
- If
--auto-update=applyis enabled, it checks GitHub Releases in the background, stages a newer signed binary, and requests a restart to apply it.
Typical files under the state path include validator identity files, signer material, RocksDB chain
state, known-peers.json, and home/.lichen/ for the persistent native PQ P2P
identity and peer-identity TOFU store. Preserving that directory preserves the validator identity and
local progress.
For new or state-scoped installs, the validator prefers --db-path/home for those P2P runtime
files. If an existing deployment already has node_identity.json under the current process
HOME, it keeps using that identity instead of generating a new node address.
For unattended updates, run the validator under a restart supervisor or service manager. Auto-update handles download and staging; the supervisor handles relaunch.
seeds.json uses seed-01.lichen.network,
seed-02.lichen.network, and seed-03.lichen.network so Lichen can rotate seed
infrastructure through normal release and auto-update flows without forcing operators or agents to
rewrite start commands.
Option B: Repo Workflow
The supported repo-local validator path is the 3-validator launcher. It prepares genesis on validator 1, starts the local cluster, and generates signed metadata once the chain is healthy.
git clone https://github.com/lobstercove/lichen.git
cd lichen
cargo build --release
export LICHEN_KEYPAIR_PASSWORD='local-e2e-secret'
bash scripts/start-local-3validators.sh start-reset
If you need custody, faucet, and post-genesis bootstrap for browser or service testing, extend the local validator path with the full-stack wrapper:
export LICHEN_KEYPAIR_PASSWORD='local-e2e-secret'
./scripts/start-local-stack.sh testnet
./scripts/status-local-stack.sh testnet
Port Assignments
| Network | RPC | WebSocket | P2P | Signer |
|---|---|---|---|---|
| Testnet | 8899 |
8900 |
7001 |
9201 |
| Mainnet | 9899 |
9900 |
8001 |
9201 |
You can run both testnet and mainnet on the same machine — they use different ports and separate data directories.
Local Status / Stop
Use the same launcher family for status checks, clean stops, and full resets:
# Validator-only cluster
bash scripts/start-local-3validators.sh status
bash scripts/start-local-3validators.sh stop
bash scripts/start-local-3validators.sh start-reset
# Full local stack
./scripts/status-local-stack.sh testnet
./scripts/stop-local-stack.sh testnet
./scripts/start-local-stack.sh testnet
Stop / Restart
# Clean local reset
bash scripts/start-local-3validators.sh stop
bash scripts/start-local-3validators.sh start-reset
Option C: Build from Source (Manual)
If you prefer manual control, build the binary and run it directly. Requires Rust 1.75+ stable toolchain.
git clone https://github.com/lobstercove/lichen.git
cd lichen
cargo build --release
export LICHEN_KEYPAIR_PASSWORD='set-a-long-random-secret-before-first-start'
# Start — the built-in supervisor auto-restarts on stalls/crashes
./target/release/lichen-validator \
--network testnet \
--rpc-port 8899 \
--ws-port 8900 \
--p2p-port 7001 \
--db-path ./data/state-testnet
# Disable the supervisor (e.g. when using systemd Restart=on-failure)
./target/release/lichen-validator --no-watchdog \
--network testnet --rpc-port 8899 --p2p-port 7001
Option D: Docker
docker-compose up validator
See the Docker Deployment section below for full details.
Initialize Validator Identity
Generate a validator keypair. This native PQ key is your node's on-chain identity. The start script does this automatically, but you can also do it manually:
lichen init --output ~/.lichen/validator-keypair.json
New validator keypair generated
Identity: Mo1tVa1idAtoR...xYz789
Saved to: ~/.lichen/validator-keypair.json
LICHEN_KEYPAIR_PASSWORD in your operator secret store;
without it, encrypted runtime key files cannot be reopened after restart or restore.
Configuration
Lichen validators are currently configured via CLI flags when starting the binary.
The section labels below (for example [network] and [rpc]) are conceptual
groupings for readability, not literal TOML sections consumed by the validator today. A real
config.toml input format is planned for a future release.
--network, --p2p-port,
--rpc-port, --ws-port, --db-path, --keypair,
--cold-store, --archive-mode, --admin-token,
--no-watchdog, --watchdog-timeout, --max-restarts,
--dev-mode, --import-key. They are documentation aliases, not direct TOML keys
parsed by the current binary.
[validator]
Core validator identity and data paths.
| Key | Type | Default | Description |
|---|---|---|---|
keypair_path |
String | ~/.lichen/validator-keypair.json |
Path to the validator native PQ keypair JSON file. Generate with lichen init.
|
data_dir |
String | ~/.lichen/data |
Directory for blockchain state, ledger, and snapshots. |
enable_validation |
Boolean | true |
Set to false to run as a full-node observer (no block production). |
[history]
Optional archival settings for cold block retention and historical account lookups.
| Flag | Type | Default | Description |
|---|---|---|---|
--cold-store <path> |
String | — | Attach a secondary RocksDB for archival block, transaction, and index retention. The validator migrates data older than the hot retention window into this path every 5 minutes. |
--archive-mode |
Boolean | false |
Persist historical account snapshots so getAccountAtSlot can answer archive
queries. Pair with --cold-store on archival nodes to keep hot-state growth
bounded. |
[network]
Peer-to-peer networking and gossip settings.
| Key | Type | Default | Description |
|---|---|---|---|
p2p_port |
Integer | 7001 |
QUIC transport listening port for P2P gossip and block propagation. Testnet default: 7001, Mainnet: 8001. |
rpc_port |
Integer | 8899 |
JSON-RPC HTTP server port. Testnet default: 8899, Mainnet: 9899. |
seed_nodes |
Array | [] |
Seed-file peer list loaded from seeds.json. Testnet:
["seed-01.lichen.network:7001", "seed-02.lichen.network:7001", "seed-03.lichen.network:7001"]
|
enable_p2p |
Boolean | true |
Enable P2P networking. Disable for local-only testing. |
gossip_interval |
Integer | 10 |
Gossip protocol interval in seconds. |
cleanup_timeout |
Integer | 300 |
Seconds before cleaning up stale peer connections. |
[consensus]
BFT consensus and staking parameters.
| Key | Type | Default | Description |
|---|---|---|---|
min_validator_stake |
Integer | 75000000000000 |
Minimum stake in spores (1 LICN = 1,000,000,000 spores). Mainnet uses 75,000 LICN (75K); testnet genesis uses 75 LICN to keep validator onboarding accessible. Bootstrap validators receive a 100,000 LICN grant on the contributory-stake path. |
slot_duration_ms |
Integer | 400 |
Base slot duration in milliseconds. Actual block time is adaptive: ~800ms for heartbeat blocks, ~200ms with transactions (Tendermint BFT). |
enable_slashing |
Boolean | true |
Enable slashing for double-signing and other protocol violations. |
[graduation]
Validator graduation and anti-fraud settings (CLI flags).
| Flag | Type | Default | Description |
|---|---|---|---|
--dev-mode |
Boolean | false |
Skip machine fingerprint uniqueness check. Allows multiple validators per machine (for local testing). Uses SHA-256(pubkey) as fingerprint. Blocked on mainnet. |
--import-key <path> |
String | — | Import an existing keypair from another machine. Copies <path> to the
validator's data directory. The validator resumes with its existing stake, debt, and
progress. Fingerprint auto-updates on next announcement. |
--import-key /path/to/keypair.json. All progress
(debt, earned rewards, blocks produced) is tied to the keypair, not the machine. The machine fingerprint
auto-updates with a 2-day cooldown between migrations.
[rpc]
JSON-RPC server configuration.
| Key | Type | Default | Description |
|---|---|---|---|
enable_rpc |
Boolean | true |
Enable the JSON-RPC server. |
bind_address |
String | "0.0.0.0" |
Bind address. Use a private interface or reverse proxy when access should stay internal. |
enable_cors |
Boolean | true |
Enable Cross-Origin Resource Sharing for browser clients. |
max_connections |
Integer | 1000 |
Maximum concurrent RPC connections. |
[logging]
Logging verbosity and output targets.
| Key | Type | Default | Description |
|---|---|---|---|
level |
String | "info" |
Log level. One of: trace, debug, info,
warn, error.
|
log_to_file |
Boolean | false |
Write logs to a file in addition to stdout. |
log_file_path |
String | ~/.lichen/validator.log |
Path to the log file (when log_to_file = true). |
log_format |
String | "text" |
Output format. "json" for structured logging (recommended for production). |
[monitoring]
Prometheus metrics and health checks.
| Key | Type | Default | Description |
|---|---|---|---|
enable_metrics |
Boolean | true |
Enable the Prometheus-compatible metrics HTTP endpoint. |
metrics_port |
Integer | 9100 |
Port for the Prometheus metrics server. |
enable_health_check |
Boolean | true |
Enable the /health HTTP endpoint for load balancers. |
[genesis]
Genesis block and chain identity.
| Key | Type | Default | Description |
|---|---|---|---|
genesis_path |
String | "./genesis.json" |
Path to the genesis configuration file. Required on first startup. |
chain_id |
String | "lichen-testnet-1" |
Chain identifier. Must match the genesis file exactly. |
[performance]
Tuning options for throughput and resource usage.
| Key | Type | Default | Description |
|---|---|---|---|
worker_threads |
Integer | 0 |
Tokio worker threads. 0 = auto-detect CPU core count. |
optimize_block_production |
Boolean | true |
Enable block production optimizations (parallel tx execution, pipelining). |
tx_batch_size |
Integer | 1000 |
Maximum transactions per block batch. |
[security]
Security hardening and access control.
| Key | Type | Default | Description |
|---|---|---|---|
check_firewall |
Boolean | true |
Check that required ports are accessible on startup. |
require_encryption |
Boolean | false |
Require TLS for all RPC connections. |
allowed_rpc_methods |
Array | [] |
Whitelist of RPC methods. Empty = all methods enabled. |
rpc_rate_limit |
Integer | 100 |
Maximum RPC requests per second per client. |
admin_token |
String | "" |
Bearer token for admin RPC endpoints. Generate with openssl rand -hex 32. Empty
= admin endpoints disabled. |
[watchdog]
Built-in self-healing supervisor. The validator automatically monitors itself for stalls (deadlocks, resource exhaustion) and restarts when needed. No external scripts or cron jobs required — just run the binary.
| Key | CLI Flag | Default | Description |
|---|---|---|---|
watchdog_timeout |
--watchdog-timeout |
120 |
Seconds of inactivity (no blocks produced or received) before the watchdog triggers a restart. |
max_restarts |
--max-restarts |
50 |
Maximum number of automatic restarts before the supervisor gives up and exits. Set to
0 for unlimited.
|
no_watchdog |
--no-watchdog |
false |
Disable the built-in supervisor entirely. Use this when running under systemd or another process manager that handles restarts. |
--no-watchdog since systemd's
Restart=on-failure already provides external supervision.
--network testnet, --p2p-port, and --rpc-port flags -- everything
else uses sane defaults.
Staking
Validators must stake LICN tokens to participate in consensus. Stake weight determines your share of block production slots and epoch-settled staking rewards. Transaction-fee share is earned as you produce blocks; inflationary staking rewards settle at epoch boundaries.
Stake via CLI
$ lichen stake add 75000000000 --rpc-url https://testnet-rpc.lichen.network
Staking 75 LICN for validator Mo1tVa1idAtoR...xYz789
Transaction: 5bN2...kQrT
Bootstrap: 100,000.000000000 LICN (granted, #47 of 200)
Debt: 100,000.000000000 LICN (repaid from settled epoch rewards: 50% standard, 75% at ≥95% uptime)
Uptime: 98.5% (≥ 95% → 75/25 debt/liquid split)
Status: Finalized (slot 1,204,817)
Validator staked! Your node will enter the active set next epoch.
Minimum Stake
- Bootstrap Grant (first 200 validators): 100,000 LICN
(
100,000,000,000,000spores) — new validators receive this at $0 cost from the treasury - Self-funded (validator 201+): Must provide your own 75,000 LICN minimum — immediately fully vested, no debt
- Vesting (standard): 50% of settled epoch rewards go to debt repayment, 50% liquid — this is the default split for all bootstrap validators
- Performance bonus (≥95% uptime): 75% of settled epoch rewards go to debt
repayment, 25% liquid — accelerates graduation by ~1.5× (via
PERFORMANCE_BONUS_BPS = 15000) - Time cap: All remaining debt is forgiven after 547 days (~18 months) regardless of repayment progress
Graduation System
Bootstrap validators go through a graduation process from Bootstrapping to
FullyVested:
| Path | Reward Split | Condition | Effect |
|---|---|---|---|
| Standard | 50% debt / 50% liquid | Bootstrap debt reaches 0 | Status → FullyVested, 100% of settled rewards become liquid |
| Performance Bonus | 75% debt / 25% liquid | ≥95% uptime (9,500+ basis points) | Same graduation, reached ~1.5× sooner |
| Time Cap | N/A | 547 days since validator started (~118M slots) | Remaining debt forgiven, immediate graduation |
Uptime Calculation
Uptime is calculated in basis points (0–10,000 = 0%–100%) based on actual block production vs. expected share:
expected_blocks = (current_slot - start_slot) / num_active_validators
uptime_bps = min(10000, blocks_produced × 10000 / expected_blocks)
- Fair share: With N active validators, each one is expected to produce 1/N of total blocks. A validator that produces its full share has 100% uptime (10,000 bps).
- Threshold: ≥ 9,500 bps (95%) triggers the performance bonus (75/25 split).
- Edge cases: New validators with no slot history default to 0 bps. If fewer slots have elapsed than validators exist, validators with at least 1 block get benefit of the doubt (10,000 bps).
Anti-Sybil Protection
Each validator collects a machine fingerprint (SHA-256 hash of platform UUID + MAC address) at startup. This fingerprint is:
- Included in & signed with validator announcements
- Registered in the stake pool — one fingerprint per bootstrap validator
- Prevents running 50 validators on one machine to steal 50 bootstrap grants
Rule: One machine = one bootstrap grant. Self-funded validators (201+) are not restricted by fingerprint.
Liquid Staking Mechanism
Lichen uses a delegated proof-of-stake liquid staking model inspired by moss ecosystems:
- Validator Coral: Each validator is a "coral head." Delegators attach stake to a validator, growing the moss.
- Epoch Activation: Validator-set changes settle at epoch boundaries (about 2 days on the live chain). Membership is applied by the protocol's epoch-transition rules, not a simple top-N stake leaderboard.
- Reward Distribution: Transaction-fee share is earned when your validator produces blocks, while staking inflation is distributed proportionally to validators and delegators when the epoch boundary settlement runs.
- Slashing: Double-signing or extended downtime results in a percentage of staked
LICN being burned. With
enable_slashing = true, evidence is gossiped via P2P and processed by theSlashingTracker.
Check Stake Status
$ lichen stake status --rpc-url https://testnet-rpc.lichen.network
Identity: Mo1tVa1idAtoR...xYz789
Stake: 100,000.000000000 LICN (bootstrap)
Delegated: 450.000000000 LICN
Total: 100,450.000000000 LICN
Active: Yes (rank #7 of 21)
Commission: 10%
Last Vote: slot 1,205,142
Monitoring
Prometheus Metrics
When [monitoring] enable_metrics = true, the validator exposes a Prometheus-compatible
endpoint at http://<host>:9100/metrics.
Key Metrics
| Metric | Type | Description |
|---|---|---|
lichen_block_height |
Gauge | Current finalized block height |
lichen_tps |
Gauge | Transactions per second (rolling 10s window) |
lichen_peer_count |
Gauge | Number of connected P2P peers |
lichen_mempool_size |
Gauge | Pending transactions in the mempool |
lichen_slot_duration_ms |
Histogram | Actual slot processing time |
lichen_vote_count |
Counter | Total votes cast by this validator |
lichen_blocks_produced |
Counter | Blocks produced by this validator |
# Fetch metrics
curl -s http://127.0.0.1:9100/metrics | grep lichen_
# Example output
lichen_block_height 1205247
lichen_tps 842
lichen_peer_count 18
lichen_mempool_size 127
Health Check
The RPC server provides a health check via the getHealth JSON-RPC method:
curl -s https://testnet-rpc.lichen.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}'
# Response: {"jsonrpc":"2.0","result":"ok","id":1}
Log Levels
Control verbosity with the RUST_LOG environment variable or the [logging] level
config key:
# Per-module granularity via RUST_LOG
RUST_LOG=info,lichen_p2p=debug,lichen_rpc=warn lichen-validator
# Or set in config.toml
# [logging]
# level = "debug"
# log_format = "json"
Docker Deployment
The recommended production deployment uses Docker Compose. The provided docker-compose.yml
runs the validator, faucet, and optional explorer.
version: "3.8"
services:
validator:
build:
context: .
dockerfile: Dockerfile
container_name: lichen-validator
restart: unless-stopped
ports:
- "7001:7001" # P2P (testnet)
- "8899:8899" # RPC (testnet)
- "8900:8900" # WebSocket (testnet)
- "9100:9100" # Metrics
volumes:
- lichen-data:/var/lib/lichen
environment:
- RUST_LOG=info
- LICHEN_NETWORK=testnet
- LICHEN_RPC_PORT=8899
- LICHEN_WS_PORT=8900
- LICHEN_P2P_PORT=7001
- LICHEN_SIGNER_BIND=127.0.0.1:9201
- LICHEN_ADMIN_TOKEN=${LICHEN_ADMIN_TOKEN:-}
networks:
- lichen
healthcheck:
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8899/ -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getHealth\"}' -H 'Content-Type: application/json' || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
volumes:
lichen-data:
driver: local
networks:
lichen:
driver: bridge
Volume Mounts
| Mount | Purpose |
|---|---|
lichen-data:/var/lib/lichen |
Persistent blockchain state — survives container restarts and upgrades. |
./config.toml:/etc/lichen/config.toml:ro |
Optional configuration file (future). Currently all settings are passed as CLI flags / environment variables in docker-compose. |
Running
# Start in foreground (see logs)
docker-compose up validator
# Start in background
docker-compose up -d validator
# View logs
docker-compose logs -f validator
# Stop
docker-compose down
Health Check
Docker will automatically restart the validator if the getHealth RPC call fails 3
consecutive times. The start_period: 15s gives the node time to initialize before checks
begin.
Systemd Deployment
For bare-metal Linux servers, install the release binary under a systemd service. Managed-host provisioning automation is intentionally outside the public repo.
Systemd Unit
The public repo ships the validator binary and service templates. Operators can adapt the unit file to their own host paths and secret-management system:
# Example: install the release binary, then install a systemd unit
sudo install -m 755 lichen-validator /usr/local/bin/lichen-validator
sudo install -m 644 deploy/lichen-validator.service /etc/systemd/system/lichen-validator.service
sudo systemctl daemon-reload
sudo systemctl enable --now lichen-validator
A production unit should perform the same high-level setup:
- Creates a dedicated
lichensystem user/group (no login shell) - Creates directories:
/etc/lichen,/var/lib/lichen,/var/log/lichen - Copies binaries to
/usr/local/bin/ - Generates
/etc/lichen/env-testnetand/or/etc/lichen/env-mainnetwith correct port assignments - Installs a network-specific systemd service and keeps secrets in an operator-managed store
Systemd Unit File
The service reads its port configuration from the environment file. Key flags map directly to the validator binary:
[Unit]
Description=Lichen Validator Node
Documentation=https://developers.lichen.network/validator.html
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=lichen
Group=lichen
# --no-watchdog: systemd handles restarts via Restart=on-failure
ExecStart=/usr/local/bin/lichen-validator --no-watchdog \
--network ${LICHEN_NETWORK} \
--rpc-port ${LICHEN_RPC_PORT} \
--ws-port ${LICHEN_WS_PORT} \
--p2p-port ${LICHEN_P2P_PORT} \
--db-path /var/lib/lichen/state-${LICHEN_NETWORK}
Restart=on-failure
RestartSec=10
LimitNOFILE=65536
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/lichen /var/log/lichen
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
# Environment
Environment=RUST_LOG=info
EnvironmentFile=/etc/lichen/env
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=lichen-validator
[Install]
WantedBy=multi-user.target
NoNewPrivileges,
ProtectSystem=strict, ProtectHome, PrivateTmp, and kernel
protection directives. The process runs as the unprivileged lichen user with write
access only to /var/lib/lichen and /var/log/lichen.
Service Management
# Start the testnet validator
sudo systemctl start lichen-validator-testnet
# Enable on boot
sudo systemctl enable lichen-validator-testnet
# Check status
sudo systemctl status lichen-validator-testnet
# Follow logs
journalctl -u lichen-validator-testnet -f
# Restart after config change
sudo systemctl restart lichen-validator-testnet
# Mainnet uses the same commands with -mainnet suffix
sudo systemctl start lichen-validator-mainnet
macOS LaunchAgent
On macOS, use a LaunchAgent to run the validator as a persistent background service that starts automatically on login. This approach downloads the latest release binary and keeps it up to date.
Create the Wrapper Script
The wrapper script downloads the latest release binary (using semver sorting, not GitHub's
latest endpoint), verifies its checksum, installs the bundled seeds.json,
keeps the bundled zk-prove, lichen-genesis, lichen,
and contracts/ artifacts, and then
execs the validator.
mkdir -p ~/.lichen/bin ~/.lichen/state-mainnet ~/.lichen/logs
cat > ~/.lichen/bin/lichen-service.sh << 'WRAPPER'
#!/bin/bash
set -euo pipefail
INSTALL_DIR="$HOME/.lichen/bin"
BINARY="$INSTALL_DIR/lichen-validator"
STATE_DIR="$HOME/.lichen/state-mainnet"
LOG_DIR="$HOME/.lichen/logs"
REPO="lobstercove/lichen"
# Set ASSET based on architecture
ARCH=$(uname -m)
case "$ARCH" in
arm64|aarch64) ASSET="lichen-validator-darwin-aarch64.tar.gz" ;;
x86_64) ASSET="lichen-validator-darwin-x86_64.tar.gz" ;;
*) echo "Unsupported arch: $ARCH"; exit 1 ;;
esac
mkdir -p "$INSTALL_DIR" "$STATE_DIR" "$LOG_DIR"
# Download binary if not present
if [ ! -x "$BINARY" ]; then
echo "$(date): Downloading latest release..."
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# Fetch highest semver tag (not GitHub "latest" which sorts by publish date)
VERSION=$(/usr/bin/curl -fsSL "https://api.github.com/repos/$REPO/releases" \
| /usr/bin/python3 -c "
import sys, json
releases = [r for r in json.load(sys.stdin) if not r['draft'] and not r['prerelease']]
releases.sort(key=lambda r: [int(x) for x in r['tag_name'].lstrip('v').split('.')], reverse=True)
print(releases[0]['tag_name'])
")
echo "$(date): Latest version: $VERSION"
/usr/bin/curl -fSL -o "$TMPDIR/$ASSET" \
"https://github.com/$REPO/releases/download/${VERSION}/${ASSET}"
/usr/bin/curl -fSL -o "$TMPDIR/SHA256SUMS" \
"https://github.com/$REPO/releases/download/${VERSION}/SHA256SUMS"
cd "$TMPDIR"
grep "$ASSET" SHA256SUMS | shasum -a 256 -c -
tar xzf "$ASSET" --strip-components=1
mv lichen-validator "$BINARY"
mv zk-prove "$INSTALL_DIR/zk-prove"
mv lichen-genesis "$INSTALL_DIR/lichen-genesis"
mv lichen "$INSTALL_DIR/lichen"
cp -R contracts "$INSTALL_DIR/contracts"
chmod +x "$BINARY" "$INSTALL_DIR/zk-prove" "$INSTALL_DIR/lichen-genesis" "$INSTALL_DIR/lichen"
cp seeds.json "$STATE_DIR/seeds.json"
cd /
echo "$(date): Installed lichen-validator $VERSION"
trap - EXIT
rm -rf "$TMPDIR"
fi
# Start the validator
echo "$(date): Starting lichen-validator..."
exec "$BINARY" \
--network mainnet \
--p2p-port 8001 \
--rpc-port 9899 \
--ws-port 9900 \
--db-path "$STATE_DIR" \
--auto-update=apply
WRAPPER
chmod +x ~/.lichen/bin/lichen-service.sh
Install the LaunchAgent
cat > ~/Library/LaunchAgents/network.lichen.validator.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>network.lichen.validator</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>$HOME/.lichen/bin/lichen-service.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>15</integer>
<key>StandardOutPath</key>
<string>$HOME/.lichen/logs/validator.log</string>
<key>StandardErrorPath</key>
<string>$HOME/.lichen/logs/validator.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>RUST_LOG</key>
<string>info</string>
</dict>
</dict>
</plist>
EOF
Manage the Service
# Load and start (runs immediately and on every login)
launchctl load ~/Library/LaunchAgents/network.lichen.validator.plist
# Stop the validator
launchctl unload ~/Library/LaunchAgents/network.lichen.validator.plist
# Restart (stop then start)
launchctl unload ~/Library/LaunchAgents/network.lichen.validator.plist
sleep 2
launchctl load ~/Library/LaunchAgents/network.lichen.validator.plist
# Follow logs
tail -f ~/.lichen/logs/validator.log
# Check if running
launchctl list | grep lichen
# Force re-download of the binary (e.g. after a new release)
launchctl unload ~/Library/LaunchAgents/network.lichen.validator.plist
rm ~/.lichen/bin/lichen-validator
launchctl load ~/Library/LaunchAgents/network.lichen.validator.plist
~/.lichen/bin/lichen-validator |
Binary |
~/.lichen/bin/lichen-service.sh |
Wrapper script |
~/.lichen/state-mainnet/ |
Blockchain state DB |
~/.lichen/logs/validator.log |
Stdout/stderr log |
~/Library/LaunchAgents/network.lichen.validator.plist |
LaunchAgent plist |
Windows Service
On Windows, use NSSM (Non-Sucking Service Manager) to run the validator as a Windows service. Alternatively, use a PowerShell scheduled task.
Option A: NSSM (Recommended)
# 1. Download and extract the release
$Version = (Invoke-RestMethod https://api.github.com/repos/lobstercove/lichen/releases/latest).tag_name
$Url = "https://github.com/lobstercove/lichen/releases/download/$Version/lichen-validator-windows-x86_64.tar.gz"
$InstallDir = "$env:LOCALAPPDATA\Lichen"
New-Item -ItemType Directory -Force -Path "$InstallDir\bin", "$InstallDir\state-mainnet", "$InstallDir\logs"
Invoke-WebRequest -Uri $Url -OutFile "$env:TEMP\lichen.tar.gz"
tar -xzf "$env:TEMP\lichen.tar.gz" -C "$InstallDir\bin" --strip-components=1
# 2. Install NSSM (https://nssm.cc/download)
# Extract nssm.exe to a directory in your PATH
# 3. Create the service
nssm install LichenValidator "$InstallDir\bin\lichen-validator.exe"
Copy-Item "$InstallDir\bin\seeds.json" "$InstallDir\state-mainnet\seeds.json" -Force
nssm set LichenValidator AppParameters "--network mainnet --p2p-port 8001 --rpc-port 9899 --ws-port 9900 --db-path $InstallDir\state-mainnet --auto-update=apply"
nssm set LichenValidator AppDirectory "$InstallDir"
nssm set LichenValidator AppStdout "$InstallDir\logs\validator.log"
nssm set LichenValidator AppStderr "$InstallDir\logs\validator.log"
nssm set LichenValidator AppEnvironmentExtra "RUST_LOG=info" "USERPROFILE=$InstallDir"
nssm set LichenValidator Start SERVICE_AUTO_START
nssm set LichenValidator AppRestartDelay 10000
# 4. Start
nssm start LichenValidator
Option B: PowerShell Scheduled Task
$InstallDir = "$env:LOCALAPPDATA\Lichen"
$Action = New-ScheduledTaskAction `
-Execute "$InstallDir\bin\lichen-validator.exe" `
-Argument "--network mainnet --p2p-port 8001 --rpc-port 9899 --ws-port 9900 --db-path $InstallDir\state-mainnet --auto-update=apply" `
-WorkingDirectory "$InstallDir"
$Trigger = New-ScheduledTaskTrigger -AtStartup
$Settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Seconds 15)
Register-ScheduledTask -TaskName "LichenValidator" `
-Action $Action -Trigger $Trigger -Settings $Settings `
-RunLevel Highest -User "SYSTEM"
# Start immediately
Start-ScheduledTask -TaskName "LichenValidator"
Service Management (Windows)
# NSSM commands
nssm status LichenValidator
nssm start LichenValidator
nssm stop LichenValidator
nssm restart LichenValidator
# View logs
Get-Content "$env:LOCALAPPDATA\Lichen\logs\validator.log" -Tail 50 -Wait
# Remove service
nssm stop LichenValidator
nssm remove LichenValidator confirm
%LOCALAPPDATA%\Lichen\state-mainnet\ |
Blockchain state DB |
%LOCALAPPDATA%\Lichen\logs\ |
Validator log files |
Shielded Runtime
The live shielded verifier consumes native Plonky3 proof envelopes directly. Validators do not load
proving-key or verification-key bundles at startup, and no shared zk/ cache is required
for shielded verification.
Operationally, this means a validator installation only needs the standard binaries, contract artifacts,
and state/log directories. The shielded pool RPC surface now reports the active scheme via
zkScheme / zk_scheme instead of publishing verification-key hashes.
Troubleshooting
Port Conflicts
If the validator fails to start with "address already in use":
# Find what's using the port (testnet example)
sudo lsof -i :7001 # P2P
sudo lsof -i :8899 # RPC
# The port scheme is fixed per network — see the Installation section above
Insufficient Stake
Your node won't produce blocks without meeting the minimum stake. Check your balance and stake:
lichen balance --rpc-url https://testnet-rpc.lichen.network
lichen stake status --rpc-url https://testnet-rpc.lichen.network
Sync Failures
If your node can't sync with the network:
- Verify
seed_nodesare correct and reachable - Check that the P2P port is open in your firewall (
sudo ufw allow 7001/tcpfor testnet) - Ensure
chain_idmatches the network you're connecting to - Try increasing log level:
RUST_LOG=debug,lichen_p2p=trace
# Test connectivity to seed nodes
nc -zv seed-01.lichen.network 7001
# Check current block height via RPC (testnet port 8899)
curl -s https://testnet-rpc.lichen.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getBlockHeight"}' | jq .result
Disk Full
The ledger grows over time. Monitor disk usage and plan for growth:
# Check data directory size
du -sh /var/lib/lichen/
# Check available disk
df -h /var/lib/lichen/
# Set up monitoring alert (example with Prometheus Alertmanager)
# Alert when disk usage > 80%
Debug Commands
# Full node status dump (testnet RPC port)
curl -s https://testnet-rpc.lichen.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}' | jq
# Peer list
curl -s https://testnet-rpc.lichen.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getClusterNodes"}' | jq
# Validator set
curl -s https://testnet-rpc.lichen.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getValidators"}' | jq
# Check systemd logs for crash details
journalctl -u lichen-validator-testnet --since "1 hour ago" --no-pager
# Run validator with maximum verbosity (testnet)
RUST_LOG=trace lichen-validator --network testnet --rpc-port 8899 --ws-port 8900 --p2p-port 7001 --db-path /var/lib/lichen/state-testnet 2>&1 | head -200
#validators channel or open an issue on GitHub with your logs and config (redact
your keypair and admin token).