Skip to content

Instantly share code, notes, and snippets.

@Aboudjem
Created February 26, 2026 20:34
Show Gist options
  • Select an option

  • Save Aboudjem/81b2343f2cc10398c3250b44997c14d8 to your computer and use it in GitHub Desktop.

Select an option

Save Aboudjem/81b2343f2cc10398c3250b44997c14d8 to your computer and use it in GitHub Desktop.
Integra Mainnet Deployment Guide

Integra Mainnet Deployment Guide

A step-by-step guide for deploying the Integra mainnet with 4 validators across 4 providers.


Overview

Property Value
Chain ID integra-1
EVM Chain ID 26217 (hex: 0x6669)
Token IRL / airl (1 IRL = 10^18 airl)
Validators 4 (Gateway, Archive, Signer-1, Signer-2)
Total Supply 100,000,000,000 IRL
Inflation 3% fixed per year

Servers

Server Moniker Provider Location Role
Mainnet-Gateway Integra-Helsinki Hetzner Helsinki, Finland Validator + Public APIs + Blockscout
Archive Integra-Paris OVH Paris, France Archive validator + Cosmos Explorer
Signer-1 Integra-Amsterdam Vultr Amsterdam, Netherlands Silent signer only
Signer-2 Integra-NewYork DigitalOcean New York, USA Silent signer only

Step 1: Key Generation (Piyush — on your personal machine)

IMPORTANT: Generate ALL keys on your own machine. Share ONLY the integra1... addresses with Adam. Never share mnemonics.

Install the binary locally

Get the intgd binary from Adam (or build from source on your machine).

Generate 9 wallets

# Mainnet keys
intgd keys add mainnet-treasury --keyring-backend file --algo eth_secp256k1
intgd keys add mainnet-gateway-validator --keyring-backend file --algo eth_secp256k1
intgd keys add archive-validator --keyring-backend file --algo eth_secp256k1
intgd keys add signer1-mainnet-validator --keyring-backend file --algo eth_secp256k1
intgd keys add signer2-mainnet-validator --keyring-backend file --algo eth_secp256k1

# Testnet keys (if redoing testnet)
intgd keys add testnet-treasury --keyring-backend file --algo eth_secp256k1
intgd keys add testnet-gateway-validator --keyring-backend file --algo eth_secp256k1
intgd keys add signer1-testnet-validator --keyring-backend file --algo eth_secp256k1
intgd keys add signer2-testnet-validator --keyring-backend file --algo eth_secp256k1

Each command outputs:

  • address (integra1...) — share this with Adam
  • 24-word mnemonic — write it down, store securely, NEVER share

What to share with Adam

Send Adam a table like this (addresses only):

mainnet-treasury:            integra1abc...
mainnet-gateway-validator:   integra1def...
archive-validator:           integra1ghi...
signer1-mainnet-validator:   integra1jkl...
signer2-mainnet-validator:   integra1mno...

Step 2: Server Setup (Adam)

Adam installs on each server:

  • Go 1.23.8
  • intgd binary at /usr/local/bin/intgd
  • UFW firewall rules
  • Caddy (Gateway and Archive only)

Build gotcha: Must build on Linux with CGO_ENABLED=1. Cross-compiling from macOS fails because Cosmos EVM uses C bindings for secp256k1.


Step 3: Genesis Ceremony (Adam)

Adam performs the genesis ceremony using ONLY the public addresses Piyush provided.

3.1 Initialize all nodes

# On each server
intgd init <moniker> --chain-id integra-1

3.2 Import keys on each server (Piyush)

Piyush SSHs into each server and imports the relevant key:

# On Mainnet-Gateway
intgd keys add mainnet-gateway-validator --recover --keyring-backend file --algo eth_secp256k1
# Enter the 24-word mnemonic for the gateway validator

# On Archive
intgd keys add archive-validator --recover --keyring-backend file --algo eth_secp256k1

# On Signer-1
intgd keys add signer1-mainnet-validator --recover --keyring-backend file --algo eth_secp256k1

# On Signer-2
intgd keys add signer2-mainnet-validator --recover --keyring-backend file --algo eth_secp256k1

# On Mainnet-Gateway (treasury too)
intgd keys add mainnet-treasury --recover --keyring-backend file --algo eth_secp256k1

3.3 Add genesis accounts (Adam, on coordinator node)

GENESIS=$HOME/.intgd/config/genesis.json

# Treasury: 99,999,996,000 IRL (4 validators x 1,000 IRL = 4,000 IRL reserved)
intgd genesis add-genesis-account <treasury-address> 99999996000000000000000000000airl

# 4 Validators: 1,000 IRL each
intgd genesis add-genesis-account <gateway-address> 1000000000000000000000airl
intgd genesis add-genesis-account <archive-address> 1000000000000000000000airl
intgd genesis add-genesis-account <signer1-address> 1000000000000000000000airl
intgd genesis add-genesis-account <signer2-address> 1000000000000000000000airl

3.4 Customize genesis parameters

Apply ALL of these via jq. The critical ones are marked.

GENESIS=$HOME/.intgd/config/genesis.json

# Token metadata
jq '.app_state.bank.denom_metadata = [{
  "description": "The native staking and governance token of Integra Layer",
  "denom_units": [
    {"denom": "airl", "exponent": 0, "aliases": ["attoirl"]},
    {"denom": "irl", "exponent": 18}
  ],
  "base": "airl", "display": "irl", "name": "Integra", "symbol": "IRL"
}]' $GENESIS > tmp.json && mv tmp.json $GENESIS

# EVM denom
jq '.app_state.vm.params.evm_denom = "airl"' $GENESIS > tmp.json && mv tmp.json $GENESIS

# *** CRITICAL: FeeMarket — base_fee MUST be "0" ***
# If base_fee is non-zero, gentx transactions fail with "insufficient fee"
# because gentxs are created without fees but DeliverTx enforces the fee floor.
# The EIP-1559 base fee adjusts upward organically after block 1.
# The actual minimum gas price is set in app.toml (minimum-gas-prices).
jq '.app_state.feemarket.params.no_base_fee = false' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.base_fee = "0"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.min_gas_price = "0.000000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.base_fee_change_denominator = 8' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.elasticity_multiplier = 2' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.min_gas_multiplier = "0.500000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS

# *** CRITICAL: Mint — goal_bonded MUST be > 0 ***
# Setting goal_bonded to "0.0" causes "goal bonded must be positive" error.
# Use "0.010000000000000000" (1%) as minimum.
jq '.app_state.mint.params.mint_denom = "airl"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.inflation_rate_change = "0.000000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.inflation_max = "0.030000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.inflation_min = "0.030000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.goal_bonded = "0.010000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.blocks_per_year = "6311520"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.minter.inflation = "0.030000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS

# Staking
jq '.app_state.staking.params.bond_denom = "airl"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.unbonding_time = "1814400s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.max_validators = 100' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.max_entries = 7' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.historical_entries = 10000' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.min_commission_rate = "0.000000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS

# Governance
jq '.app_state.gov.params.min_deposit = [{"denom":"airl","amount":"1000000000000000000000000"}]' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.expedited_min_deposit = [{"denom":"airl","amount":"5000000000000000000000000"}]' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.max_deposit_period = "604800s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.voting_period = "432000s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.expedited_voting_period = "86400s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.quorum = "0.334000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.threshold = "0.500000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.veto_threshold = "0.334000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.min_initial_deposit_ratio = "0.250000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.burn_vote_veto = true' $GENESIS > tmp.json && mv tmp.json $GENESIS

# Slashing
jq '.app_state.slashing.params.signed_blocks_window = "10000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.slashing.params.min_signed_per_window = "0.050000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.slashing.params.downtime_jail_duration = "600s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.slashing.params.slash_fraction_double_sign = "0.050000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.slashing.params.slash_fraction_downtime = "0.000100000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS

# Distribution
jq '.app_state.distribution.params.community_tax = "0.000000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.distribution.params.withdraw_addr_enabled = true' $GENESIS > tmp.json && mv tmp.json $GENESIS

3.5 Distribute genesis, create gentxs, collect, validate

Same flow as testnet — see Testnet Deployment Report for exact commands.

Key difference: 4 validators instead of 3, so 4 gentxs to collect.

3.6 Verify genesis hash matches on all 4 nodes

sha256sum /root/.intgd/config/genesis.json
# Must be identical on all 4 servers

Step 4: Node Configuration (Adam)

All nodes — app.toml

minimum-gas-prices = "5000000000000airl"

[evm]
chain-id = "26217"

All nodes — client.toml

chain-id = "integra-1"

All nodes — config.toml

[p2p]
persistent_peers = "<gateway-id>@<gateway-ip>:26656,<archive-id>@<archive-ip>:26656,<signer1-id>@<signer1-ip>:26656,<signer2-id>@<signer2-ip>:26656"

Gateway + Archive — app.toml

[api]
enable = true
address = "tcp://0.0.0.0:1317"

[json-rpc]
enable = true
address = "0.0.0.0:8545"
ws-address = "0.0.0.0:8546"

[state-sync]
snapshot-interval = 1000

Archive only — app.toml

pruning = "nothing"   # Archive mode — keep ALL history

Signer-1 and Signer-2 — app.toml

pruning = "everything"

[json-rpc]
enable = false

[tx_index]
indexer = "null"

Firewall (Gateway + Archive)

ufw allow 22/tcp      # SSH
ufw allow 80/tcp      # Let's Encrypt ACME
ufw allow 443/tcp     # HTTPS
ufw allow 1317/tcp    # REST API
ufw allow 8545/tcp    # EVM HTTP
ufw allow 8546/tcp    # EVM WS
ufw allow 26656/tcp   # P2P
ufw enable

Firewall (Signer nodes)

ufw allow 22/tcp      # SSH
ufw allow 26656/tcp   # P2P only
ufw enable

Step 5: Launch (Adam + Piyush)

Create systemd service (all 4 nodes)

# /etc/systemd/system/intgd.service
[Unit]
Description=Integra Mainnet Node
After=network-online.target
Wants=network-online.target

[Service]
User=root
ExecStart=/usr/local/bin/intgd start
Restart=always
RestartSec=3
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable intgd

Start all 4 within 60 seconds

# From coordinator, start all nodes rapidly
systemctl start intgd                                    # Local node
ssh root@<archive-ip> "systemctl start intgd"            # Archive
ssh root@<signer1-ip> "systemctl start intgd"            # Signer-1
ssh root@<signer2-ip> "systemctl start intgd"            # Signer-2

Verify

# Check blocks are being produced
intgd status | jq '.sync_info.latest_block_height'

# Check all 4 validators are bonded
intgd query staking validators --output json | jq '.validators[] | {moniker, status}'

Step 6: Delegation (Piyush)

Delegate 250,000,000 IRL from Treasury to each of the 4 validators:

# 250M IRL = 250000000000000000000000000 airl
AMOUNT="250000000000000000000000000airl"
FLAGS="--from mainnet-treasury --keyring-backend file --gas auto --gas-adjustment 1.5 --gas-prices 5000000000000airl --chain-id integra-1 -y"

intgd tx staking delegate <gateway-valoper> $AMOUNT $FLAGS
intgd tx staking delegate <archive-valoper> $AMOUNT $FLAGS
intgd tx staking delegate <signer1-valoper> $AMOUNT $FLAGS
intgd tx staking delegate <signer2-valoper> $AMOUNT $FLAGS

Verify delegation

intgd query staking validators --output json | \
  jq '.validators[] | {moniker: .description.moniker, tokens: .tokens, status: .status}'
# Each should show ~250,001,000 IRL (250M delegated + 1,000 self-stake)

Step 7: DNS + Caddy (Adam)

DNS Records (Route53)

Subdomain Points To
rpc.integralayer.com Gateway IP
rest.integralayer.com Gateway IP
evm-rpc.integralayer.com Gateway IP
scan.integralayer.com Archive IP

Caddy on Gateway

rpc.integralayer.com {
    reverse_proxy localhost:26657
}
rest.integralayer.com {
    reverse_proxy localhost:1317
}
evm-rpc.integralayer.com {
    handle {
        reverse_proxy localhost:8545
    }
    handle /ws {
        reverse_proxy localhost:8546
    }
}

Security & Trust Transparency

What Adam has access to

Resource Access Level
SSH root on all servers Full access to all files
priv_validator_key.json Can read (generated during intgd init)
node_key.json Can read
Config files Can read and modify
Server logs Can read

What Adam can see

  • Validator consensus keys (priv_validator_key.json) — used for block signing
  • Node keys (node_key.json) — used for P2P identity
  • All config filesconfig.toml, app.toml, client.toml

What Adam CANNOT see (if Piyush generates keys offsite)

  • Wallet mnemonics — the 24-word recovery phrases
  • Keyring passwords — the password used with --keyring-backend file
  • Private spending keys — unless Piyush imports them to a server Adam has SSH access to

Risk assessment

Risk Severity Details
priv_validator_key.json exposure Medium Anyone with this file can sign blocks as that validator. This enables double-signing (slashing risk: 5% of stake burned) but does NOT allow spending tokens.
Keyring on server High if imported If Piyush imports wallet mnemonics to servers via --recover, Adam can extract the keyring files and potentially decrypt them.
Treasury mnemonic on server Critical If the treasury mnemonic is ever on a server, anyone with SSH root can extract it. The treasury controls 99.999% of all tokens.

Mitigations

  1. Piyush generates ALL keys on his personal machine — never shares mnemonics
  2. Piyush imports keys to servers himself via SSH — Adam provides server access but doesn't handle key import
  3. Treasury mnemonic stays offline — never imported to any server. Delegation commands run from Piyush's local machine or a temporary import that's immediately deleted.
  4. Post-launch: Piyush can rotate SSH keys on all servers, removing Adam's access if desired
  5. Future: Implement sentry node architecture to isolate validator signing keys from public-facing nodes

The bottom line

  • The treasury mnemonic is the only key that controls funds. Keep it offline, in a hardware wallet or encrypted backup. Never put it on a server.
  • priv_validator_key.json on servers allows block signing (slashing risk) but NOT spending tokens.
  • Adam having SSH root means he CAN read priv_validator_key.json — this is standard for any server admin. The mitigation is trust + the fact that these keys cannot move funds.

Troubleshooting — Lessons from Testnet

"goal bonded must be positive"

When: Running intgd genesis gentx Cause: goal_bonded set to "0.000000000000000000" in mint params Fix: Set to "0.010000000000000000" (1% minimum)

"fee not provided. The minimum global fee is..."

When: Running intgd genesis gentx or intgd genesis collect-gentxs Cause: Feemarket base_fee is non-zero in genesis. Gentxs don't include fees. Fix: Set base_fee = "0" and min_gas_price = "0.000000000000000000" in feemarket genesis params. The app.toml minimum-gas-prices provides the actual floor post-launch.

Caddy ACME challenge timeout

When: Starting Caddy — "Timeout during connect (likely firewall problem)" Cause: UFW blocking ports 80/443 needed for Let's Encrypt HTTP-01 challenge Fix: ufw allow 80/tcp && ufw allow 443/tcp && ufw reload

If Caddy has cached failed attempts:

rm -rf /var/lib/caddy/.local/share/caddy/certificates
rm -rf /var/lib/caddy/.local/share/caddy/acme
systemctl restart caddy

Cross-compilation failure

When: Building intgd with CGO_ENABLED=0 or cross-compiling from macOS Cause: secp256k1 C bindings require CGO Fix: Build on a Linux machine with CGO_ENABLED=1

Chain halts — 2/3 validators must be online

Mainnet (4 validators): Need 3/4 online (75% > 67% threshold) Testnet (3 validators): Need 2/3 online (67% — barely passes)

If chain halts:

  1. Check which validators are down: intgd query staking validators --output json | jq '.validators[] | {moniker, jailed: .jailed, status: .status}'
  2. Restart downed nodes: systemctl restart intgd
  3. If jailed for downtime: intgd tx slashing unjail --from <validator-key> --keyring-backend file --gas auto --gas-adjustment 1.5 --gas-prices 5000000000000airl --chain-id integra-1 -y

EVM Chain ID mismatch

Symptom: MetaMask shows wrong chain or transactions fail Cause: Default EVM chain ID after intgd init is 262144, not 26217 Fix: Set in app.toml:

[evm]
chain-id = "26217"   # mainnet
# chain-id = "26218" # testnet

client.toml chain-id

Symptom: Every CLI command requires --chain-id flag Cause: Cosmos SDK v0.47+ validates chain-id from client.toml Fix: Set in client.toml:

chain-id = "integra-1"

Quick Reference

Token Amounts

Amount airl Value
1 IRL 1000000000000000000 (10^18)
1,000 IRL 1000000000000000000000 (10^21)
1,000,000 IRL 1000000000000000000000000 (10^24)
200,000,000 IRL 200000000000000000000000000 (10^26.3)
250,000,000 IRL 250000000000000000000000000

Gas Prices

Always use: --gas-prices 5000000000000airl (5000 gwei)

With auto gas estimation: --gas auto --gas-adjustment 1.5

Keyring

All commands use: --keyring-backend file

This stores keys encrypted on disk. You'll be prompted for a password.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment