diff --git a/local_node.sh b/local_node.sh index 1f4d4c32..9c4acb5b 100755 --- a/local_node.sh +++ b/local_node.sh @@ -1,5 +1,9 @@ #!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/lib/precompiles.sh +source "$SCRIPT_DIR/scripts/lib/precompiles.sh" + CHAINID="${CHAIN_ID:-10740}" MONIKER="ogevmdevnettest" # Remember to change to other types of keyring like 'file' in-case exposing to outside world, @@ -240,7 +244,7 @@ if [[ $overwrite == "y" || $overwrite == "Y" ]]; then jq '.app_state["bank"]["denom_metadata"]=[{"description":"The native staking token for evmd.","denom_units":[{"denom":"ogwei","exponent":0,"aliases":[]},{"denom":"OPG","exponent":18,"aliases":[]}],"base":"ogwei","display":"OPG","name":"OpenGradient Token","symbol":"OPG","uri":"","uri_hash":""}]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" -jq '.app_state["evm"]["params"]["active_static_precompiles"]=["0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805", "0x0000000000000000000000000000000000000806", "0x0000000000000000000000000000000000000807", "0x0000000000000000000000000000000000000900"]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" +jq --argjson p "$ACTIVE_STATIC_PRECOMPILES" '.app_state.evm.params.active_static_precompiles=$p' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state["evm"]["params"]["evm_denom"]="ogwei"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state.erc20.native_precompiles=["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" diff --git a/multi_node_startup.sh b/multi_node_startup.sh index 211c84c4..9d62a7a4 100755 --- a/multi_node_startup.sh +++ b/multi_node_startup.sh @@ -1,5 +1,9 @@ #!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/lib/precompiles.sh +source "$SCRIPT_DIR/scripts/lib/precompiles.sh" + CHAINID="${CHAIN_ID:-10740}" MONIKER="ogevmdevnettest" KEYRING="test" @@ -69,9 +73,9 @@ apply_genesis_customizations() { jq '.app_state["evm"]["params"]["evm_denom"]="ogwei"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state["mint"]["params"]["mint_denom"]="ogwei"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" - jq '.app_state["bank"]["denom_metadata"]=[{"description":"The native staking token for evmd.","denom_units":[{"denom":"ogwei","exponent":0,"aliases":[]},{"denom":"OGETH","exponent":18,"aliases":[]}],"base":"ogwei","display":"OGETH","name":"ETH Token","symbol":"OGETH","uri":"","uri_hash":""}]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" + jq '.app_state["bank"]["denom_metadata"]=[{"description":"The native staking token for evmd.","denom_units":[{"denom":"ogwei","exponent":0,"aliases":[]},{"denom":"OPG","exponent":18,"aliases":[]}],"base":"ogwei","display":"OPG","name":"OpenGradient","symbol":"OPG","uri":"","uri_hash":""}]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" - jq '.app_state["evm"]["params"]["active_static_precompiles"]=["0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805","0x0000000000000000000000000000000000000806","0x0000000000000000000000000000000000000807"]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" + jq --argjson p "$ACTIVE_STATIC_PRECOMPILES" '.app_state.evm.params.active_static_precompiles=$p' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state.erc20.native_precompiles=["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" jq '.app_state.erc20.token_pairs=[{contract_owner:1,erc20_address:"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",denom:"ogwei",enabled:true}]' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS" @@ -155,7 +159,7 @@ generate_dev_accounts() { mkdir -p "$BASEDIR" echo "# Dev Accounts - Generated $(date)" > "$OUTPUT_FILE" - echo "# Each account funded with 1000000000000000000000000ogwei (1M OGETH)" >> "$OUTPUT_FILE" + echo "# Each account funded with 1000000000000000000000000ogwei (1M OPG)" >> "$OUTPUT_FILE" echo "#" >> "$OUTPUT_FILE" local DEV_HOME="$BASEDIR/.dev_keys_tmp" diff --git a/scripts/lib/precompiles.sh b/scripts/lib/precompiles.sh new file mode 100644 index 00000000..814511f1 --- /dev/null +++ b/scripts/lib/precompiles.sh @@ -0,0 +1,17 @@ +# Static EVM precompile addresses registered on og-evm. +# Sourced by local_node.sh, multi_node_startup.sh, and scripts/mainnet/genesis.sh +# so all three networks publish the same active set. +# +# 0x…0100 P256 signature verification +# 0x…0400 Bech32 address codec +# 0x…0800 Staking +# 0x…0801 Distribution +# 0x…0802 ICS-20 IBC transfer +# 0x…0803 Vesting +# 0x…0804 Bank +# 0x…0805 Governance +# 0x…0806 Slashing +# 0x…0807 ICS-02 IBC client +# 0x…0900 TEE attestation registry + +ACTIVE_STATIC_PRECOMPILES='["0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805","0x0000000000000000000000000000000000000806","0x0000000000000000000000000000000000000807","0x0000000000000000000000000000000000000900"]' diff --git a/scripts/mainnet/README.md b/scripts/mainnet/README.md new file mode 100644 index 00000000..9629870e --- /dev/null +++ b/scripts/mainnet/README.md @@ -0,0 +1,643 @@ +# OG-EVM Mainnet Genesis Bootstrap + +Steps to bootstrap chain id `opengradient_1486-1` from scratch. + +Some steps run on every genesis machine. Others run once for the whole network. +Sections are marked **[per node]** or **[coordinator]** so the split is clear. +If one team owns all 4 genesis machines that team plays both roles. If external +validators run some nodes, the foundation typically plays the coordinator role. + +Inputs: +- a release tag of this repo +- 4 machines (private subnet) for the genesis nodes +- a chosen `genesis_time` (RFC3339 UTC) + +Outputs: +- a final `genesis.json` with all 4 gentxs collected +- 4 nodes producing block 1 at `genesis_time` + +L1 supply at genesis is intentionally minimal: 4 × 10 OPG validator self-stakes +(40 OPG total), Foundation 0, `x/svip` module 0. The canonical 1B OPG lives as +ERC-20 on Base and bridges to L1 on demand. Foundation must bridge in OPG before +submitting any L1 transaction (including governance proposals); the SVIP pool is +funded the same way before activation. See [section 17](#17-post-launch-bridge-dependencies). + +For chain settings (denoms, decimals, commission rules, supply, etc.) see +[section 16](#16-chain-settings-reference). + +## What's in this guide + +1. [Prereqs](#1-prereqs) [per node] +2. [Install evmd](#2-install-evmd) [per node] +3. [Set up the node home](#3-set-up-the-node-home) [per node] +4. [Generate keys](#4-generate-keys) [per node] +5. [Gather node identifiers](#5-gather-node-identifiers) [coordinator] +6. [Build the pre-gentx genesis](#6-build-the-pre-gentx-genesis) [coordinator] +7. [Place the pre-gentx genesis](#7-place-the-pre-gentx-genesis) [per node] +8. [Run gentx](#8-run-gentx) [per node] +9. [Collect gentxs and produce the final genesis](#9-collect-gentxs-and-produce-the-final-genesis) [coordinator] +10. [Place the final genesis](#10-place-the-final-genesis) [per node] +11. [Edit config files](#11-edit-config-files) [per node] +12. [Launch](#12-launch) [per node] +13. [Check it's working](#13-check-its-working) [per node] +14. [Security checklist](#14-security-checklist) +15. [Troubleshooting](#15-troubleshooting) +16. [Chain settings reference](#16-chain-settings-reference) +17. [Post-launch bridge dependencies](#17-post-launch-bridge-dependencies) + + +## 1. Prereqs + +[per node] + +### Hardware + +| Resource | Recommended | +|---|---| +| CPU | 16 vCPU (modern x86_64 with AVX2) | +| RAM | 64 GB | +| Disk | 2 TB NVMe SSD | +| Network | 1 Gbps both ways, low jitter | +| Filesystem | ext4 or zfs (not btrfs) | + +### OS + +Tested on Ubuntu 22.04 or 24.04. Run as a regular user (call it `evmd`) with +`sudo` for setup only. + +### Software + +```bash +sudo apt update +sudo apt install -y build-essential git curl jq make pkg-config libssl-dev clang + +# Go (use 1.26.2 or the latest patch release at install time) +curl -L https://go.dev/dl/go1.26.2.linux-amd64.tar.gz | sudo tar -C /usr/local -xz +echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc +source ~/.bashrc +go version # should print 1.26.x +``` + +### Time sync + +Clock has to be accurate. If it drifts more than a few seconds the node falls +out of consensus. + +```bash +sudo apt install -y chrony +sudo systemctl enable --now chrony +chronyc tracking # offset should be under 100 ms +``` + +### Firewall + +| Port | What it is | Open to | +|---|---|---| +| 22 | SSH | bastion only | +| 26656 | P2P | private subnet (other genesis nodes) only | +| 26657 | Cosmos RPC | localhost only | +| 8545 | Ethereum JSON-RPC | localhost only | +| 8546 | Ethereum WebSocket | localhost only | +| 9090 | gRPC | localhost only | +| 1317 | REST | localhost only | +| 26660 | Prometheus | observability subnet only | + + +## 2. Install evmd + +[per node] + +```bash +git clone https://github.com/OpenGradient/og-evm.git ~/og-evm +cd ~/og-evm +git checkout +make install # builds ~/go/bin/evmd +evmd version # prints the commit hash that was just built +``` + +Every node must build from the **same commit**. A different commit will produce +a different `app_hash` and fork off at block 1. + + +## 3. Set up the node home + +[per node] + +The node home is `$HOME/.evmd-mainnet`. Use this exact path so the rest of the +guide works without changes. + +```bash +evmd init \ + --chain-id opengradient_1486-1 \ + --home ~/.evmd-mainnet +``` + +Pick a short, recognisable moniker (`og-validator-tokyo-1` or similar). It ends +up in monitoring and alerting, so don't change it later. + +Lock down the directory permissions: + +```bash +chmod 700 ~/.evmd-mainnet +chmod 700 ~/.evmd-mainnet/config +chmod 600 ~/.evmd-mainnet/config/*.json +``` + + +## 4. Generate keys + +[per node] + +| Key | What it's for | Where it lives | Recoverable? | +|---|---|---|---| +| Operator key (eth_secp256k1) | Signs withdrawals, governance votes, edit-validator | keyring | yes, from the mnemonic | +| Consensus key (ed25519) | Signs every block. Most sensitive. | `priv_validator_key.json` | no | +| Node key (ed25519) | Peer ID on the network | `node_key.json` | no, but safe to rotate | + +### 4.1 Operator key + +```bash +evmd keys add \ + --algo eth_secp256k1 \ + --keyring-backend file \ + --home ~/.evmd-mainnet +``` + +Pick a strong passphrase. Save the **24-word mnemonic** in a hardware-backed +password manager. Losing it means losing any rewards or governance power tied +to this address. + +To print the operator address: + +```bash +evmd keys show -a --keyring-backend file --home ~/.evmd-mainnet +# prints og1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 4.2 Consensus key + +`evmd init` already created `~/.evmd-mainnet/config/priv_validator_key.json`. +Treat this file like cash: + +- Don't copy it off the host. +- Back it up once, encrypted, somewhere offline. If the host dies and the file + is gone, the validator slot is gone. +- **Never run two nodes with the same `priv_validator_key.json` at the same + time.** That's a double-sign and will get the validator slashed once slashing + is on. + +To print the consensus pubkey: + +```bash +evmd comet show-validator --home ~/.evmd-mainnet +# prints {"@type":"/cosmos.crypto.ed25519.PubKey","key":""} +``` + +The base64 string after `"key":` is what goes into `validators.yaml`. + +### 4.3 Node key + +`evmd init` also created `~/.evmd-mainnet/config/node_key.json`. This is a +peer identity (libp2p), not a signing key. To print the node ID: + +```bash +evmd comet show-node-id --home ~/.evmd-mainnet +# prints e.g. 4a1b2c3d4e5f6789... +``` + +Node IDs show up in `persistent_peers` strings, like +`@:26656`. + + +## 5. Gather node identifiers + +[coordinator] + +Collect the following from each of the 4 genesis nodes (over a channel that +preserves integrity, e.g. signed git commit, Signal, GPG-signed file): + +| What | From step | +|---|---| +| Moniker | step 3 | +| Operator address (`og1...`) | step 4.1 | +| Consensus pubkey (base64 ed25519) | step 4.2 | +| Node ID | step 4.3 | +| P2P endpoint (`:26656`) | infra | + +**Never collect or transmit `priv_validator_key.json`, `node_key.json`, or +operator mnemonics.** Each of those stays on the node that generated it. + +Fill in `scripts/mainnet/validators.yaml` with one entry per node, and +`scripts/mainnet/foundation.yaml` with the foundation multisig address (and +member pubkeys for the record). + + +## 6. Build the pre-gentx genesis + +[coordinator] + +```bash +bash scripts/mainnet/genesis.sh \ + --validators-yaml scripts/mainnet/validators.yaml \ + --foundation-yaml scripts/mainnet/foundation.yaml \ + --genesis-time \ + --out-dir ~/.evmd-mainnet-foundation +``` + +What the script does: + +1. Runs `evmd init` to start a chain home. +2. Patches every genesis parameter (chain id, denoms, staking, mint set to 0, + distribution set to 0, governance, slashing-disabled, EVM, feemarket, + ERC-20, SVIP dormant, crisis, IBC, consensus block-max-gas). +3. Adds genesis accounts: each validator's symbolic 10 OPG. (Foundation gets + 0 at genesis by default; the `x/svip` module account is NOT pre-funded. + Both are bridged in post-launch, see section 17.) +4. Sets `bank.supply` equal to the L1 sum (~40 OPG), enforcing the SDK + invariant `supply == Σ balances`. +5. Runs sanity checks: inflation is 0, community tax is 0, max gas isn't + `-1`, SVIP is not activated, denoms match, total balances equal + `bank.supply`, and L1 supply is ≤ 1% of canonical (catches yaml mistakes). +6. Runs `evmd genesis validate`. + +To rehearse the script without real keys: + +```bash +bash scripts/mainnet/genesis.sh --dry-run --out-dir /tmp/og-dryrun +``` + +Publish `~/.evmd-mainnet-foundation/config/genesis.json` and its sha256: + +```bash +shasum -a 256 ~/.evmd-mainnet-foundation/config/genesis.json +``` + + +## 7. Place the pre-gentx genesis + +[per node] + +```bash +curl -fSL -o /tmp/genesis.json +echo " /tmp/genesis.json" | sha256sum -c - + +cp /tmp/genesis.json ~/.evmd-mainnet/config/genesis.json +chmod 600 ~/.evmd-mainnet/config/genesis.json +``` + +Quick sanity checks: + +```bash +jq -r '.chain_id' ~/.evmd-mainnet/config/genesis.json +# should print: opengradient_1486-1 + +jq -r '.app_state.staking.params.bond_denom' ~/.evmd-mainnet/config/genesis.json +# should print: ogwei + +jq '.app_state.bank.balances | length' ~/.evmd-mainnet/config/genesis.json +# should print: 4 (validators only; foundation and svip module are unfunded at genesis) + +jq -r '.app_state.bank.supply[0].amount' ~/.evmd-mainnet/config/genesis.json +# should print: 40000000000000000000 (40 OPG = 4 × 10 OPG validator self-stakes) +``` + + +## 8. Run gentx + +[per node] + +Run on the host that holds the consensus key: + +```bash +# 10 OPG = 10000000000000000000 ogwei +evmd genesis gentx 10000000000000000000ogwei \ + --commission-rate 0.05 \ + --commission-max-rate 0.50 \ + --commission-max-change-rate 0.01 \ + --min-self-delegation 10000000000000000000 \ + --keyring-backend file \ + --chain-id opengradient_1486-1 \ + --home ~/.evmd-mainnet \ + --moniker \ + --gas-prices 1000000000ogwei +``` + +This creates `~/.evmd-mainnet/config/gentx/gentx-*.json`. Hand that file (only +that file) to the coordinator. + + +## 9. Collect gentxs and produce the final genesis + +[coordinator] + +When all 4 gentxs are in: + +```bash +cp received-gentx-*.json ~/.evmd-mainnet-foundation/config/gentx/ +evmd genesis collect-gentxs --home ~/.evmd-mainnet-foundation +evmd genesis validate --home ~/.evmd-mainnet-foundation +shasum -a 256 ~/.evmd-mainnet-foundation/config/genesis.json +``` + +Publish the final `genesis.json` and its new sha256. + + +## 10. Place the final genesis + +[per node] + +```bash +curl -fSL -o /tmp/genesis-final.json +echo " /tmp/genesis-final.json" | sha256sum -c - + +# Replace the pre-gentx file. The gentx is now embedded in this version. +cp /tmp/genesis-final.json ~/.evmd-mainnet/config/genesis.json + +evmd genesis validate --home ~/.evmd-mainnet +``` + +Post the locally computed sha256 back to the coordination channel. All 4 nodes +should report the same hash. + + +## 11. Edit config files + +[per node] + +### 11.1 `config.toml`: timing + +Must match across the network. Don't change. + +```bash +TOML=~/.evmd-mainnet/config/config.toml + +sed -i 's/^timeout_propose =.*/timeout_propose = "2s"/' $TOML +sed -i 's/^timeout_propose_delta =.*/timeout_propose_delta = "200ms"/' $TOML +sed -i 's/^timeout_prevote =.*/timeout_prevote = "500ms"/' $TOML +sed -i 's/^timeout_prevote_delta =.*/timeout_prevote_delta = "200ms"/' $TOML +sed -i 's/^timeout_precommit =.*/timeout_precommit = "500ms"/' $TOML +sed -i 's/^timeout_precommit_delta =.*/timeout_precommit_delta = "200ms"/' $TOML +sed -i 's/^timeout_commit =.*/timeout_commit = "1s"/' $TOML +``` + +For Prometheus and OpenTelemetry, follow the +[`monitoring/README.md`](../../monitoring/README.md) "Configure your node" +section before continuing. + +### 11.2 `config.toml`: peering + +Build the peer list from `validators.yaml`: + +```bash +# All peer strings (one per line) +yq -r '.validators[] | "\(.node_id)@\(.p2p_endpoint)"' \ + scripts/mainnet/validators.yaml + +# All node IDs (CSV, for unconditional_peer_ids / private_peer_ids) +yq -r '[.validators[].node_id] | join(",")' \ + scripts/mainnet/validators.yaml +``` + +For each node, drop its own entry from `persistent_peers` and `*_peer_ids` +(node N peers with the 3 others, not itself). Edit `config.toml`: + +```toml +laddr = "tcp://0.0.0.0:26656" +external_address = "" # private node, do not advertise +seeds = "" +persistent_peers = "<3 other validators' peer strings, comma-separated>" +pex = false # do not gossip peers +addr_book_strict = true +unconditional_peer_ids = "<3 other validators' node IDs, comma-separated>" +private_peer_ids = "" +allow_duplicate_ip = false +``` + +### 11.3 `app.toml` + +```bash +APP=~/.evmd-mainnet/config/app.toml + +# Minimum gas price (matches the genesis floor of 1 ogwei) +sed -i 's|^minimum-gas-prices =.*|minimum-gas-prices = "1ogwei"|' $APP + +# Pruning +sed -i 's|^pruning =.*|pruning = "default"|' $APP + +# Turn on the APIs +sed -i '/^\[api\]/,/^\[/ s|^enable =.*|enable = true|' $APP +sed -i '/^\[grpc\]/,/^\[/ s|^enable =.*|enable = true|' $APP +sed -i '/^\[json-rpc\]/,/^\[/ s|^enable =.*|enable = true|' $APP +``` + +Bind addresses: `evmd init` already binds the host-facing services to localhost +by default (Cosmos REST `tcp://localhost:1317`, gRPC `localhost:9090`, EVM +JSON-RPC `127.0.0.1:8545`, EVM WebSocket `127.0.0.1:8546`, Cosmos RPC +`tcp://127.0.0.1:26657`). Prometheus instrumentation is **disabled** by +default (`prometheus = false`), so its world-bound `prometheus_listen_addr` +is dormant until [`monitoring/README.md`](../../monitoring/README.md) turns +it on. When you do, lock the listen address down to the subnet your +monitoring stack scrapes from: + +```bash +sed -i 's|^prometheus_listen_addr = ":26660"|prometheus_listen_addr = "127.0.0.1:26660"|' $TOML +``` + + +## 12. Launch + +[per node] + +Set up `evmd` as a systemd service. Replace `` with the Linux account +that owns `~/.evmd-mainnet` (the user that ran the steps in §3). + +```ini +# /etc/systemd/system/evmd.service +[Unit] +Description=evmd mainnet +After=network-online.target +Wants=network-online.target + +[Service] +User= +Group= +ExecStart=/home//go/bin/evmd start --home /home//.evmd-mainnet +Restart=on-failure +RestartSec=5s +LimitNOFILE=65536 + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable evmd +``` + +Don't start yet. About 5 minutes before `genesis_time`: + +```bash +sudo systemctl start evmd +journalctl -u evmd -f +``` + +The log shows `Starting node` then `Waiting for genesis_time`. At +`genesis_time` consensus tries to start. As soon as 3 of 4 validators are +online (the 2/3 threshold), block 1 is produced. + + +## 13. Check it's working + +[per node] + +```bash +# PoA module is loaded (params is the only query this module exposes; +# call succeeding is the signal; the response body is intentionally empty) +evmd q poa params --node tcp://localhost:26657 + +# Validator records exist (validators are stored in x/staking; x/poa just +# gates who can be added to the set via /poa.MsgAddValidator) +evmd q staking validators --node tcp://localhost:26657 +# expect 4 entries + +# Active consensus set +evmd q comet-validator-set --node tcp://localhost:26657 +# the consensus pubkey should match the one from step 4.2 + +# Node is signing blocks +journalctl -u evmd -f | grep -i 'committed state\|signed proposal\|signed vote' +# expect a signed vote about every 2 seconds +``` + +PoA enforcement runs in `xrplevm/node/v10/x/poa/ante/poa.go:18-23`, which +rejects any tx whose top-level message is `MsgDelegate`, `MsgUndelegate`, +`MsgBeginRedelegate`, or `MsgCancelUnbondingDelegation`. The decorator is +wired in at `ante/cosmos.go:44`, last in the antehandler chain. Validators +are created at genesis-time via `MsgCreateValidator` inside each gentx; +that message type is not on the blocked list, which is why genesis works. +Post-launch, new validators are admitted only through `/poa.MsgAddValidator` +governance proposals (see `evmd/docs/POA_ADD_VALIDATOR_VIA_GOV.md`). + +Hook the node into the monitoring stack. The full how-to is in +[`monitoring/README.md`](../../monitoring/README.md). + + +## 14. Security checklist + +- [ ] `priv_validator_key.json` is on the validator host only +- [ ] `~/.evmd-mainnet` is `chmod 700`. Key files are `chmod 600`. +- [ ] Operator mnemonic is in a hardware-backed password manager, not on the host +- [ ] SSH is key-only, root login is off, only the bastion can reach port 22 +- [ ] P2P (port 26656) only accepts traffic from other genesis nodes +- [ ] Public ingress to 26657, 8545, 8546, 9090, and 1317 is blocked +- [ ] `pex = false` +- [ ] `private_peer_ids` lists every other validator's node ID +- [ ] Time is in sync (chrony offset under 100 ms) +- [ ] Disk encryption at rest is on +- [ ] A cold backup of `priv_validator_key.json` exists, encrypted, off-host +- [ ] `priv_validator_key.json` has never been copied to a second host + + +## 15. Troubleshooting + +### Node won't start: "genesis doc hash mismatch" + +The local `genesis.json` differs from what other nodes have. Re-fetch the final +file from the coordinator and check the sha256. + +### Node starts but no blocks + +- `journalctl -u evmd -f` shows "no peers" → typo in `persistent_peers` or a + firewall blocking port 26656. +- `curl -s localhost:26657/status | jq '.result.sync_info'` → `catching_up: + true` means the node is behind. `false` with no height increment means + consensus is stuck (probably fewer than 3 of 4 validators online). + +### `eth_chainId` returns the wrong value + +The EVM chain ID is `1486` decimal, `0x5ce` hex. Anything else means the wrong +`genesis.json` was placed. Stop, re-fetch, restart. + + +## 16. Chain settings reference + +| Setting | Value | +|---|---| +| Chain ID | `opengradient_1486-1` (EVM chain ID 1486 / `0x5ce`) | +| Binary | `evmd` | +| Bech32 prefix | `og` (validators: `ogvaloper`) | +| Base denom | `ogwei` (18 decimals) | +| Display denom and symbol | `OPG` | +| Canonical supply (Base ERC-20) | 1,000,000,000 OPG. The fully-bridgeable supply lives here. | +| L1 `bank.supply` at genesis | 40 OPG (4 × 10 OPG validator self-stakes). Everything else stays on Base and bridges in on demand. | +| L1 inflation | 0 | +| Community tax | 0 | +| Block time | about 2 seconds | +| Block max gas | 40,000,000 | +| Min gas price | 1 ogwei | +| Base fee at genesis | 1 Gwei (1,000,000,000 ogwei). Goes to validators, not burned. | +| Slashing at launch | off | +| Validator set at launch | PoA (xrplevm `x/poa`). New validators added via `/poa.MsgAddValidator` gov proposals. `MsgDelegate` / `MsgUndelegate` / `MsgBeginRedelegate` / `MsgCancelUnbondingDelegation` are blocked at the ante handler. | +| Unbonding period | 21 days | +| Min commission | 5% | +| Max commission per validator | 50% | +| Max commission daily change | 1% | +| Governance min deposit | 5,000 OPG | +| Voting period | 5 days | +| Quorum / threshold / veto | 33.4% / 50% / 33.4% | +| SVIP allocation | 10% of canonical supply (100 million OPG). Bridged from Base into the `x/svip` module account by Foundation BEFORE governance activates SVIP. Not present in genesis. | + + +## 17. Post-launch bridge dependencies + +The chain has 40 OPG at block 1. Every other on-chain action depends on Foundation +bridging OPG from Base to L1 first. + +**Foundation operational liquidity (immediate post-launch).** +The Foundation multisig holds 0 OPG at genesis. To submit any L1 transaction +(including the first governance proposal), the Foundation must bridge OPG from +Base into its multisig address. The minimum that makes sense is: + +- 1× governance proposal min deposit (5,000 OPG) +- ~1 OPG for gas runway + +So a first bridge of ~10,000 OPG to the Foundation multisig unblocks day-1 +governance. + +**SVIP pool funding (before activation).** +The 100 million OPG SVIP pool is bridged from Base into the `x/svip` module +account on L1 BEFORE governance activates SVIP. The module account address is +deterministic: + +``` +og157j8m5l05q0theh7fep9ejqkqkejtwxdxtwzqh +``` + +It is derived as `bech32(og, sha256("svip")[:20])`, which equals the Cosmos SDK's +`authtypes.NewModuleAddress("svip").String()` with the chain's `og` bech32 prefix. +Confirm the address pre-bridge with: + +```bash +evmd debug addr "$(printf 'svip' | shasum -a 256 | awk '{print substr($1,1,40)}')" +# Bech32 Acc og157j8m5l05q0theh7fep9ejqkqkejtwxdxtwzqh +``` + +Activation flow: + +1. Foundation locks 100M OPG on the Base ERC-20 contract via the bridge. +2. Bridge mints 100M OPG into the L1 svip module address. + Verify with `evmd q bank balance og157j8m5l05q0theh7fep9ejqkqkejtwxdxtwzqh`. +3. Governance proposal sets `svip.params.half_life_seconds` and flips + `svip.activated = true`. The keeper snapshots `pool_balance_at_activation` + from the module's bank balance at activation block. + +**Why not just put 100M in the genesis?** +Tokens seated in genesis without a corresponding lock on Base are unbacked: if +all L1 OPG holders try to bridge to Base for trading, the Base contract runs out +of unlocked supply for that 10%. Bridging the pool from Base post-launch keeps +the canonical-on-Base invariant intact and avoids that depeg/run risk. + + +If anything in this guide is wrong or unclear, flag it on the coordination +channel before launch. diff --git a/scripts/mainnet/foundation.yaml b/scripts/mainnet/foundation.yaml new file mode 100644 index 00000000..b480dfaa --- /dev/null +++ b/scripts/mainnet/foundation.yaml @@ -0,0 +1,34 @@ +# OG-EVM Mainnet Foundation Multisig +# +# Foundation holds 0 OPG on L1 at genesis. The full canonical 1B OPG supply lives +# as ERC-20 on Base; the Foundation bridges to L1 on demand. This template still +# records the multisig spec because: +# - the bridge ceremony post-launch transfers OPG to this address on L1 +# - the multisig threshold + members are part of the public launch record +# +# How to create the multisig key (off-chain, BEFORE genesis): +# evmd keys add foundation-multisig \ +# --multisig ,,..., \ +# --multisig-threshold M \ +# --keyring-backend file +# This produces a deterministic og1... address derived from the threshold + members. +# +# Field semantics: +# address bech32 og1... address of the multisig +# multisig_threshold M from M-of-N (must be a positive integer) +# multisig_members bech32 addresses of the N members (for traceability only; +# genesis.sh does not use them) +# allocation_opg L1 genesis OPG to seat in this address. Defaults to 0 +# (Foundation bridges in post-launch). Override only if the +# team explicitly wants L1 starting liquidity. + +foundation: + address: "og1REPLACE_FOUNDATION_MULTISIG_ADDRESS_HERE" + multisig_threshold: 0 # e.g. 3 + multisig_members: # bech32 og1... member addresses, recorded for traceability + - "og1REPLACE_member_1" + - "og1REPLACE_member_2" + - "og1REPLACE_member_3" + - "og1REPLACE_member_4" + - "og1REPLACE_member_5" + allocation_opg: 0 diff --git a/scripts/mainnet/genesis.sh b/scripts/mainnet/genesis.sh new file mode 100755 index 00000000..7e929be9 --- /dev/null +++ b/scripts/mainnet/genesis.sh @@ -0,0 +1,519 @@ +#!/bin/bash +# +# OG-EVM Mainnet Genesis Builder +# +# Produces the canonical genesis.json for opengradient_1486-1. +# +# In normal mode the script does NOT generate validator gentxs — those come from each +# operator and are merged in via `evmd genesis collect-gentxs` (see README.md). +# In --dry-run mode the script generates ephemeral keys for all 4 validators, exercises +# a 1-validator gentx + collect cycle, and runs `evmd genesis validate` end-to-end. +# +# Usage: +# bash scripts/mainnet/genesis.sh \ +# --validators-yaml scripts/mainnet/validators.yaml \ +# --foundation-yaml scripts/mainnet/foundation.yaml \ +# --genesis-time 2026-06-01T15:00:00Z \ +# --out-dir /tmp/og-mainnet +# +# bash scripts/mainnet/genesis.sh --dry-run --out-dir /tmp/og-dryrun + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/precompiles.sh +source "$SCRIPT_DIR/../lib/precompiles.sh" + +# ---------- Constants ---------- +CHAIN_ID="opengradient_1486-1" +BASE_DENOM="ogwei" +DISPLAY_DENOM="OPG" +KEY_ALGO="eth_secp256k1" +KEYRING="test" # ceremony validators use --keyring-backend=file; --dry-run uses test +# Canonical OPG supply lives as ERC-20 on Base. The L1 `bank.supply` at genesis is the +# sum of L1 balances seated in this script (validator self-stakes + foundation alloc), +# typically ~40 OPG. Foundation and the x/svip pool are funded post-launch via bridge. +CANONICAL_SUPPLY_OPG_BASE=1000000000 # reference only; not used in genesis math +VALIDATOR_COUNT_EXPECTED=4 +# Symbolic genesis self-stake. Voting power in PoA comes from /poa.MsgAddValidator, +# not from the gentx amount, so this is intentionally tiny (10 OPG). Validators can +# raise their self-delegation later with MsgDelegate / MsgEditValidator. +DEFAULT_SELF_STAKE_OPG=10 + +UNBONDING_TIME="1814400s" +MAX_VALIDATORS=75 +MAX_ENTRIES=7 +HISTORICAL_ENTRIES=10000 +MIN_COMMISSION="0.050000000000000000" +COMMUNITY_TAX="0.000000000000000000" +INFLATION="0.000000000000000000" +GOAL_BONDED="0.670000000000000000" +BLOCKS_PER_YEAR=15768000 +GOV_MIN_DEPOSIT_OPG=5000 +GOV_EXP_MIN_DEPOSIT_OPG=25000 +MAX_DEPOSIT_PERIOD="604800s" +VOTING_PERIOD="432000s" +QUORUM="0.334000000000000000" +THRESHOLD="0.500000000000000000" +VETO_THRESHOLD="0.334000000000000000" +EXP_VOTING_PERIOD="86400s" +EXP_THRESHOLD="0.667000000000000000" +SIGNED_BLOCKS_WINDOW="10000" +MIN_SIGNED_PER_WINDOW="0.050000000000000000" +DOWNTIME_JAIL_DURATION="600s" +SLASH_DOUBLE_SIGN="0.000000000000000000" +SLASH_DOWNTIME="0.000000000000000000" +BASE_FEE_OGWEI="1000000000.000000000000000000" +MIN_GAS_PRICE_OGWEI="1.000000000000000000" +BASE_FEE_DENOMINATOR=8 +ELASTICITY_MULTIPLIER=2 +MIN_GAS_MULTIPLIER="0.500000000000000000" +BLOCK_MAX_BYTES="22020096" +BLOCK_MAX_GAS="40000000" +EVIDENCE_MAX_AGE_BLOCKS="100000" +EVIDENCE_MAX_AGE_DURATION="172800000000000" +EVIDENCE_MAX_BYTES="1048576" +HISTORY_SERVE_WINDOW="8192" +CRISIS_FEE_OPG=10000 +COMMISSION_MAX="0.500000000000000000" +COMMISSION_CHANGE_MAX="0.010000000000000000" +# Matches DEFAULT_SELF_STAKE_OPG: gentx fails if self-delegation < min-self-delegation. +# Each validator can raise their floor post-launch with MsgEditValidator. +MIN_SELF_DELEGATION_OPG=10 + +NATIVE_PRECOMPILE='"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"' + +# ---------- Helpers ---------- +die() { echo "ERROR: $*" >&2; exit 1; } +require() { [[ -n "${!1:-}" ]] || die "$1 is required"; } +guard() { [[ "$2" == "$3" ]] || die "GUARD FAIL: $1 = $2 (expected $3)"; ok "$1 = $2"; } +ok() { echo " ✓ $*"; } + +# OPG → ogwei (10^18). OPG is a non-negative integer; appending 18 zeros is exact and +# avoids needing `bc`. +opg_to_ogwei() { + printf '%s000000000000000000\n' "$1" +} + +# Apply one or more jq filters to genesis.json in place. +jqp() { + jq "$@" "$GENESIS" > "$TMP" + mv "$TMP" "$GENESIS" +} + +# ---------- CLI parsing ---------- +VALIDATORS_YAML=""; FOUNDATION_YAML=""; GENESIS_TIME=""; OUT_DIR=""; DRY_RUN=false + +usage() { sed -n '2,18p' "$0"; exit 1; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --validators-yaml) VALIDATORS_YAML="$2"; shift 2 ;; + --foundation-yaml) FOUNDATION_YAML="$2"; shift 2 ;; + --genesis-time) GENESIS_TIME="$2"; shift 2 ;; + --out-dir) OUT_DIR="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + -h|--help) usage ;; + *) die "unknown flag: $1" ;; + esac +done + +require OUT_DIR +if ! $DRY_RUN; then + require VALIDATORS_YAML + require FOUNDATION_YAML + require GENESIS_TIME +fi + +for cmd in evmd jq shasum bc; do command -v "$cmd" >/dev/null || die "$cmd not installed"; done +$DRY_RUN || command -v yq >/dev/null || die "yq not installed (Mike Farah Go version)" + +CHAINDIR="$OUT_DIR" +GENESIS="$CHAINDIR/config/genesis.json" +TMP="$CHAINDIR/config/tmp_genesis.json" + +echo "==> output dir: $CHAINDIR" +rm -rf "$CHAINDIR" + +$DRY_RUN && GENESIS_TIME="2026-06-01T00:00:00Z" + +# ---------- 1. Init chain ---------- +mkdir -p "$CHAINDIR" +evmd init og-mainnet -o --chain-id "$CHAIN_ID" --home "$CHAINDIR" >/dev/null 2>&1 + +# ---------- 2. Top-level + denoms + denom_metadata ---------- +echo "==> chain identity, denoms, denom_metadata" +jqp \ + --arg chain_id "$CHAIN_ID" \ + --arg genesis_time "$GENESIS_TIME" \ + --arg denom "$BASE_DENOM" \ + --arg display "$DISPLAY_DENOM" \ + ' + .chain_id = $chain_id + | .genesis_time = $genesis_time + | .app_state.staking.params.bond_denom = $denom + | .app_state.gov.params.min_deposit[0].denom = $denom + | .app_state.gov.params.expedited_min_deposit[0].denom = $denom + | .app_state.evm.params.evm_denom = $denom + | .app_state.mint.params.mint_denom = $denom + | .app_state.bank.denom_metadata = [{ + description: "OpenGradient native token", + denom_units: [ + { denom: $denom, exponent: 0, aliases: [] }, + { denom: $display, exponent: 18, aliases: [] } + ], + base: $denom, display: $display, + name: "OpenGradient", symbol: $display, + uri: "", uri_hash: "" + }] + ' + +# ---------- 3. Staking ---------- +echo "==> staking" +jqp \ + --arg unbonding "$UNBONDING_TIME" \ + --argjson max_v "$MAX_VALIDATORS" \ + --argjson max_e "$MAX_ENTRIES" \ + --argjson hist "$HISTORICAL_ENTRIES" \ + --arg min_comm "$MIN_COMMISSION" \ + '.app_state.staking.params |= ( + .unbonding_time = $unbonding + | .max_validators = $max_v + | .max_entries = $max_e + | .historical_entries = $hist + | .min_commission_rate = $min_comm + )' + +# ---------- 4. Mint (zero inflation) ---------- +echo "==> mint (zero inflation)" +jqp \ + --arg zero "$INFLATION" \ + --arg goal "$GOAL_BONDED" \ + --argjson bpy "$BLOCKS_PER_YEAR" \ + ' + .app_state.mint.minter |= ( + .inflation = $zero + | .annual_provisions = "0.000000000000000000" + ) + | .app_state.mint.params |= ( + .inflation_rate_change = $zero + | .inflation_max = $zero + | .inflation_min = $zero + | .goal_bonded = $goal + | .blocks_per_year = ($bpy | tostring) + ) + ' + +# ---------- 5. Distribution (zero community tax) ---------- +echo "==> distribution (zero community tax)" +jqp \ + --arg ctax "$COMMUNITY_TAX" \ + '.app_state.distribution.params |= ( + .community_tax = $ctax + | .base_proposer_reward = "0.000000000000000000" + | .bonus_proposer_reward = "0.000000000000000000" + | .withdraw_addr_enabled = true + )' + +# ---------- 6. Governance ---------- +echo "==> governance" +GOV_MIN_DEPOSIT_WEI=$(opg_to_ogwei "$GOV_MIN_DEPOSIT_OPG") +GOV_EXP_MIN_DEPOSIT_WEI=$(opg_to_ogwei "$GOV_EXP_MIN_DEPOSIT_OPG") +jqp \ + --arg denom "$BASE_DENOM" \ + --arg min_dep "$GOV_MIN_DEPOSIT_WEI" \ + --arg exp_min "$GOV_EXP_MIN_DEPOSIT_WEI" \ + --arg max_dep_period "$MAX_DEPOSIT_PERIOD" \ + --arg vote_period "$VOTING_PERIOD" \ + --arg quorum "$QUORUM" \ + --arg threshold "$THRESHOLD" \ + --arg veto "$VETO_THRESHOLD" \ + --arg exp_period "$EXP_VOTING_PERIOD" \ + --arg exp_thresh "$EXP_THRESHOLD" \ + '.app_state.gov.params |= ( + .min_deposit = [{denom: $denom, amount: $min_dep}] + | .expedited_min_deposit = [{denom: $denom, amount: $exp_min}] + | .max_deposit_period = $max_dep_period + | .voting_period = $vote_period + | .quorum = $quorum + | .threshold = $threshold + | .veto_threshold = $veto + | .expedited_voting_period = $exp_period + | .expedited_threshold = $exp_thresh + | .burn_vote_veto = true + | .burn_vote_quorum = false + | .burn_proposal_deposit_prevote = false + )' + +# ---------- 7. Slashing (disabled at launch) ---------- +echo "==> slashing (slash fractions = 0)" +jqp \ + --arg window "$SIGNED_BLOCKS_WINDOW" \ + --arg minsig "$MIN_SIGNED_PER_WINDOW" \ + --arg jail "$DOWNTIME_JAIL_DURATION" \ + --arg ds "$SLASH_DOUBLE_SIGN" \ + --arg dt "$SLASH_DOWNTIME" \ + '.app_state.slashing.params |= ( + .signed_blocks_window = $window + | .min_signed_per_window = $minsig + | .downtime_jail_duration = $jail + | .slash_fraction_double_sign = $ds + | .slash_fraction_downtime = $dt + )' + +# ---------- 8. EVM ---------- +echo "==> evm + precompiles" +jqp \ + --argjson precompiles "$ACTIVE_STATIC_PRECOMPILES" \ + --arg history "$HISTORY_SERVE_WINDOW" \ + '.app_state.evm.params |= ( + .access_control.create.access_type = "ACCESS_TYPE_PERMISSIONLESS" + | .access_control.call.access_type = "ACCESS_TYPE_PERMISSIONLESS" + | .active_static_precompiles = $precompiles + | .history_serve_window = $history + | .extra_eips = [] + )' + +# ---------- 9. Feemarket (EIP-1559) ---------- +echo "==> feemarket (EIP-1559)" +jqp \ + --arg base_fee "$BASE_FEE_OGWEI" \ + --arg min_gas "$MIN_GAS_PRICE_OGWEI" \ + --argjson denom "$BASE_FEE_DENOMINATOR" \ + --argjson elastic "$ELASTICITY_MULTIPLIER" \ + --arg multiplier "$MIN_GAS_MULTIPLIER" \ + '.app_state.feemarket.params |= ( + .no_base_fee = false + | .base_fee = $base_fee + | .min_gas_price = $min_gas + | .base_fee_change_denominator = $denom + | .elasticity_multiplier = $elastic + | .min_gas_multiplier = $multiplier + | .enable_height = "0" + )' + +# ---------- 10. ERC-20 ---------- +echo "==> erc20" +jqp \ + --argjson native "$NATIVE_PRECOMPILE" \ + --arg denom "$BASE_DENOM" \ + ' + .app_state.erc20.params |= (.enable_erc20 = true | .permissionless_registration = false) + | .app_state.erc20.native_precompiles = [$native] + | .app_state.erc20.token_pairs = [{ + contract_owner: 1, + erc20_address: $native, + denom: $denom, + enabled: true + }] + ' + +# ---------- 11. SVIP (dormant at genesis) ---------- +# Schema per proto/cosmos/svip/v1/genesis.proto on origin/feat/svip-module. +echo "==> svip (dormant)" +jqp ' + .app_state.svip = { + params: { half_life_seconds: "0" }, + activated: false, + paused: false, + total_distributed: "0", + pool_balance_at_activation: "0", + activation_time: "0001-01-01T00:00:00Z", + last_block_time: "0001-01-01T00:00:00Z", + total_paused_seconds: "0" + } +' + +# ---------- 12. Crisis ---------- +echo "==> crisis" +CRISIS_FEE_WEI=$(opg_to_ogwei "$CRISIS_FEE_OPG") +jqp \ + --arg denom "$BASE_DENOM" \ + --arg amt "$CRISIS_FEE_WEI" \ + '.app_state.crisis.constant_fee = {denom: $denom, amount: $amt}' + +# ---------- 13. IBC transfer ---------- +echo "==> ibc transfer" +jqp '.app_state.transfer.params |= (.send_enabled = true | .receive_enabled = true)' + +# ---------- 14. Consensus params ---------- +echo "==> consensus params" +jqp \ + --arg max_bytes "$BLOCK_MAX_BYTES" \ + --arg max_gas "$BLOCK_MAX_GAS" \ + --arg ev_blocks "$EVIDENCE_MAX_AGE_BLOCKS" \ + --arg ev_dur "$EVIDENCE_MAX_AGE_DURATION" \ + --arg ev_bytes "$EVIDENCE_MAX_BYTES" \ + '.consensus.params |= ( + .block.max_bytes = $max_bytes + | .block.max_gas = $max_gas + | .evidence.max_age_num_blocks = $ev_blocks + | .evidence.max_age_duration = $ev_dur + | .evidence.max_bytes = $ev_bytes + | .validator.pub_key_types = ["ed25519"] + )' + +# ---------- 15. Read inputs (or generate dry-run inputs) ---------- +declare -a VAL_MONIKERS VAL_ADDRS VAL_SELF_STAKES +declare FOUND_ADDR FOUND_ALLOC_OPG + +if $DRY_RUN; then + echo "==> DRY RUN: generating ephemeral keys for foundation + $VALIDATOR_COUNT_EXPECTED validators" + evmd keys add foundation --keyring-backend "$KEYRING" --algo "$KEY_ALGO" --home "$CHAINDIR" >/dev/null 2>&1 + FOUND_ADDR=$(evmd keys show foundation -a --keyring-backend "$KEYRING" --home "$CHAINDIR") + FOUND_ALLOC_OPG=null + for i in $(seq 1 $VALIDATOR_COUNT_EXPECTED); do + keyname="val${i}" + evmd keys add "$keyname" --keyring-backend "$KEYRING" --algo "$KEY_ALGO" --home "$CHAINDIR" >/dev/null 2>&1 + VAL_MONIKERS[i]="og-mainnet-val-$i" + VAL_ADDRS[i]=$(evmd keys show "$keyname" -a --keyring-backend "$KEYRING" --home "$CHAINDIR") + VAL_SELF_STAKES[i]=$DEFAULT_SELF_STAKE_OPG + done +else + echo "==> reading $FOUNDATION_YAML and $VALIDATORS_YAML" + FOUND_ADDR=$(yq -r '.foundation.address' "$FOUNDATION_YAML") + FOUND_ALLOC_OPG=$(yq -r '.foundation.allocation_opg' "$FOUNDATION_YAML") + # Catch a forgotten template: refuse to bootstrap with placeholder multisig metadata. + FOUND_THRESHOLD=$(yq -r '.foundation.multisig_threshold' "$FOUNDATION_YAML") + FOUND_MEMBERS=$(yq -r '.foundation.multisig_members | length' "$FOUNDATION_YAML") + [[ "$FOUND_THRESHOLD" =~ ^[0-9]+$ && "$FOUND_THRESHOLD" -gt 0 ]] \ + || die "foundation.multisig_threshold must be a positive integer (got '$FOUND_THRESHOLD')" + [[ "$FOUND_MEMBERS" -ge "$FOUND_THRESHOLD" ]] \ + || die "foundation.multisig_members has $FOUND_MEMBERS entries, fewer than threshold $FOUND_THRESHOLD" + vcount=$(yq -r '.validators | length' "$VALIDATORS_YAML") + [[ "$vcount" == "$VALIDATOR_COUNT_EXPECTED" ]] || die "expected $VALIDATOR_COUNT_EXPECTED validators, got $vcount" + for i in $(seq 1 "$vcount"); do + idx=$((i-1)) + VAL_MONIKERS[i]=$(yq -r ".validators[$idx].moniker" "$VALIDATORS_YAML") + VAL_ADDRS[i]=$(yq -r ".validators[$idx].operator_addr" "$VALIDATORS_YAML") + VAL_SELF_STAKES[i]=$(yq -r ".validators[$idx].self_stake_opg" "$VALIDATORS_YAML") + done +fi + +[[ "$FOUND_ADDR" =~ ^og1 ]] || die "foundation address must start with og1: '$FOUND_ADDR'" +for i in $(seq 1 $VALIDATOR_COUNT_EXPECTED); do + [[ "${VAL_ADDRS[i]}" =~ ^og1 ]] || die "validator $i address must start with og1: '${VAL_ADDRS[i]}'" +done + +# ---------- 16. Compute supply allocation ---------- +TOTAL_VAL_STAKE_OPG=0 +for i in $(seq 1 $VALIDATOR_COUNT_EXPECTED); do + TOTAL_VAL_STAKE_OPG=$((TOTAL_VAL_STAKE_OPG + VAL_SELF_STAKES[i])) +done + +if [[ "$FOUND_ALLOC_OPG" == "null" || -z "$FOUND_ALLOC_OPG" ]]; then + FOUND_ALLOC_OPG=0 +fi + +# L1 genesis supply is intentionally minimal. Foundation bridges in post-launch. +TOTAL_L1_SUPPLY_OPG=$((FOUND_ALLOC_OPG + TOTAL_VAL_STAKE_OPG)) +echo "==> L1 genesis supply: $TOTAL_L1_SUPPLY_OPG OPG (validators: $TOTAL_VAL_STAKE_OPG, foundation: $FOUND_ALLOC_OPG; svip: 0 — funded via bridge before activation)" + +# Sanity: if the L1 supply somehow exceeds 1% of canonical, refuse — it's almost +# certainly a yaml input mistake and would create an unbacked-tokens drift on Base. +ONE_PERCENT=$((CANONICAL_SUPPLY_OPG_BASE / 100)) +[[ "$TOTAL_L1_SUPPLY_OPG" -le "$ONE_PERCENT" ]] \ + || die "L1 supply $TOTAL_L1_SUPPLY_OPG OPG exceeds 1% of canonical ($ONE_PERCENT OPG); check yaml inputs" + +# ---------- 17. Add genesis accounts ---------- +echo "==> add genesis accounts" +if [[ "$FOUND_ALLOC_OPG" -gt 0 ]]; then + evmd genesis add-genesis-account "$FOUND_ADDR" "$(opg_to_ogwei "$FOUND_ALLOC_OPG")${BASE_DENOM}" --home "$CHAINDIR" >/dev/null +fi +for i in $(seq 1 $VALIDATOR_COUNT_EXPECTED); do + evmd genesis add-genesis-account "${VAL_ADDRS[i]}" "$(opg_to_ogwei "${VAL_SELF_STAKES[i]}")${BASE_DENOM}" --home "$CHAINDIR" >/dev/null +done + +# ---------- 18. Update bank.supply ---------- +# L1 `bank.supply` MUST equal the sum of L1 balances (Cosmos SDK invariant). The svip +# module account is intentionally NOT pre-funded here — Foundation locks 100M OPG on +# Base and bridges into address `module_address("svip")` before activating SVIP. +TOTAL_L1_SUPPLY_WEI=$(opg_to_ogwei "$TOTAL_L1_SUPPLY_OPG") +jqp \ + --arg denom "$BASE_DENOM" \ + --arg amt "$TOTAL_L1_SUPPLY_WEI" \ + '.app_state.bank.supply = [{denom: $denom, amount: $amt}]' + +# ---------- 20. Gentxs ---------- +if $DRY_RUN; then + echo "==> DRY RUN: producing gentx for val1" + evmd genesis gentx val1 \ + "$(opg_to_ogwei "${VAL_SELF_STAKES[1]}")${BASE_DENOM}" \ + --commission-rate "0.05" \ + --commission-max-rate "$COMMISSION_MAX" \ + --commission-max-change-rate "$COMMISSION_CHANGE_MAX" \ + --min-self-delegation "$(opg_to_ogwei "$MIN_SELF_DELEGATION_OPG")" \ + --keyring-backend "$KEYRING" \ + --chain-id "$CHAIN_ID" \ + --home "$CHAINDIR" \ + --moniker "${VAL_MONIKERS[1]}" \ + --gas-prices "${BASE_FEE_OGWEI%.*}${BASE_DENOM}" \ + >/dev/null + evmd genesis collect-gentxs --home "$CHAINDIR" >/dev/null +else + cat < Pre-gentx genesis assembled. Distribute $GENESIS to all $VALIDATOR_COUNT_EXPECTED operators. + + Each operator (with their own \$NODE_HOME): + + cp \$NODE_HOME/config/genesis.json + evmd genesis gentx $(opg_to_ogwei $DEFAULT_SELF_STAKE_OPG)$BASE_DENOM \\ + --commission-rate 0.05 --commission-max-rate $COMMISSION_MAX \\ + --commission-max-change-rate $COMMISSION_CHANGE_MAX \\ + --min-self-delegation $(opg_to_ogwei $MIN_SELF_DELEGATION_OPG) \\ + --keyring-backend file --chain-id $CHAIN_ID \\ + --gas-prices ${BASE_FEE_OGWEI%.*}${BASE_DENOM} --moniker + + Operators submit the gentx file. Foundation drops all $VALIDATOR_COUNT_EXPECTED into + \$CHAINDIR/config/gentx/ then runs: + + evmd genesis collect-gentxs --home $CHAINDIR + +EOF +fi + +# ---------- 21. Hard guards ---------- +echo "" +echo "==> hard-guard verification" +read -r INF INF_MAX CTAX MAX_GAS SVIP_ACT BOND_D EVM_D MINT_D <<< "$( + jq -r '[ + .app_state.mint.minter.inflation, + .app_state.mint.params.inflation_max, + .app_state.distribution.params.community_tax, + .consensus.params.block.max_gas, + (.app_state.svip.activated | tostring), + .app_state.staking.params.bond_denom, + .app_state.evm.params.evm_denom, + .app_state.mint.params.mint_denom + ] | @tsv' "$GENESIS" +)" + +guard "mint.inflation" "$INF" "$INFLATION" +guard "mint.inflation_max" "$INF_MAX" "$INFLATION" +guard "distribution.community_tax" "$CTAX" "$COMMUNITY_TAX" +[[ "$MAX_GAS" != "-1" ]] || die "block.max_gas = -1 (unbounded)" +ok "block.max_gas = $MAX_GAS (finite)" +guard "svip.activated" "$SVIP_ACT" "false" +[[ "$BOND_D" == "$BASE_DENOM" && "$EVM_D" == "$BASE_DENOM" && "$MINT_D" == "$BASE_DENOM" ]] \ + || die "denom mismatch (bond=$BOND_D evm=$EVM_D mint=$MINT_D)" +ok "bond_denom = evm_denom = mint_denom = $BASE_DENOM" + +# `bc` for the only arbitrary-precision arithmetic (27-digit ogwei sum overflows bash int). +balances_sum=$(jq -r '.app_state.bank.balances[].coins[] | select(.denom=="ogwei") | .amount' "$GENESIS" \ + | paste -sd+ - | bc) +supply=$(jq -r '.app_state.bank.supply[0].amount' "$GENESIS") +guard "Σ balances vs supply" "$balances_sum" "$supply" +guard "L1 bank.supply" "$supply" "$TOTAL_L1_SUPPLY_WEI" + +# ---------- 22. evmd genesis validate ---------- +echo "" +echo "==> evmd genesis validate" +evmd genesis validate --home "$CHAINDIR" + +echo "" +echo "==> SUCCESS" +echo " chain_id: $CHAIN_ID" +echo " genesis_time: $GENESIS_TIME" +echo " genesis.json: $GENESIS" +echo " sha256: $(shasum -a 256 "$GENESIS" | awk '{print $1}')" diff --git a/scripts/mainnet/validators.yaml b/scripts/mainnet/validators.yaml new file mode 100644 index 00000000..ef3407c3 --- /dev/null +++ b/scripts/mainnet/validators.yaml @@ -0,0 +1,65 @@ +# OG-EVM Mainnet Genesis Validators +# +# Four validators at launch. The coordinator collects one entry per node. +# +# These 40 OPG (4 × 10) are the ENTIRE L1 supply at genesis. The canonical 1B OPG +# lives as ERC-20 on Base; the Foundation bridges to L1 on demand post-launch. +# +# Per-entry fields: +# moniker human-readable name shown in block explorers +# operator_addr bech32 og1... address from the operator's eth_secp256k1 key +# consensus_pubkey tendermint/PubKeyEd25519 base64 string from priv_validator_key.json +# node_id libp2p node ID (hex) from node_key.json +# p2p_endpoint :26656 reachable from the other genesis nodes +# self_stake_opg amount in OPG; genesis.sh converts to ogwei +# +# How fields are used: +# genesis.sh consumes: moniker, operator_addr, self_stake_opg +# peer-list generation: node_id, p2p_endpoint +# record-keeping only: consensus_pubkey (each validator's gentx pulls it from +# priv_validator_key.json directly) +# +# About self_stake_opg = 10 (symbolic): +# In PoA, voting power comes from /poa.MsgAddValidator. The gentx amount is a +# bookkeeping placeholder. genesis.sh sets --min-self-delegation = self_stake_opg +# so MsgCreateValidator passes SDK validation. While PoA is active, MsgDelegate is +# blocked at the ante (see xrplevm/node/v10/x/poa/ante/poa.go), so validators +# cannot top up their self-delegation until governance transitions the chain to +# permissionless. min_self_delegation can be raised via MsgEditValidator any time. +# +# How to collect: +# operator_addr evmd keys show -a --keyring-backend file --home +# consensus_pubkey evmd comet show-validator --home (the "key" field) +# node_id evmd comet show-node-id --home +# +# Do NOT collect or transmit priv_validator_key.json, node_key.json, or operator +# mnemonics. Each stays on the node that generated it. + +validators: + - moniker: "REPLACE-validator-1" + operator_addr: "og1REPLACE0000000000000000000000000000000" + consensus_pubkey: "REPLACE_BASE64_ED25519_PUBKEY" + node_id: "REPLACE_HEX_NODE_ID" + p2p_endpoint: "REPLACE-host-1:26656" + self_stake_opg: 10 + + - moniker: "REPLACE-validator-2" + operator_addr: "og1REPLACE0000000000000000000000000000001" + consensus_pubkey: "REPLACE_BASE64_ED25519_PUBKEY" + node_id: "REPLACE_HEX_NODE_ID" + p2p_endpoint: "REPLACE-host-2:26656" + self_stake_opg: 10 + + - moniker: "REPLACE-validator-3" + operator_addr: "og1REPLACE0000000000000000000000000000002" + consensus_pubkey: "REPLACE_BASE64_ED25519_PUBKEY" + node_id: "REPLACE_HEX_NODE_ID" + p2p_endpoint: "REPLACE-host-3:26656" + self_stake_opg: 10 + + - moniker: "REPLACE-validator-4" + operator_addr: "og1REPLACE0000000000000000000000000000003" + consensus_pubkey: "REPLACE_BASE64_ED25519_PUBKEY" + node_id: "REPLACE_HEX_NODE_ID" + p2p_endpoint: "REPLACE-host-4:26656" + self_stake_opg: 10