> For the complete documentation index, see [llms.txt](https://docs.gravity.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.gravity.xyz/gravity-networks/run-a-mainnet-validator.md).

# Run a Gravity Mainnet Validator (Invite Only)

This guide walks approved operators through adding a validator to Gravity Mainnet (L1, Chain ID `127001`). A validator participates in AptosBFT consensus and must be backed by a StakePool that has been approved through governance.

Validator onboarding is currently **invite-only**. If you only need a node for reads, writes, or indexing, run a [Public Full Node](/gravity-networks/run-a-mainnet-pfn.md) instead.

> **Placeholders.** Commands below use `<YOUR_...>` placeholders. Replace each value before running. Never paste private keys into shared shells or commit generated node identities.

## Network Role

Validators connect to each other on the validator network and optionally expose VFN-network endpoints for VFNs:

```
validator set --validator net--> consensus
validator --VFN net--> VFN --public net--> PFN / RPC
```

The onboarding flow is:

1. Prepare validator config and identity.
2. Deploy the node package.
3. Fund the validator account.
4. Create a StakePool.
5. Add the StakePool to the validator whitelist through governance.
6. Join the validator set with the node's public identity.
7. Start the node and verify status.

## Prerequisites

1. **Approval from current governance / operators.** The StakePool must be whitelisted before it can join the validator set.
2. **A Linux x86-64 host**, preferably Ubuntu 24.04 LTS.
3. **`gravity_node` and `gravity_cli`.**

   ```bash
   git clone https://github.com/Galxe/gravity-sdk.git
   cd gravity-sdk
   RUSTFLAGS="--cfg tokio_unstable" \
     cargo build --profile quick-release -p gravity_node -p gravity_cli
   ```
4. **Mainnet `genesis.json` and `waypoint.txt`.** Use the canonical mainnet artifacts from [`gravity-sdk/genesis/mainnet`](https://github.com/Galxe/gravity-sdk/tree/main/genesis/mainnet); do not regenerate genesis locally.
5. **A fresh validator EVM account.** This account owns and operates the StakePool. Do not reuse public test keys for a long-running validator.
6. **System contract addresses** for staking, validator management, and governance. See the mainnet contract reference for current addresses.

## 1. Write `cluster-validator.toml`

Download the canonical mainnet artifacts where the config below expects them:

```bash
mkdir -p <YOUR_GRAVITY_SDK_PATH>/cluster/output
curl -fsSL https://raw.githubusercontent.com/Galxe/gravity-sdk/main/genesis/mainnet/genesis.json \
  -o <YOUR_GRAVITY_SDK_PATH>/cluster/output/genesis.json
curl -fsSL https://raw.githubusercontent.com/Galxe/gravity-sdk/main/genesis/mainnet/waypoint.txt \
  -o <YOUR_GRAVITY_SDK_PATH>/cluster/output/waypoint.txt
```

Create a config such as:

```toml
[cluster]
name = "<YOUR_VALIDATOR_CLUSTER_NAME>"
base_dir = "<YOUR_DEPLOY_BASE_DIR>"

[genesis_source]
genesis_path = "./output/genesis.json"
waypoint_path = "./output/waypoint.txt"

[relayer]
relayer_rpc_url = "<YOUR_L1_RPC_URL>"

[[nodes]]
id = "validator-1"
role = "validator"
source = { bin_path = "../target/quick-release/gravity_node" }
identity = { source = "file" }
host = "<YOUR_VALIDATOR_HOST>"

# Deploy directory, not the database directory.
# deploy.sh renders storage to <YOUR_DEPLOY_BASE_DIR>/validator-1/data
# and reth to <YOUR_DEPLOY_BASE_DIR>/validator-1/data/reth.
data_dir = "<YOUR_DEPLOY_BASE_DIR>/validator-1"

validator_port = 6180
vfn_port = 6190
rpc_port = 8545
metrics_port = 9001
inspection_port = 10001
https_port = 11001
authrpc_port = 8661
reth_p2p_port = 12024
txpool_max_account_slots = 64
```

If identity is stored in a secret manager, pin a fixed version rather than `latest`:

```toml
identity = {
  source = "gcp_secret",
  secret = "projects/<YOUR_PROJECT>/secrets/<YOUR_SECRET>/versions/1",
}
```

## 2. Generate Identity and Deploy

```bash
cd <YOUR_GRAVITY_SDK_PATH>/cluster
just init cluster-validator.toml
just deploy cluster-validator.toml
```

Expected layout:

```
<YOUR_DEPLOY_BASE_DIR>/validator-1/config/
<YOUR_DEPLOY_BASE_DIR>/validator-1/data/
<YOUR_DEPLOY_BASE_DIR>/validator-1/script/start.sh
<YOUR_DEPLOY_BASE_DIR>/validator-1/script/stop.sh
```

The public identity used during `validator join` is written to:

```
<YOUR_GRAVITY_SDK_PATH>/cluster/output/validator-1/config/identity.public.yaml
```

## 3. Optional: Warm Start the Databases

To reduce cold-sync time, copy only chain databases from an existing RPC/PFN you control:

```bash
cd <YOUR_EXISTING_NODE_DATA_DIR>
tar -cf - consensus_db quorumstoreDB reth | \
  ssh <YOUR_TARGET_HOST> \
    "mkdir -p <YOUR_DEPLOY_BASE_DIR>/validator-1/data && \
     cd <YOUR_DEPLOY_BASE_DIR>/validator-1/data && tar -xf -"
```

Do **not** copy another node's `secure_storage.json`. It contains safety-rules owner and consensus-key state. Reusing it can produce errors such as:

```
The validator is not in the validator set. Address not in set: <OLD_OWNER_ACCOUNT>
```

## 4. Fund the Validator Account

Set common variables:

```bash
export RPC_URL="https://mainnet-rpc.gravity.xyz"
export VALIDATOR_ADDRESS="<YOUR_VALIDATOR_EVM_ADDRESS>"
export STAKE_AMOUNT="<YOUR_STAKE_AMOUNT>"
export STAKING_ADDRESS="<STAKING_CONTRACT_ADDRESS>"
export VALIDATOR_MANAGER_ADDRESS="<VALIDATOR_MANAGER_CONTRACT_ADDRESS>"
export GOVERNANCE_ADDRESS="<GOVERNANCE_CONTRACT_ADDRESS>"
```

Fund the validator account with enough balance for stake and gas:

```bash
cast balance "$VALIDATOR_ADDRESS" --rpc-url "$RPC_URL"
```

The validator account must sign StakePool and validator lifecycle operations. Use your normal secure signing workflow. If `gravity_cli` prompts for a key on stdin, ensure the signer is the StakePool operator.

## 5. Create a StakePool

Use the validator account to create a StakePool:

```bash
gravity_cli stake create \
  --rpc-url "$RPC_URL" \
  --stake-amount "$STAKE_AMOUNT" \
  --lockup-duration <YOUR_LOCKUP_SECONDS> \
  --gas-limit <YOUR_GAS_LIMIT>
```

Record the created pool:

```bash
export STAKE_POOL="<YOUR_STAKE_POOL_ADDRESS>"
```

Verify:

```bash
gravity_cli stake get \
  --rpc-url "$RPC_URL" \
  --owner "$VALIDATOR_ADDRESS"

cast call "$STAKING_ADDRESS" \
  "isPool(address)(bool)" "$STAKE_POOL" \
  --rpc-url "$RPC_URL"
```

If the chain enforces a minimum lockup duration, do not set the lockup exactly at the minimum. Add a buffer so timestamp drift does not trigger `LockupDurationTooShort`.

## 6. Whitelist the StakePool

The whitelist target is the StakePool address, not the validator EOA:

```solidity
ValidatorManagement.setValidatorPoolAllowed(address stakePool, bool allowed)
```

This call must go through governance. Direct calls from an EOA are expected to revert.

Create calldata:

```bash
export ALLOW_CALL=$(cast calldata \
  "setValidatorPoolAllowed(address,bool)" \
  "$STAKE_POOL" true)
```

Create, vote, resolve, and execute the proposal according to the current governance process:

```bash
cast send "$GOVERNANCE_ADDRESS" \
  "createProposal(address,address[],bytes[],string)(uint64)" \
  "<YOUR_PROPOSER_POOL>" \
  "[$VALIDATOR_MANAGER_ADDRESS]" \
  "[$ALLOW_CALL]" \
  "add-validator-whitelist-validator-1" \
  --rpc-url "$RPC_URL" \
  --private-key <YOUR_PROPOSER_PRIVATE_KEY>

export PROPOSAL_ID="<YOUR_PROPOSAL_ID>"

cast send "$GOVERNANCE_ADDRESS" \
  "vote(address,uint64,uint128,bool)" \
  "<YOUR_PROPOSER_POOL>" "$PROPOSAL_ID" \
  340282366920938463463374607431768211455 true \
  --rpc-url "$RPC_URL" \
  --private-key <YOUR_PROPOSER_PRIVATE_KEY>

cast send "$GOVERNANCE_ADDRESS" \
  "resolve(uint64)" "$PROPOSAL_ID" \
  --rpc-url "$RPC_URL" \
  --private-key <YOUR_PROPOSER_PRIVATE_KEY>

cast send "$GOVERNANCE_ADDRESS" \
  "execute(uint64,address[],bytes[])" \
  "$PROPOSAL_ID" \
  "[$VALIDATOR_MANAGER_ADDRESS]" \
  "[$ALLOW_CALL]" \
  --rpc-url "$RPC_URL" \
  --private-key <YOUR_GOVERNANCE_EXECUTOR_PRIVATE_KEY>
```

Confirm the pool is allowed:

```bash
cast call "$VALIDATOR_MANAGER_ADDRESS" \
  "isValidatorPoolAllowed(address)(bool)" "$STAKE_POOL" \
  --rpc-url "$RPC_URL"
```

## 7. Join the Validator Set

Read the node's public identity:

```bash
export CONSENSUS_PUBLIC_KEY=$(yq -r '.consensus_public_key' \
  <YOUR_GRAVITY_SDK_PATH>/cluster/output/validator-1/config/identity.public.yaml)
export CONSENSUS_POP=$(yq -r '.consensus_pop' \
  <YOUR_GRAVITY_SDK_PATH>/cluster/output/validator-1/config/identity.public.yaml)
export NETWORK_PUBLIC_KEY=$(yq -r '.network_public_key' \
  <YOUR_GRAVITY_SDK_PATH>/cluster/output/validator-1/config/identity.public.yaml)
```

Join:

```bash
gravity_cli validator join \
  --rpc-url "$RPC_URL" \
  --stake-pool "$STAKE_POOL" \
  --consensus-public-key "$CONSENSUS_PUBLIC_KEY" \
  --consensus-pop "$CONSENSUS_POP" \
  --network-public-key "$NETWORK_PUBLIC_KEY" \
  --validator-network-address "/dns/<YOUR_VALIDATOR_HOST>/tcp/6180" \
  --fullnode-network-address "/dns/<YOUR_VALIDATOR_HOST>/tcp/6190" \
  --moniker "validator-1" \
  --gas-limit <YOUR_GAS_LIMIT>
```

The signer must be the StakePool operator.

## 8. Start and Verify

```bash
bash <YOUR_DEPLOY_BASE_DIR>/validator-1/script/start.sh
```

Check validator set and epoch status:

```bash
gravity_cli validator list --rpc-url "$RPC_URL"
gravity_cli epoch status --rpc-url "$RPC_URL"

cast call "$VALIDATOR_MANAGER_ADDRESS" \
  "getValidatorStatus(address)(uint8)" "$STAKE_POOL" \
  --rpc-url "$RPC_URL"
```

Status values:

| Value | Status             |
| ----- | ------------------ |
| `0`   | `INACTIVE`         |
| `1`   | `PENDING_ACTIVE`   |
| `2`   | `ACTIVE`           |
| `3`   | `PENDING_INACTIVE` |

After joining, a validator usually enters `PENDING_ACTIVE` and becomes `ACTIVE` after an epoch transition.

You can also check node health:

```bash
gravity_cli status --rpc-url "$RPC_URL"
curl -s http://127.0.0.1:8545 \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
```

## Leave the Validator Set

To leave:

```bash
gravity_cli validator leave \
  --rpc-url "$RPC_URL" \
  --stake-pool "$STAKE_POOL" \
  --gas-limit <YOUR_GAS_LIMIT>
```

If the validator is `ACTIVE`, it becomes `PENDING_INACTIVE` and exits after the next epoch transition. If it is still `PENDING_ACTIVE`, it can become `INACTIVE` immediately.

Verify:

```bash
gravity_cli validator list --rpc-url "$RPC_URL"
```

## Troubleshooting

| Symptom                                   | Cause / fix                                                                                                   |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `isValidatorPoolAllowed` returns `false`  | The governance whitelist proposal has not executed, or it targeted the wrong StakePool.                       |
| `LockupDurationTooShort`                  | Increase the lockup duration above the minimum with a buffer.                                                 |
| Validator remains `PENDING_ACTIVE`        | Activation normally happens at the next epoch transition. Check `gravity_cli epoch status`.                   |
| `Address not in set: <OLD_OWNER_ACCOUNT>` | Another node's `secure_storage.json` was copied. Remove it and regenerate/redeploy with the correct identity. |
| Node paths do not point to `<node>/data`  | `data_dir` should be the deploy directory, not the database directory.                                        |

## See Also

* [Run a Gravity Mainnet Public Full Node](/gravity-networks/run-a-mainnet-pfn.md)
* [Run a Gravity Mainnet VFN](/gravity-networks/run-a-mainnet-vfn.md)
* [Gravity Mainnet (L1)](/gravity-networks/l1-mainnet.md)
* [Gravity Mainnet (L1) Contracts](/developer-resources/mainnet.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.gravity.xyz/gravity-networks/run-a-mainnet-validator.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
