# Node Installation

## Node Installation

This page explains how to install a Monad full node and prepare it for validator use on testnet.

### Recommended disk layout

**Disk 1** → OS + MonadBFT\
**Disk 2** → TrieDB / Execution

RAID is not recommended for TrieDB workloads because it may increase latency and reduce IOPS. :contentReference\[oaicite:4]{index=4}

***

### 1. Install the operating system

This example assumes a **Hetzner Dedicated Server**.

Enable **Rescue System** in the Hetzner Robot panel, reboot the server, and connect via SSH:

```bash
ssh root@YOUR_SERVER_IP
```

Run the installer:

```
installimage
```

Use only one disk for the OS.

Example layout:

```
DRIVE1 /dev/nvme0n1
#DRIVE2 /dev/nvme1n1

SWRAID 0

BOOTLOADER grub

PART /boot ext3 1G
PART swap swap 16G
PART / ext4 500G
PART /home ext4 all
```

After installation, reboot the server.

**Important:** the second NVMe disk may still contain an old partition table from a previous installation.\
`installimage` only installs the OS on the disk you specify and does not automatically wipe other disks.

This is normal.

You will verify the disk layout in the next step. If the second disk is not empty, wipe it before configuring TrieDB.

If the second disk is **not empty**, wipe it before configuring TrieDB.

```
wipefs -a /dev/nvme1n1
sgdisk --zap-all /dev/nvme1n1
partprobe /dev/nvme1n1
```

***

### 2. Verify disks

```
lsblk
```

Expected result:

```
nvme0n1 -> OS disk
nvme1n1 -> empty disk
```

***

### 3. Check kernel version

```
uname -r
```

Expected:

```
6.8.0-90-generic
```

Any kernel version `>= 6.8.0.60` is recommended.

***

### 4. Disable HyperThreading / SMT

Check SMT status:

```
cat /sys/devices/system/cpu/smt/active
```

* `1` = enabled
* `0` = disabled

You can also check:

```
lscpu | grep Thread
```

If it shows:

```
Thread(s) per core: 2
```

SMT is enabled.

If it shows:

```
Thread(s) per core: 1
```

SMT is disabled.

#### Disable via GRUB

```
nano /etc/default/grub
```

Find:

```
GRUB_CMDLINE_LINUX_DEFAULT="quiet"
```

Change to:

```
GRUB_CMDLINE_LINUX_DEFAULT="quiet nosmt"
```

Apply changes:

```
update-grub
reboot
```

#### Hetzner-specific case

Some Hetzner servers override GRUB settings through:

```
nano /etc/default/grub.d/hetzner.cfg
```

Find:

```
GRUB_CMDLINE_LINUX_DEFAULT="consoleblank=0"
```

Change to:

```
GRUB_CMDLINE_LINUX_DEFAULT="consoleblank=0 nosmt"
```

Apply:

```
update-grub
reboot
```

Verify again:

```
cat /sys/devices/system/cpu/smt/active
lscpu | grep Thread
```

***

### 5. Update the system

```
apt update && apt upgrade -y
```

If the kernel was upgraded, reboot the server.

Install dependencies:

```
apt install -y curl nvme-cli aria2 jq
```

***

### 6. Install Monad

Add the APT repository:

```
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 Monad:

```
apt update
apt install -y monad=0.13.0
apt-mark hold monad
```

> Monad docs currently show testnet install instructions with the `monad` package pinned and held through APT. Before publishing, verify the [latest testnet package version](https://docs.monad.xyz/node-ops/upgrade-instructions/) in the official docs or release notes.

After installation, we can check the node version.

```
monad-rpc --version
```

***

### 7. Create the monad user

```
useradd -m -s /bin/bash monad
```

Create the directory structure:

```
mkdir -p /home/monad/monad-bft/config \
         /home/monad/monad-bft/ledger \
         /home/monad/monad-bft/config/forkpoint \
         /home/monad/monad-bft/config/validators
```

***

### 8. Configure TrieDB

> Be careful. Do not format the system disk.

Check all NVMe disks:

```
nvme list
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL
```

Pick the empty disk with no mounted filesystem.

Example:

```
TRIEDB_DRIVE=/dev/nvme1n1
```

Create the partition table:

```
parted $TRIEDB_DRIVE mklabel gpt
parted $TRIEDB_DRIVE mkpart triedb 0% 100%
```

Create a `udev` rule:

```
PARTUUID=$(lsblk -o PARTUUID $TRIEDB_DRIVE | 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
```

Reload rules:

```
udevadm trigger
udevadm control --reload
udevadm settle
ls -l /dev/triedb
```

#### Check LBA format

```
nvme id-ns -H $TRIEDB_DRIVE | grep 'LBA Format' | grep 'in use'
```

Expected:

```
Data Size: 512 bytes (in use)
```

If not:

```
nvme format --lbaf=0 $TRIEDB_DRIVE
```

Initialize TrieDB:

```
systemctl start monad-mpt && journalctl -u monad-mpt -n 14 -o cat
```

If there are no errors, TrieDB is ready.

***

### 9. Configure firewall

```
ufw allow ssh
ufw allow 8000
ufw allow 8001
ufw enable
ufw status
```

Optional spam protection:

```
sudo iptables -I INPUT -p udp --dport 8000 -m length --length 0:1400 -j DROP
```

***

### 10. Install OTEL collector

```
OTEL_VERSION="0.139.0"
OTEL_PACKAGE="https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTEL_VERSION}/otelcol_${OTEL_VERSION}_linux_amd64.deb"

curl -fsSL "$OTEL_PACKAGE" -o /tmp/otelcol_linux_amd64.deb
dpkg -i /tmp/otelcol_linux_amd64.deb

cp /opt/monad/scripts/otel-config.yaml /etc/otelcol/config.yaml
systemctl restart otelcol
```

Metrics endpoint:

```
http://IP:8889/metrics
```

What to check next:

```
systemctl status otelcol --no-pager
```

If everything is okay, you should see something like:

* active (running)

***

### 11. Download config files

#### Full node

```
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
```

#### Validator

```
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/node.toml
```

These are the same config paths described in the official node-ops docs.

***

### 12. Set keystore password

```
sed -i "s|^KEYSTORE_PASSWORD=$|KEYSTORE_PASSWORD='$(openssl rand -base64 32)'|" /home/monad/.env
source /home/monad/.env
```

Create backup directory:

```
mkdir -p /opt/monad/backup
echo "Keystore password: ${KEYSTORE_PASSWORD}" > /opt/monad/backup/keystore-password-backup
```

***

### 13. Generate BLS and SECP keys

```
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
```

Mandatory external backup:

* `/opt/monad/backup/secp-backup`
* `/opt/monad/backup/bls-backup`
* `/opt/monad/backup/keystore-password-backup`

If you lose the server and do not have these backups, your node identity cannot be recovered.

***

### 14. Configure `node.toml`

Open:

```
nano /home/monad/monad-bft/config/node.toml
```

#### Beneficiary

For a full node:

```
beneficiary = "0x0000000000000000000000000000000000000000"
```

For a validator:

```
beneficiary = "0xYOUR_ADDRESS"
```

#### Node name

```
node_name = "full_blockpro_1"
```

#### Public full node settings

Make sure these are enabled:

```
[fullnode_raptorcast]
enable_client = true
```

```
[statesync]
expand_to_group = true
```

For a public full node, `[blocksync_override]` should remain empty.

***

### 15. Sign the node name record

```
source /home/monad/.env
monad-sign-name-record \
  --address $(curl -s4 ifconfig.me):8000 \
  --authenticated-udp-port 8001 \
  --keystore-path /home/monad/monad-bft/config/id-secp \
  --password "${KEYSTORE_PASSWORD}" \
  --self-record-seq-num 1
```

Then insert the values into the `peer_discovery` section in `node.toml`:

Now open the `node.toml` file and find the `peer_discovery` section.

```
nano /home/monad/monad-bft/config/node.toml
```

It should look approximately like this:

```
[peer_discovery]
self_address = "YOUR_IP:8000"
self_record_seq_num = 1
self_name_record_sig = "YOUR_SIGNATURE"
```

***

### 16. Optional remote config fetching

Add these values to `/home/monad/.env`:

```
REMOTE_VALIDATORS_URL='https://bucket.monadinfra.com/validators/testnet/validators.toml'
REMOTE_FORKPOINT_URL='https://bucket.monadinfra.com/forkpoint/testnet/forkpoint.toml'
```

Monad nodes can fetch `validators.toml` and `forkpoint.toml` automatically on startup when these variables are defined.

***

### 17. Optional trace calls

For archive or RPC-oriented nodes, you can enable call traces:

```
systemctl edit monad-execution
```

Example override:

```
[Service]
Type=simple
ExecStart=
ExecStart=/usr/local/bin/monad \
    ... \
    --trace_calls \
    ...
```

This is useful for workloads like `debug_traceTransaction`.

***

### 18. Start the node

Set permissions:

```
chown -R monad:monad /home/monad/
```

Enable services:

```
systemctl enable monad-bft monad-execution monad-rpc
```

Hard reset and restore snapshot:

```
bash /opt/monad/scripts/reset-workspace.sh
curl -sSL https://bucket.monadinfra.com/scripts/testnet/restore-from-snapshot.sh | bash
```

Start services:

```
systemctl start monad-bft monad-execution monad-rpc
```

Statesync usually completes quickly on testnet, after which RPC becomes active and blocksync catches up the remainder. The official docs also describe hard reset plus snapshot restore as the normal fast recovery path.

***

### 19. Validator registration

Once the node is fully synced, you can register a validator.

Monad validator registration uses the staking precompile and the `add-validator` flow. Before proceeding, make sure your full node is fully synced and that you have your SECP key, BLS key, auth address, and self-stake amount ready.

#### Install staking CLI

First, install the required packages:

```
apt update
apt install -y python3 python3-venv python3-pip git
```

It is recommended to install and use the staking CLI under the `monad` user instead of `root`.

```
sudo -u monad bash -lc '
cd ~
git clone https://github.com/monad-developers/staking-sdk-cli.git
cd staking-sdk-cli
python3 -m venv cli-venv
source cli-venv/bin/activate
pip install --upgrade pip
pip install .
'
```

#### Create the staking CLI config

The CLI expects a config file. Copy the example config to the `monad` user home directory:

```
sudo -u monad cp /home/monad/staking-sdk-cli/staking-cli/config.toml.example /home/monad/config.toml
```

Then edit it if needed:

```
sudo -u monad nano /home/monad/config.toml
```

#### Launch the staking CLI TUI

```
sudo -u monad bash -lc '
cd ~/staking-sdk-cli
source cli-venv/bin/activate
python3 staking-cli/main.py tui --config-path ~/config.toml
'
```

#### Recover private keys

Load your environment variables first:

```
source /home/monad/.env
```

Recover the SECP private key:

```
monad-keystore recover \
  --password "$KEYSTORE_PASSWORD" \
  --keystore-path /home/monad/monad-bft/config/id-secp \
  --key-type secp
```

Recover the BLS private key:

```
monad-keystore recover \
  --password "$KEYSTORE_PASSWORD" \
  --keystore-path /home/monad/monad-bft/config/id-bls \
  --key-type bls
```

#### Register the validator

After recovering your keys, export them as environment variables:

```
export SECP_PRIVATE_KEY="your_secp_private_key"
export BLS_PRIVATE_KEY="your_bls_private_key"
export AUTH_ADDRESS="your_auth_address"
```

Then run the validator registration command:

```
sudo -u monad bash -lc '
cd ~/staking-sdk-cli
source cli-venv/bin/activate
python3 staking-cli/main.py add-validator \
  --secp-privkey "'"${SECP_PRIVATE_KEY}"'" \
  --bls-privkey "'"${BLS_PRIVATE_KEY}"'" \
  --auth-address "'"${AUTH_ADDRESS}"'" \
  --amount 100_000
'
```

#### Verify pubkeys

```
cat /home/monad/pubkey-secp-bls
```

#### Expected success output

```
Tx status: 1
Validator Created! ID: 19
```

According to Monad docs, validator registration requires valid keys and signatures, and a minimum self-stake of `100,000 MON`. Activation into the active set depends on stake ranking and active-set rules.
