A comprehensive Ethereum bootnode implementation supporting both Execution Layer (EL) and Consensus Layer (CL) peer discovery with intelligent fork-aware filtering.
Note: This project is under active development and not yet production-ready. Use at your own risk.
bootnodoor provides a unified bootnode service with multiple protocol implementations:
A complete dual-stack bootnode supporting both EL and CL networks:
- Dual Layer Support: Run EL-only, CL-only, or both simultaneously
- Dual Protocol Support: Discovery v4 (discv4) and Discovery v5 (discv5)
- Protocol Multiplexing: Both protocols share a single UDP port
- Fork-Aware Filtering:
- EL: EIP-2124 fork ID validation via
ethENR field - CL: Fork digest validation via
eth2ENR field with grace periods
- EL: EIP-2124 fork ID validation via
- Separate Routing Tables: Independent tables for EL and CL nodes (500 nodes each by default)
- Intelligent Node Routing: Only serves compatible nodes to requesters
- LAN-Aware Filtering: Prevents leaking private network topology
- IP Discovery: Automatic external IP detection from PONG consensus
- Web UI: Real-time statistics dashboard with EL/CL breakdowns
Legacy protocol for Execution Layer discovery:
- Full Wire Protocol: PING/PONG, FINDNODE/NEIGHBORS, ENRREQUEST/ENRRESPONSE
- Bond Mechanism: Bidirectional PING/PONG required before serving nodes
- Storm Prevention: Protection against ping-pong amplification attacks
- ENR Support: Supports both modern ENR and legacy enode URLs
- EL-Specific: Only used for Execution Layer nodes
Modern encrypted protocol for both EL and CL:
- Protocol Handler: Full discv5 wire protocol (PING/PONG, FINDNODE/NODES, TALKREQ/TALKRESP)
- Session Management: Encrypted messaging with WHOAREYOU handshakes
- UDP Transport: Network communication with per-IP rate limiting
- ENR Support: Full Ethereum Node Records implementation
- DoS Protection: Bounded pending maps with LRU eviction and per-IP limits
- Cross-Layer: Used by both EL and CL nodes
Single bootnode binary supporting all Ethereum network types:
- Execution Layer: Ethereum mainnet and testnets (EL nodes)
- Consensus Layer: Beacon chain nodes (CL nodes)
- Dual-Stack Mode: Serve both EL and CL simultaneously on the same port
- Protocol Agnostic: Supports both discv4 (EL) and discv5 (EL+CL)
- Fork ID Validation: Validates nodes based on
ethENR field - Chain Compatibility: Ensures nodes are on the correct chain and fork
- Historical Acceptance: Accepts any valid fork from chain history
- Network Isolation: Mainnet nodes won't be served to testnet peers
- Fork Digest Validation: Validates nodes based on
eth2ENR field - Grace Period Support: Accepts nodes from previous forks during transitions (default: 60 minutes)
- Fork Awareness: Automatically updates accepted fork digests based on beacon chain schedule
- Quality Assurance: Only serves nodes that have been validated and pinged successfully
This ensures that peers connecting to the bootnode receive only valid, reachable nodes from their specific network and fork.
- Context-Driven: Graceful shutdown via context cancellation
- Memory Efficient: Bounded data structures with configurable limits (500 nodes per table by default)
- Attack Resistant: Multi-layer DoS protection (per-IP limits, rate limiting, bond mechanism, storm prevention)
- Observable: Comprehensive statistics and web UI for monitoring
- Database Persistence: SQLite storage with automatic migrations
- Self-Configuring: Automatic external IP discovery from PONG consensus
go build -o bootnodoor ./cmd/bootnodoor./bootnodoor \
--private-key "$(openssl rand -hex 32)" \
--bind-port 30303 \
--enr-ip $(curl -s ifconfig.me) \
--el-config ./config-mainnet-el.json \
--el-genesis-hash 0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3 \
--el-genesis-time 1438269988 \
--cl-config ./config-mainnet-cl.yaml \
--genesis-validators-root 0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95 \
--nodedb ./data/mainnet.db \
--web-ui./bootnodoor \
--private-key "$(openssl rand -hex 32)" \
--bind-port 30303 \
--enr-ip $(curl -s ifconfig.me) \
--el-config ./config-mainnet-el.json \
--el-genesis-hash 0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3 \
--el-genesis-time 1438269988 \
--el-bootnodes "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303" \
--nodedb ./data/mainnet-el.db \
--web-ui./bootnodoor \
--private-key "$(openssl rand -hex 32)" \
--bind-port 9000 \
--enr-ip $(curl -s ifconfig.me) \
--cl-config ./config-mainnet-cl.yaml \
--genesis-validators-root 0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95 \
--cl-bootnodes "enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg" \
--nodedb ./data/mainnet-cl.db \
--web-uiFor Execution Layer:
-
--el-config <path>: Path to EL chain config file (JSON)- Contains fork schedule and network parameters
- Example: Mainnet, Sepolia, Holesky configs
-
--el-genesis-hash <hex>: Genesis block hash (0x-prefixed hex)- Used to compute fork IDs
- Example:
0xd4e56740...for Ethereum mainnet
-
--el-genesis-time <unix>: Genesis block timestamp (Unix time)- Example:
1438269988for Ethereum mainnet
- Example:
For Consensus Layer:
-
--cl-config <path>: Path to CL beacon config file (YAML)- Contains fork schedule, genesis config, and network parameters
- Example files:
config-mainnet.yaml,config-sepolia.yaml
-
--genesis-validators-root <hex>: Genesis validators root (0x-prefixed hex)- Used to compute fork digests
- Unique per network (mainnet, sepolia, holesky, etc.)
Common Required:
-
--private-key <hex>: Node private key (64 hex characters, optional 0x prefix)- Used for node identity and ENR signing
- Keep this secret! Anyone with this key can impersonate your node
-
--enr-ip <ip>: Public IPv4 address to advertise in ENR- This is the address other nodes will use to connect to you
- Must be reachable from the internet
- Can be omitted for automatic discovery from PONG responses
--bind-addr <ip>: IP address to bind UDP socket (default:0.0.0.0)--bind-port <port>: UDP port to bind (default:30303)--enr-ip6 <ip>: Optional IPv6 address to advertise--enr-port <port>: UDP port to advertise (default: use--bind-port)
--enable-el: Enable execution bootnode (default:true)--enable-cl: Enable consensus bootnode (default:true)
--nodedb <path>: Path to persistent SQLite database file- Stores discovered nodes across restarts with automatic migrations
- Leave empty for in-memory database (no persistence)
- Database contains separate tables for EL and CL nodes
-
--el-bootnodes <enr1,enr2,...>: Comma-separated list of EL bootnode ENRs or enode URLs- Used for initial EL peer discovery
- Supports both ENR and legacy enode format
-
--cl-bootnodes <enr1,enr2,...>: Comma-separated list of CL bootnode ENRs- Used for initial CL peer discovery
- Only ENR format supported
-
--max-active-nodes <count>: Maximum active nodes per table (default:500)- Separate limit for EL and CL tables
-
--max-nodes-per-ip <count>: Maximum nodes to track per IP address (default:10)- Prevents single IPs from dominating the routing table
--grace-period <duration>: Grace period for old CL fork digests (default:60m)- How long to accept nodes from previous forks after transition
- Format:
60m,2h,30s - EL fork IDs accept any historical fork without grace period
--web-ui: Enable web UI dashboard--web-host <ip>: Web UI host (default:0.0.0.0)--web-port <port>: Web UI port (default:8080)--web-sitename <name>: Web UI site name (default:bootnodoor)--pprof: Enable pprof performance profiling endpoints
--log-level <level>: Log level:debug,info,warn,error(default:info)
./bootnodoor \
--private-key "$(openssl rand -hex 32)" \
--bind-port 30303 \
--enr-ip $(curl -s ifconfig.me) \
--el-config ./configs/mainnet-el.json \
--el-genesis-hash 0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3 \
--el-genesis-time 1438269988 \
--cl-config ./configs/mainnet-cl.yaml \
--genesis-validators-root 0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95 \
--nodedb ./data/mainnet.db \
--web-ui \
--web-port 8080./bootnodoor \
--cl-config ./configs/sepolia-cl.yaml \
--genesis-validators-root 0xd8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078 \
--private-key "$(openssl rand -hex 32)" \
--bind-port 9000 \
--enr-ip $(curl -s ifconfig.me) \
--nodedb ./data/sepolia-cl.db \
--web-ui./bootnodoor \
--el-config ./configs/holesky-el.json \
--el-genesis-hash 0xb5f7f912443c940f21fd611f12828d75b534364ed9e95ca4e307729a4661bde4 \
--el-genesis-time 1695902100 \
--private-key "$(openssl rand -hex 32)" \
--bind-port 30303 \
--enr-ip $(curl -s ifconfig.me) \
--nodedb ./data/holesky-el.db \
--web-ui./bootnodoor \
--cl-config ./configs/holesky-cl.yaml \
--genesis-validators-root 0x9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1 \
--private-key "1234567890123456789012345678901212345678901234567890123456789012" \
--bind-addr 127.0.0.1 \
--bind-port 9000 \
--enr-ip 127.0.0.1 \
--log-level debugThe generic discv5 library can be used independently:
package main
import (
"context"
"crypto/ecdsa"
"log"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethpandaops/bootnodoor/discv5"
"github.com/ethpandaops/bootnodoor/discv5/protocol"
)
func main() {
// Generate private key
privKey, _ := ethcrypto.GenerateKey()
// Create configuration
cfg := discv5.DefaultConfig()
cfg.PrivateKey = privKey
cfg.BindPort = 9000
// Set callbacks for protocol events
cfg.OnHandshakeComplete = func(n *node.Node, incoming bool) {
log.Printf("Handshake complete with %s", n.PeerID())
}
cfg.OnFindNode = func(msg *protocol.FindNode) []*node.Node {
// Return nodes from your routing table
return myTable.FindClosest(msg.Distances)
}
// Create service
service, err := discv5.New(cfg)
if err != nil {
log.Fatal(err)
}
// Start service
if err := service.Start(); err != nil {
log.Fatal(err)
}
defer service.Stop()
// Use the service
nodes, err := service.FindNode(targetNode, []uint{256})
if err != nil {
log.Fatal(err)
}
log.Printf("Discovered %d nodes", len(nodes))
}When --web-ui is enabled, access the dashboard at http://localhost:8080 (or your configured port).
The dashboard shows:
- Overview: Real-time EL and CL node counts, database statistics
- EL Nodes Page: List of all EL nodes with fork IDs, protocol support, and quality metrics
- CL Nodes Page: List of all CL nodes with fork digests and statistics
- Fork Information: Current and historical fork data for both layers
- Protocol Metrics: Packets sent/received, handshakes, sessions
- Database Stats: Node counts, quality metrics, protocol support distribution
GET /- Web dashboard overviewGET /el-nodes- EL nodes list pageGET /cl-nodes- CL nodes list pageGET /enr- Local ENR in base64 formatGET /enode- Local enode URL (EL format)GET /metrics- Prometheus metrics (when enabled)GET /debug/pprof/- Performance profiling (when--pprofenabled)
Fork IDs are computed from the EL chain config (JSON):
{
"chainId": 1,
"homesteadBlock": 1150000,
"daoForkBlock": 1920000,
"eip150Block": 2463000,
...
"shanghaiTime": 1681338455,
"cancunTime": 1710338135,
"pragueTime": 1746612311
}The bootnode automatically:
- Computes fork ID from genesis hash and all fork blocks/timestamps
- Validates
ethENR field against chain history - Accepts any valid historical fork ID
- No grace period needed (all historical forks are valid)
Fork digests are computed from the CL beacon config (YAML):
# config-mainnet-cl.yaml
CONFIG_NAME: "mainnet"
# Fork schedule
ALTAIR_FORK_EPOCH: 74240
BELLATRIX_FORK_EPOCH: 144896
CAPELLA_FORK_EPOCH: 194048
DENEB_FORK_EPOCH: 269568
# Fork versions
GENESIS_FORK_VERSION: 0x00000000
ALTAIR_FORK_VERSION: 0x01000000
BELLATRIX_FORK_VERSION: 0x02000000
CAPELLA_FORK_VERSION: 0x03000000
DENEB_FORK_VERSION: 0x04000000
# Network parameters
SECONDS_PER_SLOT: 12
SLOTS_PER_EPOCH: 32The bootnode automatically:
- Computes fork digests:
compute_fork_digest(fork_version, genesis_validators_root)[:4] - Determines current fork based on network time
- Accepts nodes with current fork digest
- Accepts nodes with old fork digests within grace period (default: 60 minutes)
- Deprioritizes but accepts historical fork digests
The bootnode implements multiple layers of DoS protection:
- Rate Limiting: 100 packets/second per IP at transport layer
- Pending Limits: Bounded pending maps for handshakes and challenges
- Per-IP Limits: Max 10 nodes per IP address to prevent Sybil attacks
- LRU Eviction: Oldest entries evicted when limits reached
- Session Limits: Max 1024 concurrent discv5 sessions with 12-hour lifetime
- Bond Mechanism: Discv4 requires bidirectional PING/PONG before serving nodes
- Storm Prevention: Protection against ping-pong amplification attacks
- Bad Node Caching: Tracks rejected nodes to avoid repeated validation
- WAN clients don't receive LAN nodes (RFC1918 filtering)
- Prevents disclosure of private network topology
- Separate handling for LAN and WAN requesters
- Private Key: Generate a unique key for each bootnode, never reuse keys
- Firewall: Allow only UDP traffic on your configured port
- Monitoring: Enable web UI on localhost only or behind authentication/firewall
- Database: Backup node database periodically for faster restarts
- Updates: Keep bootnode updated for latest fork schedule changes
- IP Discovery: Use
--enr-ipto manually set public IP, or rely on automatic discovery
For both layers:
- Check
--enr-ipis your public IP, not0.0.0.0or127.0.0.1 - Verify UDP port is open in firewall:
nc -u -z -v <ip> <port> - Ensure system time is synchronized (use NTP)
- Check bootnode configuration (--el-bootnodes or --cl-bootnodes)
For EL:
- Verify
--el-genesis-hashmatches your network - Check
--el-genesis-timeis correct - Ensure EL config has correct fork schedule
- Verify discv4 is enabled if using enode bootnodes
For CL:
- Verify
--genesis-validators-rootis correct for your network - Ensure CL config has correct fork schedule
- Check
GENESIS_FORK_VERSIONin config file
EL fork ID errors:
- Verify genesis hash and time match the network
- Check EL chain config has correct fork blocks and timestamps
- Ensure config matches the network you're trying to join
CL fork digest errors:
- Verify genesis validators root is correct
- Check CL config file has correct fork versions
- Ensure fork schedule matches the network
- Reduce
--max-active-nodes(default: 500 per table) - Reduce
--max-nodes-per-ip(default: 10) - Enable database persistence with
--nodedb - Disable unused layer (don't configure both EL and CL if only one is needed)
- Add more bootnodes (
--el-bootnodesand/or--cl-bootnodes) - Check network connectivity to initial bootnodes
- Verify protocol settings (--enable-discv4, --enable-discv5)
- For CL: reduce grace period if too permissive:
--grace-period 30m
go test ./...make buildContributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
MIT License - see LICENSE file for details.