Validator Documentation
Complete technical guide to deploying and operating a QELT Mainnet validator node. Covers the installer script, QBFT admission process, security hardening, operational commands, and troubleshooting.
Overview
QELT uses QBFT (Quorum Byzantine Fault Tolerance) with block-header validator selection. Validator admission is governed by on-chain voting — there is no smart contract involved. The protocol is built into the consensus engine itself.
The validator installer is a single idempotent bash script that sets up everything needed to run a production QELT validator node on Ubuntu 22.04/24.04 LTS. It installs Java 21, Hyperledger Besu 25.12.0, configures the genesis, generates keys, sets up systemd, and optionally creates a public HTTPS RPC endpoint.
How Admission Works
New Operator Existing Validators Blockchain
| | |
+-- Runs installer script | |
+-- Node syncs to chain tip | |
+-- Shares validator address -------->| |
| | |
| Validator A calls | |
| proposeValidatorVote ----------------------> |
| | Vote in block header |
| Validator B calls | |
| proposeValidatorVote ----------------------> |
| | Vote in block header |
| Validator C calls | |
| proposeValidatorVote ----------------------> |
| | 3/5 threshold met! |
| | |
|<------------- New validator now participates in consensus ------|Vote Mechanism
qbft_proposeValidatorVote JSON-RPC call
Activation
When on-chain vote threshold is met (recorded in block headers)
Epoch Behavior
Resets unrecorded pending votes; recorded votes persist
Restart Required?
No — once voted in, your node participates automatically
Validator Rewards
QELT validators earn revenue exclusively from transaction fees. There is no block subsidy and no inflation — the fixed 10 billion QELT supply never grows from block rewards.
Reward Structure
| Component | Value | Notes |
|---|---|---|
| Block Reward | 0 QELT | No block subsidy / no inflation |
| Transaction Fees | 100% to block proposer | Entire fee goes to the validator that proposes the block |
| Base Fee | 0 | zeroBaseFee is enabled in genesis |
| Minimum Gas Price | 1,000 wei per gas unit | Configured via --min-gas-price=1000 |
How Validators Earn
When a user sends a transaction, they pay Gas Used × Gas Price (minimum 1,000 wei per gas unit). The entire fee goes to whichever validator proposes the block containing that transaction.
QBFT uses round-robin block proposing. Validators take turns proposing blocks every 5 seconds. With N validators, each validator proposes roughly 17,280 / N blocks per day (~17,280 blocks/day total at 5-second intervals).
Revenue depends entirely on network activity. More transactions = more fees = more validator income. Empty blocks produce zero revenue.
Current Math (5 Validators)
Blocks per day: 17,280 (86,400 seconds / 5 seconds per block) Blocks per validator: 3,456 (17,280 / 5 validators) Example — simple QELT transfer: Gas used: 21,000 Gas price (minimum): 1,000 wei Fee per tx: 21,000 × 1,000 = 21,000,000 wei = 0.000000021 QELT Revenue per block = sum of all transaction fees in that block Revenue per day = sum of fees across all ~3,456 blocks you propose
Key Points
No Staking Requirement
QELT is a permissioned QBFT network, not Proof of Stake. There is no capital lock-up.
No Block Reward Inflation
The 10 billion QELT supply does not grow from block rewards. Supply is fixed.
Revenue Scales with Adoption
As more dApps deploy and users transact, validator fee revenue increases proportionally.
Equal Opportunity
All validators earn roughly equally due to round-robin block proposing. More validators = smaller share per validator.
Server Requirements
You need a dedicated server (VPS or bare-metal) running Ubuntu. The installer validates all requirements at startup and will abort with clear error messages if anything is missing.
| Requirement | Minimum | Recommended |
|---|---|---|
| OS | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS |
| Architecture | x86_64 | x86_64 |
| RAM | 8 GB | 16 GB |
| CPU | 4 cores | 8 cores |
| Storage | 100 GB SSD | 500 GB NVMe |
| Network | 100 Mbps, static IP | 1 Gbps |
| Ports | 30303 TCP+UDP open inbound | Same |
Software installed by the script: OpenJDK 21, Hyperledger Besu 25.12.0, curl, jq, openssl, and optionally nginx + Node.js + certbot for public RPC endpoints.
Installation
SSH into your server and run the following commands. The script requires root (sudo) and is interactive — it will prompt you for decisions at each step.
# Install git if not present sudo apt update && sudo apt install -y git # Clone the repository git clone https://github.com/PRQELT/qelt-validator-node.git # Enter the directory cd qelt-validator-node # Make the installer executable chmod +x install-qelt-validator.sh # Run the installer (requires root) sudo bash install-qelt-validator.sh
Idempotent: The script is safe to re-run. It will NOT overwrite an existing node key unless you explicitly choose to. If Besu is already installed at the correct version, it skips the download.
Alternative one-liner (when available):
curl -sSL https://install.qelt.ai/validator.sh -o install.sh chmod +x install.sh && sudo ./install.sh
Script Walkthrough (10 Steps)
The installer runs through 10 sequential steps. Here is exactly what each step does:
Preflight Checks
Validates your environment before making any changes:
- • Running as root (required for systemd, package install, user creation)
- • OS is Ubuntu 22.04 or 24.04, architecture is x86_64
- • RAM ≥ 8 GB, available disk ≥ 50 GB
- • Port 30303 is not already in use
- • Internet connectivity — probes the bootnode RPC with
eth_chainIdto verify Chain ID 770
If any critical check fails, the script aborts with a clear error message.
Installing Dependencies
- • System packages: curl, wget, jq, openssl, ca-certificates, gnupg, tar, unzip
- • OpenJDK 21 (headless) — the JVM runtime for Besu
- • Verifies Java version after installation
Installing Hyperledger Besu 25.12.0
- • Downloads from GitHub Releases:
besu-25.12.0.tar.gz - • SHA256 checksum verification against pinned hash — aborts on mismatch
- • Extracts to
/opt/besu/ - • Creates symlink:
/usr/local/bin/besu - • Skips download if correct version already installed
Creating System User and Directories
- • Creates
besusystem user (non-login, no home directory) - • Creates directories:
/data/qelt/,/data/qelt/keys/,/etc/qelt/ - • Sets ownership to besu:besu, keys directory chmod 700
Deploying Genesis Configuration
- • Writes production genesis to
/etc/qelt/genesis.json - • SHA256 verification ensures exact match with network
- • Chain ID 770, QBFT consensus, 5-second block period, Cancun EVM
- • The extraData field is RLP-encoded initial validator set — never modify
- • If genesis already exists and matches, it is kept as-is
Validator Key Management
- • New key: Generates secp256k1 private key using
openssl rand -hex 32 - • Import: Paste a hex-encoded private key or provide a file path
- • Existing key: Keep, replace, or import a new one
- • Old keys always backed up before replacement
- • Sets chmod 600 on key file, owned by besu user
- • Extracts and displays your Validator Address
+============================================================+ | YOUR VALIDATOR IDENTITY | +============================================================+ | Validator Address: 0xYOUR_ADDRESS_HERE | | | | WARNING: SAVE THIS ADDRESS -- you will need it to request | | admission to the QELT validator set. | +============================================================+
Configuring Systemd Service
- • Auto-detects public IP (with manual override option)
- • Writes static-nodes.json with bootnode enode URL
- • Calculates JVM heap size based on available RAM (4/6/8 GB)
- • RPC bound to 127.0.0.1:8545 (localhost only) with ETH, NET, QBFT, WEB3, TXPOOL APIs
- • Metrics enabled on 127.0.0.1:9090
- • Bonsai storage format, FULL sync mode
- • Systemd security: ProtectHome, ProtectSystem=strict, NoNewPrivileges, PrivateTmp
- • Service auto-restarts on failure with 10-second delay
Key Besu flags configured:
--data-path=/data/qelt --genesis-file=/etc/qelt/genesis.json --node-private-key-file=/data/qelt/keys/nodekey --bootnodes=enode://710abc...@62.169.25.2:30303 --p2p-host=YOUR_PUBLIC_IP --p2p-port=30303 --discovery-enabled=true --max-peers=25 --rpc-http-enabled --rpc-http-host=127.0.0.1 --rpc-http-port=8545 --rpc-http-api=ETH,NET,QBFT,WEB3,TXPOOL --metrics-enabled --metrics-port=9090 --data-storage-format=BONSAI --sync-mode=FULL --target-gas-limit=50000000 --logging=INFO
Public HTTPS RPC (Optional)
If you choose to expose a public RPC endpoint, the script sets up a full production stack:
- • Node.js RPC middleware (port 8547) — method whitelisting, parameter validation, 1 MB body limit
- • nginx reverse proxy — rate limiting (100r/m + 20r/s burst), connection limits, security headers
- • Let's Encrypt SSL via certbot — automatic HTTPS with auto-renewal
- • Architecture: Internet → nginx (443) → middleware (8547) → Besu (8545)
- • Besu RPC never directly exposed to the internet
The middleware allows 30+ read-only RPC methods and blocks all admin/debug/write methods.
Firewall Configuration
- • Always: 30303/tcp, 30303/udp (P2P), 22/tcp (SSH)
- • If public RPC: Also 80/tcp (ACME validation), 443/tcp (HTTPS)
- • Enables UFW if not already active
Starting and Verifying
- • Starts the systemd service
- • Waits 15 seconds for JVM initialization
- • Verifies service is running
- • Waits for RPC to become responsive (up to 60 seconds)
- • Verifies Chain ID matches 770
- • Checks peer count and reports sync status
- • Starts RPC middleware (if configured)
- • Displays complete summary with email template
- • Saves details to
/data/qelt/VALIDATOR_INFO.txt
Sync & Verification
CRITICAL: Your node MUST be fully synced before requesting admission.
When voted in, the total validator count increases. If your node is offline or not synced, it counts as a fault — potentially reducing the network's fault tolerance to zero, which could halt block production for the entire network.
Monitor Sync Progress
# Watch live logs
sudo journalctl -u besu-qelt-validator -f
# Check sync status (returns "false" when fully synced)
curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' \
http://127.0.0.1:8545 | jq .result
# Check current block number
curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://127.0.0.1:8545 | jq -r .result
# Check peer count (should be >= 1)
curl -s -X POST --data '{"jsonrpc":"2.0","method":"net_peerCount","params":[],"id":1}' \
http://127.0.0.1:8545 | jq -r .resultCompare with Network
# Compare your block height with the network
LOCAL=$(curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://127.0.0.1:8545 | jq -r .result)
REMOTE=$(curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
https://mainnet.qelt.ai | jq -r .result)
echo "Local: $LOCAL | Network: $REMOTE"When both values match (or are within a few blocks), your node is fully synced.
View Your Validator Details
cat /data/qelt/VALIDATOR_INFO.txt besu public-key export-address --node-private-key-file=/data/qelt/keys/nodekey
Validator Admission (For New Operators)
After your node is fully synced, follow these steps to get admitted to the validator set:
Step 1: Confirm Sync
Verify eth_syncing returns false and your block number matches the network.
Step 2: Pay Enrollment Fee
Send 100 USDT or USDC to 0x1ad873Bd75E5bb1a1a3933B3FC1E21F1Da986FA2 on any EVM chain.
Step 3: Email Your Information
Send the following to laurent@qxmp.ai:
- • Payment transaction hash
- • Validator Address (e.g.,
0x1c0dffbe7183...) - • Enode URL (e.g.,
enode://abc...@84.247.133.98:30303) - • Confirmation that your node is fully synced
Find these values anytime: cat /data/qelt/VALIDATOR_INFO.txt
Step 4: Wait for Vote
The QELT team coordinates a vote among existing validators. QBFT requires a majority (currently 3 of 5) to vote for admission. This typically takes 24-48 hours.
Step 5: Verify Admission
Check if your address appears in the validator set:
curl -s -X POST --data '{"jsonrpc":"2.0","method":"qbft_getValidatorsByBlockNumber","params":["latest"],"id":1}' \
http://127.0.0.1:8545 | jq .resultWhen your address appears — congratulations, you are a QELT validator! Your node begins participating in consensus automatically. No restart needed.
For Existing Validators (Voting)
This section is for existing QELT validators who need to vote on admitting a new validator.
Prerequisites
- • Your node must have the QBFT API namespace enabled in
--rpc-http-api - • You must have local RPC access (localhost:8545)
1. Verify the New Node is Ready
Before casting your vote, confirm: the new node is synced, has peers, and you have the correct validator address.
2. Cast Your Vote
# Replace NEW_VALIDATOR_ADDRESS with the actual address
curl -X POST --data '{
"jsonrpc":"2.0",
"method":"qbft_proposeValidatorVote",
"params":["NEW_VALIDATOR_ADDRESS", true],
"id":1
}' http://localhost:8545Expected response: {"jsonrpc":"2.0","id":1,"result":true}
Important Notes About Voting
- • The vote is NOT immediately written to the chain — it is stored locally and included in the next block YOUR node proposes
- • QBFT uses round-robin block proposing, so your vote may take several blocks to appear on-chain
- • You only need to call proposeValidatorVote once — the local proposal persists across epoch boundaries
- • You need 3 of 5 existing validators to vote for the new address
3. Verify the Vote Passed
curl -s -X POST --data '{
"jsonrpc":"2.0",
"method":"qbft_getValidatorsByBlockNumber",
"params":["latest"],
"id":1
}' http://localhost:8545 | jq .result4. Clean Up (Optional)
curl -X POST --data '{
"jsonrpc":"2.0",
"method":"qbft_discardValidatorVote",
"params":["NEW_VALIDATOR_ADDRESS"],
"id":1
}' http://localhost:8545Check Pending Votes
curl -s -X POST --data '{
"jsonrpc":"2.0",
"method":"qbft_getPendingVotes",
"params":[],
"id":1
}' http://localhost:8545 | jq .resultNetwork Information
| Parameter | Value |
|---|---|
| Network Name | QELT Mainnet |
| Chain ID | 770 |
| Consensus | QBFT (Quorum Byzantine Fault Tolerance) |
| Block Time | 5 seconds |
| EVM Version | Cancun |
| Block Finality | Immediate (no reorgs) |
| Gas Limit | 50,000,000 |
| Client | Hyperledger Besu 25.12.0 |
| Epoch Length | 30,000 blocks (~41.7 hours) |
Public RPC Endpoints
https://mainnet.qelt.ai -- Validator 1 (Bootnode) https://mainnet2.qelt.ai -- Validator 2 https://mainnet3.qelt.ai -- Validator 3 https://mainnet4.qelt.ai -- Validator 4 https://mainnet5.qelt.ai -- Validator 5 https://archivem.qelt.ai -- Archive Node (full history)
Bootnode Enode
enode://710abc6491ff7de558de11d6835f64ca10ae3fd58b5a235d5cec068830fbd4e9568ec4e68293232a0a88f242fc7e81703827c9d90cad2bebb7a890cadb4220bc@62.169.25.2:30303
Common Commands
Service Management
sudo systemctl status besu-qelt-validator # Check status sudo systemctl restart besu-qelt-validator # Restart sudo systemctl stop besu-qelt-validator # Stop sudo journalctl -u besu-qelt-validator -f # Live logs sudo journalctl -u besu-qelt-validator --no-pager -n 50 # Last 50 lines
Network Queries (Local RPC)
# Current block number
curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://127.0.0.1:8545 | jq -r .result
# Peer count
curl -s -X POST --data '{"jsonrpc":"2.0","method":"net_peerCount","params":[],"id":1}' \
http://127.0.0.1:8545 | jq -r .result
# Sync status
curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' \
http://127.0.0.1:8545 | jq .result
# Chain ID
curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
http://127.0.0.1:8545 | jq -r .result
# Current validator set
curl -s -X POST --data '{"jsonrpc":"2.0","method":"qbft_getValidatorsByBlockNumber","params":["latest"],"id":1}' \
http://127.0.0.1:8545 | jq .resultKey Management
# View your validator info cat /data/qelt/VALIDATOR_INFO.txt # Export validator address from key besu public-key export-address --node-private-key-file=/data/qelt/keys/nodekey # Export public key besu public-key export --node-private-key-file=/data/qelt/keys/nodekey
Files & Directories
| File / Directory | Purpose |
|---|---|
| /data/qelt/ | Blockchain data directory |
| /data/qelt/keys/nodekey | Your validator private key (back this up!) |
| /data/qelt/static-nodes.json | Bootnode peer entry for resilient discovery |
| /data/qelt/VALIDATOR_INFO.txt | Summary of your node details |
| /etc/qelt/genesis.json | Network genesis configuration |
| /etc/systemd/system/besu-qelt-validator.service | Systemd service unit |
| /opt/besu/ | Besu binary installation |
| /opt/qelt-rpc-validator/ | RPC middleware (if public RPC enabled) |
Security
The installer implements production security best practices:
Dedicated System User
The node daemon runs as besu user, never as root
Systemd Sandboxing
ProtectHome, ProtectSystem=strict, NoNewPrivileges, PrivateTmp
Strict Key Permissions
chmod 600 on node key, chmod 700 on keys directory
RPC Localhost Only
Not exposed to the internet by default
SHA256 Verification
Both Besu binary and genesis file verified against pinned hashes
UFW Firewall
30303 TCP/UDP for P2P, SSH, and optionally 80/443
RPC Method Whitelisting
Node.js middleware blocks admin/debug methods if public RPC enabled
Nginx Rate Limiting
100r/m general + 20r/s burst with security headers
Protect Your Node Key
Your node key (/data/qelt/keys/nodekey) is your validator identity. Back it up securely — if lost, you lose your validator slot. Never share your private key.
Updating
Updating the Installer
cd /root/qelt-validator-node git pull
Updating Besu
When a new Besu version is approved by the QELT team:
sudo systemctl stop besu-qelt-validator # Download and extract the new version (update URL accordingly) sudo rm -rf /opt/besu sudo mkdir -p /opt/besu sudo tar -xzf besu-NEW_VERSION.tar.gz -C /opt/besu --strip-components=1 sudo ln -sf /opt/besu/bin/besu /usr/local/bin/besu # Verify besu --version # Restart sudo systemctl start besu-qelt-validator
Always check Besu release notes for breaking changes before upgrading. Coordinate with the QELT team before updating.
Liveness & Safety Considerations
The 2/3 Rule
QBFT requires 2/3 or more of validators to sign each block. The network can tolerate up to floor((N-1)/3) faulty or offline nodes.
| Total Validators | Fault Tolerance | Min Signers | Risk if Adding Offline Node |
|---|---|---|---|
| 5 | 1 | 4 | Adding 6th offline = fault tolerance consumed |
| 6 | 1 | 4 | Only 1 more failure allowed before halt |
| 7 | 2 | 5 | Safer — can tolerate 2 failures |
Rule: NEVER vote in a node that is not fully synced and peered
If you increase the validator set size with a non-participating node, the fault tolerance does NOT increase. You have used up one fault slot on a node that cannot sign blocks. One more failure = network halt.
Recovery from a Stalled Network
If the network halts due to insufficient validators:
- Restart all available validator nodes
- Ensure P2P connectivity between all alive nodes
- Wait for consensus to resume (QBFT has backoff behavior)
- As a last resort, Besu supports genesis transition overrides to force a validator set change — this requires coordinated manual intervention across all nodes
Removing a Validator
If a validator needs to be removed (compromised key, operator leaves, accidental admission):
# Vote to REMOVE -- note the second parameter is "false"
curl -X POST --data '{
"jsonrpc":"2.0",
"method":"qbft_proposeValidatorVote",
"params":["ADDRESS_TO_REMOVE", false],
"id":1
}' http://localhost:8545Same threshold applies: more than 50% of current validators must vote to remove.
Troubleshooting
Node will not start
sudo journalctl -u besu-qelt-validator --no-pager -n 50
Common causes: port 30303 already in use, insufficient RAM, wrong genesis file, key permissions.
No peers connecting
sudo ss -tlnp | grep 30303 # Check port is open sudo ufw status | grep 30303 # Check firewall
Ensure both TCP and UDP are open on port 30303. Check that your public IP is correctly configured.
Sync stuck at block 0
- • Verify genesis file matches:
sha256sum /etc/qelt/genesis.jsonshould return fa5b3534... - • Check bootnode enode URL in service file
- • Ensure outbound + inbound traffic on port 30303
Method not enabled when calling qbft_proposeValidatorVote
The QBFT API namespace is not enabled. Edit the systemd service to include QBFT:
--rpc-http-api=ETH,NET,QBFT,WEB3,TXPOOL
Then: sudo systemctl daemon-reload && sudo systemctl restart besu-qelt-validator
Vote cast but validator not appearing in set
- • Votes are included when your node proposes a block (round-robin). Wait for a few rounds.
- • Check that 3 of 5 validators have actually voted.
- • If an epoch boundary passes before enough votes accumulate, the unrecorded votes reset — but local proposals persist.
New validator synced but not producing blocks after admission
- • Verify the validator address appears in qbft_getValidatorsByBlockNumber
- • Check that the node private key derives to the correct address:
besu public-key export-address --node-private-key-file=/data/qelt/keys/nodekey
Check logs for consensus errors: sudo journalctl -u besu-qelt-validator --since "10 min ago"
Network stalled after adding new validator
- • If the new node is offline: vote it out immediately using proposeValidatorVote(address, false)
- • Restart all available validators if needed
- • Monitor block production via the public RPC endpoints
Ready to Become a Validator?
Check out the enrollment page for a simplified overview, or head straight to GitHub to clone the installer.
