Tool for generating proofs for delayed validator exits in the Lido ecosystem. This bot monitors validator exit requests and generates cryptographic proofs when validators fail to exit by their required deadline, enabling penalty enforcement on node operators.
The Late Proof Verifier Bot is a critical component of Lido's validator exit delay monitoring system. It:
- Monitors beacon chain for validator exit requests
- Detects delayed exits when validators haven't exited by their deadline
- Generates proofs using Merkle tree cryptography for delayed validator exits
- Submits verification to the ValidatorExitDelayVerifier contract
- Enables penalty enforcement on node operators whose validators are delayed
The bot runs as a daemon that continuously processes beacon chain roots:
The algorithm is as follows
-
Get finalized beacon chain root
- Fetches the latest finalized beacon chain header
- Determines the previous root to process (from storage, START_ROOT, or parent root)
-
Process block range
- Processes execution layer blocks between previous and current beacon chain roots
- Discovers validator exit requests from ValidatorsExitBusOracle events
-
Group validators by deadline
- Analyzes exit requests and groups validators by their exit deadline slots
- Calculates when each validator should have exited based on activation time
-
Generate proofs for delayed exits
- For each validator past its deadline, generates a Merkle proof of its current state
- Supports both current slot verification and historical slot verification using historical summaries
-
Submit to verifier contract
- Calls
verifyValidatorExitDelay()orverifyHistoricalValidatorExitDelay() - Enables penalty application on node operators with delayed validators
- Calls
-
Store progress and repeat
- Saves the last processed root to storage
- Sleeps for configured interval (default 5 minutes) and repeats the process
Key Features:
- Sequential processing: Processes beacon chain roots sequentially to avoid missing blocks
- Crash recovery: Resumes from the last processed root after restart
- Historical verification: Can generate proofs for older slots using historical summaries
- Batch processing: Efficiently handles multiple validators per transaction
- Comprehensive monitoring: Tracks processing via Prometheus metrics
- Node.js 16+
- Access to Ethereum execution layer RPC
- Access to Ethereum consensus layer API
- Private key for transaction signing (if not in dry-run mode)
-
Clone and install dependencies
git clone https://github.com/lidofinance/late-prover-bot.git cd late-prover-bot yarn install -
Generate contract types
yarn run typechain
-
Build the project
yarn build
-
Create environment file
cp .env.example .env
-
Fill in the required variables (see Environment Variables section below)
# Start daemon with monitoring stack
docker-compose up -d daemon prometheus
# View logs
docker-compose logs -f daemon
# Access metrics
curl http://localhost:8081/metrics# Development mode
NODE_OPTIONS=--max-old-space-size=8192 yarn run start:dev
# Production mode
yarn run start:prod| Name | Description | Required | Default |
|---|---|---|---|
| Core Settings | |||
WORKING_MODE |
Working mode: daemon |
no | daemon |
HTTP_PORT |
Port for HTTP server (health/metrics) | no | 8080 |
DRY_RUN |
Dry run mode (no transactions) | no | false |
DAEMON_SLEEP_INTERVAL_MS |
Sleep interval between daemon cycles (milliseconds) | no | 300000 (5 minutes) |
CHAIN_ID |
Ethereum chain ID (1=mainnet, 5=goerli, 17000=holesky) | yes | |
| Blockchain Connection | |||
EL_RPC_URLS |
Comma-separated execution layer RPC URLs | yes | |
EL_RPC_RETRY_DELAY_MS |
Delay between EL RPC retries | no | 500 |
EL_RPC_RESPONSE_TIMEOUT_MS |
EL RPC response timeout | no | 60000 |
EL_RPC_MAX_RETRIES |
Maximum EL RPC retries | no | 3 |
CL_API_URLS |
Comma-separated consensus layer API URLs | yes | |
CL_API_RETRY_DELAY_MS |
Delay between CL API retries | no | 500 |
CL_API_RESPONSE_TIMEOUT_MS |
CL API response timeout | no | 60000 |
CL_API_MAX_RETRIES |
Maximum CL API retries | no | 3 |
FORK_NAME |
Ethereum consensus layer fork name (fallback if not in headers) | no | electra |
| Contracts | |||
LIDO_LOCATOR_ADDRESS |
Lido Locator contract address | yes | |
TX_SIGNER_PRIVATE_KEY |
Private key for transaction signing | yes (if not dry run) | |
| Transaction Settings | |||
TX_MIN_GAS_PRIORITY_FEE |
Minimum gas priority fee (wei) | no | 50000000 (0.05 gwei) |
TX_MAX_GAS_PRIORITY_FEE |
Maximum gas priority fee (wei) | no | 10000000000 (10 gwei) |
TX_GAS_PRIORITY_FEE_PERCENTILE |
Gas priority fee percentile | no | 25 |
TX_GAS_FEE_HISTORY_DAYS |
Days of gas fee history | no | 1 |
TX_GAS_FEE_HISTORY_PERCENTILE |
Gas fee history percentile | no | 50 |
TX_GAS_LIMIT |
Hard upper limit for gas (transactions will be rejected if estimated gas exceeds this) | no | 2000000 |
VALIDATOR_BATCH_SIZE |
Maximum validators per transaction | no | 50 |
MAX_TRANSACTION_SIZE_BYTES |
Maximum transaction size in bytes | no | 100000 |
TX_MINING_WAITING_TIMEOUT_MS |
Transaction mining timeout | no | 3600000 (1 hour) |
TX_CONFIRMATIONS |
Required confirmations | no | 1 |
| Startup Options | |||
START_ROOT |
Start from specific beacon chain root | no | |
START_SLOT |
Start from specific beacon chain slot | no | |
START_EPOCH |
Start from specific beacon chain epoch | no | |
| Logging | |||
LOG_LEVEL |
Log level (debug, info, warn, error) |
no | info |
LOG_FORMAT |
Log format (simple, json) |
no | simple |
Before running the bot, you can estimate current gas costs on mainnet using the standalone gas estimation script:
# Basic usage (uses default public RPC)
yarn estimate-gas
# With your own RPC
EL_RPC_URLS=https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY yarn estimate-gas
# With custom configuration
TX_GAS_FEE_HISTORY_DAYS=3 TX_GAS_FEE_HISTORY_PERCENTILE=75 yarn estimate-gasWhat it does:
- ✅ Fetches current mainnet base fee
- ✅ Analyzes historical gas data (1 day by default)
- ✅ Calculates recommended gas fee using percentile
- ✅ Estimates costs for small/typical/large batches
- ✅ Shows if bot would send transactions now
- ✅ Displays costs in Gwei, ETH, and USD
Example output:
Current base fee: 25.3 Gwei
Recommended (50th percentile): 28.5 Gwei
Status: ✅ ACCEPTABLE
Typical Batch (~2.2M gas):
Gas Cost: 55,660,000 Gwei
ETH Cost: 0.055660 ETH
USD Cost: $138.35
✅ DECISION: Bot would send transactions now.
See scripts/README.md for full documentation.
curl http://localhost:8081/healthThe bot uses dynamic gas estimation with hard limits:
- Estimates gas required for each transaction using
eth_estimateGas - Adds 20% buffer to account for gas variation
- Enforces hard limit:
TX_GAS_LIMITis a hard upper bound - Rejects transaction if estimated gas (with buffer) exceeds
TX_GAS_LIMIT
Example behavior:
- Configured:
TX_GAS_LIMIT=2000000 - Estimated:
1,500,000gas - Used:
1,800,000gas (1.5M × 1.2 buffer) ✅ - If estimated:
2,500,000gas (with buffer:3,000,000)- ❌ Transaction rejected: Exceeds hard limit of
2,000,000 - Error message shows required minimum:
TX_GAS_LIMIT=3000000
- ❌ Transaction rejected: Exceeds hard limit of
If you encounter UNPREDICTABLE_GAS_LIMIT errors:
- Check logs: Look for the estimated gas amount before the error
- Increase gas limit: Set
TX_GAS_LIMITto estimated × 1.5 for safety - Check contract state: Ensure your validators and exit requests are valid
- Network issues: Verify your RPC endpoints are stable and responsive
If you see "Transaction rejected: Estimated gas exceeds hard limit":
- Check the error message: It will show the required gas limit
Estimated gas (with buffer): 2,500,000 Configured hard limit (TX_GAS_LIMIT): 2,000,000 Required: TX_GAS_LIMIT must be at least 2,500,000 - Increase TX_GAS_LIMIT: Set it to at least the required amount
TX_GAS_LIMIT=2500000
- Or reduce batch size: Process fewer validators per transaction
VALIDATOR_BATCH_SIZE=25 # Reduce from default 50
If you see "intrinsic gas too low: gas X, minimum needed Y":
- Set higher gas limit: Use the "minimum needed" value + 20% buffer
# If error shows "minimum needed 1588836" TX_GAS_LIMIT=1906600 # 1588836 * 1.2
- Check batch size: Larger batches need more gas - consider reducing
VALIDATOR_BATCH_SIZE
- 1-10 validators:
TX_GAS_LIMIT=1500000(with safety margin) - 11-25 validators:
TX_GAS_LIMIT=2000000(with safety margin) - 26-50 validators:
TX_GAS_LIMIT=3000000(with safety margin) - 51+ validators:
TX_GAS_LIMIT=4000000+or reduce batch size
Note: TX_GAS_LIMIT is a hard upper limit. Transactions will be rejected if estimated gas (with 20% buffer) exceeds this value.
If you encounter "oversized data" or "transaction size limit exceeded" errors:
- Reduce batch size: Set
VALIDATOR_BATCH_SIZE=25(or lower) - Set size limit: Adjust
MAX_TRANSACTION_SIZE_BYTES=50000for stricter limits - Monitor logs: Check how many validators are being processed per batch
- RPC provider limits: Some providers have stricter size limits (Alchemy: 128KB, Infura: varies)
Example configuration for large validator sets:
VALIDATOR_BATCH_SIZE=20
MAX_TRANSACTION_SIZE_BYTES=80000
TX_GAS_LIMIT=2500000The application automatically splits large validator groups into smaller batches to prevent oversized transactions and provides detailed batch processing logs.
The bot exposes comprehensive metrics at /metrics endpoint:
# View all metrics
curl http://localhost:8081/metrics
# View custom metrics
curl http://localhost:8081/metrics | grep late_prover_bot- Proof Generation: Duration and success rates
- Validator Processing: Processed, skipped, and eligible validators
- Contract Interactions: Call duration and verification counts
- Block Processing: Range processing and batch operations
- Memory Usage: Heap usage and RSS memory
- Daemon Operations: Cycle duration and sleep tracking
- Error Tracking: Various error counters
The project includes a complete monitoring setup:
# Start with Prometheus
docker-compose up -d daemon prometheus
# Access Prometheus UI
open http://localhost:9090
# Uncomment Grafana in docker-compose.yml for dashboards
# open http://localhost:8082 (admin/MYPASSWORT)# Build for production
yarn build
# Build and watch for changes
yarn run start:dev# Unit tests
yarn test
# E2E daemon tests
yarn run test-daemon
# Test coverage
yarn run test:cov# Check code style
yarn run lint
# Fix code style issues
yarn run lint:fix
# Format code
yarn run format- DaemonService: Main orchestrator running the processing loop
- RootsProcessor: Processes beacon chain roots and block ranges
- RootsProvider: Provides next roots to process with crash recovery
- ProverService: Generates Merkle proofs for delayed validator exits
- Contract Services: Interact with Lido contracts (ValidatorExitDelayVerifier, StakingRouter, etc.)
- Consensus/Execution Providers: Interface with beacon chain and execution layer
- Root Discovery: RootsProvider determines next beacon chain root to process
- Block Range Processing: RootsProcessor handles execution layer blocks
- Exit Request Detection: ProverService discovers exit requests from events
- Validator Analysis: Groups validators by deadline and checks exit status
- Proof Generation: Creates Merkle proofs for delayed validators
- Contract Submission: Submits proofs to ValidatorExitDelayVerifier contract
- Progress Tracking: Stores last processed root and updates metrics
GPL-3.0
For issues and questions, please open an issue on the GitHub repository.