🟣 Monad Testnet Full Node & Validator Setup Guide
A complete guide to running a Monad testnet full node and registering as a validator
RAID management, TrieDB preparation, snapshot import, service startup, and validator registration — step by step.
Author: HazenNetworkSolutions
Network: Monad Testnet (Chain ID: 10143)
Version: v0.14.3
Last Updated: 19 May 2026
Table of Contents
- Hardware Requirements
- Step 1 — System Verification
- Step 2 — System Update and Dependencies
- Step 3 — Verify Kernel Version
- Step 4 — Disable SMT (HyperThreading)
- Step 5 — CPU Performance Mode and File Descriptor Limits
- Step 6 — Prepare the TrieDB Disk
- Step 7 — Install the Monad Package
- Step 8 — Create User and Directory Structure
- Step 9 — Download Configuration Files
- Step 10 — Generate Keystore Password
- Step 11 — Generate BLS and SECP Keystores
- Step 12 — Configure node.toml
- Step 13 — Remote Configuration URLs
- Step 14 — Firewall Configuration
- Step 15 — Set File Permissions
- Step 16 — Format TrieDB
- Step 17 — Import Snapshot
- Step 18 — Start the Node
- Step 19 — Validator Registration (VDP)
- Step 20 — Update node.toml for Validator
- Step 21 — validator-info PR
- Step 22 — Configure OTEL Metrics Push
- Monitoring the Node
- Staying Updated
Hardware Requirements
| Component | Minimum |
|---|---|
| Operating System | Ubuntu 24.04+ (bare-metal, NOT a VM) |
| CPU | 16 physical cores @ 4.5GHz (AMD Ryzen 7950X/9950X recommended) |
| RAM | 32GB minimum (64GB recommended) |
| Disk 1 | 2TB NVMe SSD (TrieDB — separate, no RAID) |
| Disk 2 | 500GB+ NVMe SSD (OS, logs) |
| Kernel | Linux 6.8.0-60 or higher |
⚠️ SMT/HyperThreading must be disabled via BIOS.
⚠️ The TrieDB disk must be a separate drive with no mounted filesystem and no RAID configured.
Step 1 — System Verification
After SSH-ing into your server, verify the system meets requirements:
lsb_release -a # Should be Ubuntu 24.04
uname -r # Should be 6.8.0-60 or higher
lscpu | grep -E "Model name|CPU\(s\)|Thread|Socket|Core"
free -h # Minimum 32GB RAM
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL
Step 2 — System Update and Dependencies
apt update && apt upgrade -y
apt install -y curl nvme-cli aria2 jq rsync parted cpufrequtils mdadm
If a kernel upgrade was installed, reboot:
reboot
Step 3 — Verify Kernel Version
uname -r
Output should be 6.8.0-60 or higher.
Step 4 — Disable SMT (HyperThreading)
Monad requires SMT to be disabled for optimal performance.
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 nosmt"/' /etc/default/grub
update-grub
reboot
Verify after reboot:
cat /sys/devices/system/cpu/smt/active # Should output 0
nproc # Should output 16 (for Ryzen 7950X)
Step 5 — CPU Performance Mode and File Descriptor Limits
echo 'GOVERNOR="performance"' > /etc/default/cpufrequtils
systemctl restart cpufrequtils
echo "* soft nofile 65536" >> /etc/security/limits.conf
echo "* hard nofile 65536" >> /etc/security/limits.conf
Step 6 — Prepare the TrieDB Disk
⚠️ Formatting the wrong drive will destroy your operating system! Proceed carefully.
List available disks:
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL
Identify the drive with no mountpoints and no RAID. Set it as TRIEDB_DRIVE.
If the disk is part of a RAID1 array, remove it first:
# Check RAID status
cat /proc/mdstat
# Remove disk from all arrays (example with nvme1n1)
mdadm /dev/md2 --fail /dev/nvme1n1p4 && mdadm /dev/md2 --remove /dev/nvme1n1p4
mdadm /dev/md0 --fail /dev/nvme1n1p2 && mdadm /dev/md0 --remove /dev/nvme1n1p2
mdadm /dev/md1 --fail /dev/nvme1n1p3 && mdadm /dev/md1 --remove /dev/nvme1n1p3
# Reboot if root partition is busy
reboot
# Wipe RAID metadata
mdadm --zero-superblock /dev/nvme1n1p2
mdadm --zero-superblock /dev/nvme1n1p3
mdadm --zero-superblock /dev/nvme1n1p4
# Wipe the entire disk
wipefs -a /dev/nvme1n1
Create partition:
parted /dev/nvme1n1 mklabel gpt
parted /dev/nvme1n1 mkpart triedb 0% 100%
Create udev rule (Monad uses /dev/triedb symlink):
PARTUUID=$(lsblk -o PARTUUID /dev/nvme1n1 | tail -n 1)
echo "Disk PartUUID: ${PARTUUID}"
echo "ENV{ID_PART_ENTRY_UUID}==\"$PARTUUID\", MODE=\"0666\", SYMLINK+=\"triedb\"" \
| tee /etc/udev/rules.d/99-triedb.rules
udevadm trigger
udevadm control --reload
udevadm settle
ls -l /dev/triedb # Should point to nvme1n1p1
Verify LBA format (must be 512 bytes):
nvme id-ns -H /dev/nvme1n1 | grep 'LBA Format' | grep 'in use'
Expected output: Data Size: 512 bytes (in use)
If not, fix it:
nvme format --lbaf=0 /dev/nvme1n1
Step 7 — Install the Monad Package
Configure the APT repository:
mkdir -p /etc/apt/keyrings
cat <<EOF > /etc/apt/sources.list.d/category-labs.sources
Types: deb
URIs: https://pkg.category.xyz/
Suites: noble
Components: main
Signed-By: /etc/apt/keyrings/category-labs.gpg
EOF
curl -fsSL https://pkg.category.xyz/keys/public-key.asc \
| gpg --dearmor --yes -o /etc/apt/keyrings/category-labs.gpg
Install the package:
apt update && apt install -y monad
Verify installation:
monad --version
Note:
--chain is requiredoutput is normal — it confirms the binary is working.
Step 8 — Create User and Directory Structure
useradd -m -s /bin/bash monad
mkdir -p /home/monad/monad-bft/config \
/home/monad/monad-bft/ledger \
/home/monad/monad-bft/config/forkpoint \
/home/monad/monad-bft/config/validators
Step 9 — Download Configuration Files
MF_BUCKET=https://bucket.monadinfra.com
curl -o /home/monad/.env $MF_BUCKET/config/testnet/latest/.env.example
curl -o /home/monad/monad-bft/config/node.toml $MF_BUCKET/config/testnet/latest/full-node-node.toml
Step 10 — Generate Keystore Password
sed -i "s|^KEYSTORE_PASSWORD=$|KEYSTORE_PASSWORD='$(openssl rand -base64 32)'|" /home/monad/.env
source /home/monad/.env
mkdir -p /opt/monad/backup/
echo "Keystore password: ${KEYSTORE_PASSWORD}" > /opt/monad/backup/keystore-password-backup
Step 11 — Generate BLS and SECP Keystores
bash <<'EOF'
set -e
source /home/monad/.env
if [[ -z "$KEYSTORE_PASSWORD" || \
-f /home/monad/monad-bft/config/id-secp || \
-f /home/monad/monad-bft/config/id-bls ]]; then
echo "Skipping: missing KEYSTORE_PASSWORD or keys already exist."
exit 1
fi
monad-keystore create \
--key-type secp \
--keystore-path /home/monad/monad-bft/config/id-secp \
--password "${KEYSTORE_PASSWORD}" > /opt/monad/backup/secp-backup
monad-keystore create \
--key-type bls \
--keystore-path /home/monad/monad-bft/config/id-bls \
--password "${KEYSTORE_PASSWORD}" > /opt/monad/backup/bls-backup
grep "public key" /opt/monad/backup/secp-backup /opt/monad/backup/bls-backup \
| tee /home/monad/pubkey-secp-bls
echo "Success: New keystores generated"
EOF
🔐 CRITICAL: Back up the following files to an external location (password manager, external drive, etc.). Without these files, you cannot recover your node identity or validator delegation.
File Location Description id-secp/home/monad/monad-bft/config/id-secpNode identity (SECP keystore) id-bls/home/monad/monad-bft/config/id-blsValidator signing (BLS keystore) node.toml/home/monad/monad-bft/config/node.tomlNode configuration keystore-password-backup/opt/monad/backup/keystore-password-backupKeystore password secp-backup/opt/monad/backup/secp-backupSECP key backup bls-backup/opt/monad/backup/bls-backupBLS key backup Download to your local machine:
scp root@SERVER_IP:/home/monad/monad-bft/config/id-secp ./ scp root@SERVER_IP:/home/monad/monad-bft/config/id-bls ./ scp root@SERVER_IP:/home/monad/monad-bft/config/node.toml ./ scp root@SERVER_IP:/opt/monad/backup/keystore-password-backup ./ scp root@SERVER_IP:/opt/monad/backup/secp-backup ./ scp root@SERVER_IP:/opt/monad/backup/bls-backup ./
Step 12 — Configure node.toml
nano /home/monad/monad-bft/config/node.toml
Edit the following fields:
| Field | Value |
|---|---|
beneficiary | "0x0000000000000000000000000000000000000000" (burn address for full nodes) |
node_name | "full_PROVIDERNAME" |
Sign the Name Record
Get your server's public IP:
curl -s4 ifconfig.me
Generate the signature:
source /home/monad/.env
monad-sign-name-record \
--address <SERVER_IP>:8000 \
--authenticated-udp-port 8001 \
--keystore-path /home/monad/monad-bft/config/id-secp \
--password "${KEYSTORE_PASSWORD}" \
--self-record-seq-num 1
Update the [peer_discovery] section in node.toml:
self_address = "<SERVER_IP>:8000"
self_record_seq_num = 1
self_name_record_sig = "<SIGNATURE_OUTPUT>"
Also verify these settings are correct:
enable_client = trueunder[fullnode_raptorcast]expand_to_group = trueunder[statesync]
Step 13 — Remote Configuration URLs
cat >> /home/monad/.env << 'EOF'
REMOTE_VALIDATORS_URL='https://bucket.monadinfra.com/validators/testnet/validators.toml'
REMOTE_FORKPOINT_URL='https://bucket.monadinfra.com/forkpoint/testnet/forkpoint.toml'
EOF
Step 14 — Firewall Configuration
ufw allow ssh
ufw allow 8000
ufw allow 8001
ufw enable
ufw status
Anti-spam iptables rule:
iptables -I INPUT -p udp --dport 8000 -m length --length 0:1400 -j DROP
Note: This iptables rule resets on reboot. Use
iptables-persistentto make it permanent.
Step 15 — Set File Permissions
chown -R monad:monad /home/monad/
Step 16 — Format TrieDB
systemctl start monad-mpt
journalctl -u monad-mpt -n 14 -o cat
Expected output:
MPT database on storages:
Capacity Used % Path
1.75 Tb 256.03 Mb 0.01% "/dev/nvme1n1p1"
...
monad-mpt.service: Deactivated successfully.
Step 17 — Import Snapshot (Hard Reset)
bash /opt/monad/scripts/reset-workspace.sh
Download and import the latest snapshot:
MF_BUCKET=https://bucket.monadinfra.com
curl -sSL $MF_BUCKET/scripts/testnet/restore-from-snapshot.sh | bash
This takes approximately 5 minutes. Wait for:
Snapshot imported at block ID: XXXXXXXX
Step 18 — Start the Node
systemctl enable monad-bft monad-execution monad-rpc
systemctl start monad-bft monad-execution monad-rpc
Verify all services are running:
systemctl status monad-bft monad-execution monad-rpc --no-pager
All three services should show active (running).
Monitoring the Node
Watch live block commits:
journalctl -u monad-bft -f --no-pager | grep "committed block"
Expected output:
"committed block","num_tx":4,"block_num":22917311
Full logs:
journalctl -u monad-bft -f --no-pager
journalctl -u monad-execution -f --no-pager
journalctl -u monad-rpc -f --no-pager
Service management:
# Restart services
systemctl restart monad-bft monad-execution monad-rpc
# Stop services
systemctl stop monad-bft monad-execution monad-rpc
# Check status
systemctl status monad-bft monad-execution monad-rpc
Staying Updated
- Telegram: Monad Node Announcements
- Discord: Monad Developer Discord
- Official Docs: docs.monad.xyz
Step 19 — Validator Registration (VDP)
This step is only for node operators who have received tokens through the Monad Validator Delegation Program (VDP).
Requirements
- At least 100,000 MON sent to your EOA address
- Full node running and synced to network tip
Install staking-sdk-cli
apt install -y python3.12-venv git
git clone https://github.com/monad-developers/staking-sdk-cli.git
cd staking-sdk-cli
python3 -m venv venv
source venv/bin/activate
pip install .
cp staking-cli/config.toml.example config.toml
Edit config.toml
Fill in the funded_address_private_key field with your EOA private key:
nano config.toml
funded_address_private_key = "0xYOUR_EOA_PRIVATE_KEY"
⚠️ Private key must include the
0xprefix. Keep this file secure, never share it.
Extract Node Private Keys
⚠️ Do NOT use keys from backup files — recover them from the keystore:
source /home/monad/.env
monad-keystore recover --password "$KEYSTORE_PASSWORD" \
--keystore-path /home/monad/monad-bft/config/id-secp --key-type secp
monad-keystore recover --password "$KEYSTORE_PASSWORD" \
--keystore-path /home/monad/monad-bft/config/id-bls --key-type bls
Note the output:
- SECP private key → enter without
0xprefix - BLS private key → enter with
0xprefix
Run addValidator (TUI)
python staking-cli/main.py tui
- Select
1(Add Validator) - Fill in the values:
- SECP Private Key → without
0x - BLS Private Key → with
0x - Amount →
100000 - Authorized Address → your EOA address
- SECP Private Key → without
- Verify that the Derived Public Keys match your actual keys
- Confirm with
y
Successful output:
Status: ✅ Success
Validator Created! ID: <VALIDATOR_ID>
Step 20 — Update node.toml for Validator
Once your validator is active, block rewards are sent to the beneficiary address. Replace the burn address with your own EOA:
nano /home/monad/monad-bft/config/node.toml
Update the following line:
beneficiary = "0xYOUR_EOA_ADDRESS"
Save and restart services:
systemctl restart monad-bft monad-execution monad-rpc
Step 21 — validator-info PR
As requested by the Monad Foundation, submit a PR to the monad-developers/validator-info repository:
- Fork https://github.com/monad-developers/validator-info
- Create
<SECP_KEY>.jsoninside thetestnet/folder:
{
"id": <YOUR_VALIDATOR_ID>,
"name": "<YOUR_NODE_NAME>",
"secp": "<YOUR_SECP_PUBLIC_KEY>",
"bls": "<YOUR_BLS_PUBLIC_KEY>",
"website": "https://your-website.com",
"description": "Your description here",
"logo": "https://raw.githubusercontent.com/your-repo/logo/main/logo.png",
"x": "https://x.com/your_x_handle"
}
- Commit the file and open a pull request
- Share the PR link with the Monad Foundation via Telegram/Discord
⚠️ PRs that are not shared via Discord/Telegram will not be reviewed.
Step 22 — Configure OTEL Metrics Push
The VDP requires all validators to push metrics to Monad Foundation's monitoring infrastructure. The script below automatically detects your SECP key and configures the OTEL Collector to push to Monad Foundation's endpoint.
curl -fsSL -o /tmp/otel-validator-setup.sh https://bucket.monadinfra.com/tmp/otel-validator-setup.sh
echo "92cca0add5456db13c792b6e1b484999724b348ebd41a40a73d7f42086f26a23 /tmp/otel-validator-setup.sh" | sha256sum -c && sudo bash /tmp/otel-validator-setup.sh
Successful output:
/tmp/otel-validator-setup.sh: OK
Secp public key: <SECP_PUBLIC_KEY>
✔ METRIC PUSHING WITH SECP KEY CONFIGURED
Verify the configuration:
curl -s http://localhost:8889/metrics | grep secp_key | head -5
If metric lines containing secp_key="<SECP_PUBLIC_KEY>" appear, push is active.
Note: The script is idempotent — if already configured, it exits without making changes.
About the Author
This guide was prepared by HazenNetworkSolutions.
🌐 hazennetworksolutions.com