Becoming a Validator

Registering as a validator and making self-delegations

Similar to other Delegated Proof-of-Stake networks, Exocore requires validators to stake their own tokens and delegators to delegate their token to these validators. However, one major distinction is that Exocore supports multiple types of tokens, which may or may not live on the same chain.

Exocore is an L1 Cosmos-based chain which supports multiple tokens on Ethereum Sepolia and Holesky testnets, with additional chains to be supported in the near future. To that end, users are required to have two addresses - one on Ethereum and one on Exocore (generated previously) which may or may not be derived from the same mnemonic.

If any of the below transactions produce error code -32000: execution reverted, it likely means that you don't have enough ETH to pay for gas. This is caused by a spike in Sepolia gas prices; you can either (1) wait for the gas prices to normalize, or (2) obtain more Sepolia ETH through the faucets.

Deposit Tokens

Acquire Tokens

At the time of writing, Exocore's testnet (exocoretestnet_233-5) supports exoETH and wstETH on Sepolia.

  • ETH on Sepolia / Holesky can be obtained from faucets provided by Alchemy or Infura.

    • Purpose: ETH is used to pay for gas on the Ethereum testnet.

  • exoETH can be obtained from Exocore contributors.

    • Purpose: exoETH is used for staking on Exocore testnet.

  • wstETH can be obtained by sending Sepolia ETH to the wstETH address 0xB82381A3fBD3FaFA77B3a7bE693342618240067b.

    • Purpose: wstETH is Lido’s liquid staking token and is used for restaking.

  • exo can be obtained from Exocore’s faucet after the bootstrap phase

    • Purpose: exo is used to pay for gas on Exocore.

Deposit Tokens

The contract which accepts deposits is currently deployed on Ethereum, which means the Ethereum private key ($ETH_PRIVATE_KEY) is needed.

## Script constants
EXO_ETH_ADDR=0x83E6850591425e3C1E263c054f4466838B9Bd9e4
WST_ETH_ADDR=0xB82381A3fBD3FaFA77B3a7bE693342618240067b
GATEWAY_ADDR=0x8fc4E764C2C3B0646ba572AEa958fB5724706412

## User choices
# Enter your Ethereum testnet private key
ETH_PRIVATE_KEY=0xabcde.....
# Enter the address corresponding to the key
ETH_ADDRESS=0x123....
# Obtain the token as described above, and choose the one you intend to deposit.
TOKEN=$EXO_ETH_ADDR
# Quantity of token to deposit (without the 1e18 multiplier)
# Decimal values are supported
QUANTITY=0.5
# Endpoint for Ethereum RPC
ETHEREUM_RPC_URL=https://rpc.ankr.com/eth_sepolia

# Approve the deposit by Vault
VAULT_ADDR=$(cast call --rpc-url $ETHEREUM_RPC_URL $GATEWAY_ADDR "tokenToVault(address) returns (address)" $TOKEN)
cast send --rpc-url $ETHEREUM_RPC_URL \
    $TOKEN \
    "approve(address,uint256)" \
    $VAULT_ADDR \
    "$(cast maxu)" \
    --private-key $ETH_PRIVATE_KEY

BOOTSTRAPPED=$(cast call --rpc-url $ETHEREUM_RPC_URL $GATEWAY_ADDR "bootstrapped() returns (bool)")
if [ "$BOOTSTRAPPED" = "true" ]; then
    # Generate the LZ messsage to calculate the fees
    DEPOSIT_PREFIX="0x00"
    TOKEN_B32=$(cast 2b $TOKEN)
    ETH_ADDRESS_B32=$(cast 2b $ETH_ADDRESS)
    QUANTITY_B32=$(printf "%064s" $(cast 2h $(cast 2w $QUANTITY) | cut -c3-) | tr ' ' '0') # left padding
    LZ_MESSAGE=$(cast ch $DEPOSIT_PREFIX $TOKEN_B32 $ETH_ADDRESS_B32 $QUANTITY_B32)
    VALUE=$(cast call --rpc-url $ETHEREUM_RPC_URL $GATEWAY_ADDR "quote(bytes)" $LZ_MESSAGE)
else
    # No fees
    VALUE=0
fi

# Execute the transaction
cast send --rpc-url $ETHEREUM_RPC_URL \
    $GATEWAY_ADDR \
    "deposit(address,uint256)" \
    $TOKEN \
    $(cast 2w $QUANTITY) \
    --private-key $ETH_PRIVATE_KEY \
    --value $(cast 2d $VALUE)

The expected output of the commands should look similar to below:

blockHash               0x496b22768c38687dedeaf1f6b3823b0973b25ae90ea961e702f6dd4ba82a980a
blockNumber             6679149

<! -- truncated -->

transactionHash         0x24426c2acf0e6a411961f478bca68a36c12ed0bff3d7206cd1cd7662908d40cf
transactionIndex        84
type                    2
blobGasPrice            
blobGasUsed             
authorizationList       
to                      0x8fc4E764C2C3B0646ba572AEa958fB5724706412

If the above transaction was executed after the network bootstrap phase, LayerZeroScan may be used to check whether the Deposit message, sent from Ethereum to Exocore, was executed on the destination chain successfully. Once it is executed, the deposited amount can be queried from Exocore.

ETH_LZ_ID=40161 # Sepolia, where our $GATEWAY_ADDR lives
STAKER_ID=$(echo "${ETH_ADDRESS}" | tr '[:upper:]' '[:lower:]')_$(printf '0x%x\n' "${ETH_LZ_ID}")
# our public endpoint is available below, but you can use your own node too
EXOCORE_COS_GRPC_URL=https://api-cosmos-grpc.exocore-restaking.com:443
exocored query assets QueStakerAssetInfos \
    $STAKER_ID --node $EXOCORE_COS_GRPC_URL \
    --output json | jq

The expected output of this will represent the quantity of tokens deposited, multiplied by 1e18.

{
  "asset_infos": [
    {
      // the `asset_id` will vary depending on $TOKEN chosen
      // the `_0x9ce1` represents that hex value of the ETH_LZ_ID, a unique ID
      // assigned by LayerZero to the Sepolia network
      "asset_id": "0xb82381a3fbd3fafa77b3a7be693342618240067b_0x9ce1",
      "info": {
        // Represents a quantity of 0.5 * 1e18
        "total_deposit_amount": "500000000000000000",
        "withdrawable_amount": "500000000000000000",
        "pending_undelegation_amount": "0"
      }
    }
  ]
}

Validator Registration

Exocore has a unique, open-ended way to kickstart its network. Instead of locking in a small group of initial validators, Exocore allows anyone to become a validator right from the start using our Bootstrap contract.

Here's how it works:

  • Before the Network Launches: If the network isn't live yet, you can register as a validator through the Bootstrap contract on Ethereum. However, keep in mind that this registration process closes 24 hours before the network launch to give the bootstrap validators time to prepare.

  • After the Network Goes Live: Once the network is up and running, you can register directly on the Exocore chain itself.

This approach ensures that the validator set isn't pre-determined by a few stakeholders. Instead, it's open to anyone who wants to participate, making the launch phase more inclusive and decentralized.

To ensure your validator is eligible to participate in consensus, it is mandatory to have a minimum self-delegation of 1000 USD of value in any supported token. This self-delegation acts as a commitment and ensures that validators have a vested interest in maintaining the integrity and security of the network.

Before Network Launch

Using the same ETH_PRIVATE_KEY that was used to make the deposit, validators may register themselves with the Bootstrap contract during this phase.

# The declaration of ETH_PRIVATE_KEY is written again for completeness but isn't needed if the steps are followed sequentially
ETH_PRIVATE_KEY=0xabcde.....
# must be unique
VALIDATOR_NAME="Dumpling Validator"
## The commissions should be between 0 and 1, both inclusive
# can be changed at most once during Bootstrap, and every 24 hours thereafter
COMMISSION_RATE=0.05
# can never be changed once set
COMMISSION_MAX_RATE=0.7
# can never be changed once set
COMMISSION_MAX_CHANGE_RATE=0.35
# Endpoint for Ethereum RPC
ETHEREUM_RPC_URL=https://rpc.ankr.com/eth_sepolia

# Constants
BOOTSTRAP_ADDR=0x53f39D2ECd900Fb018180dB692A9fc29c8efD38E

# Derived values, depending on $ACCOUNT_KEY_NAME and $HOMEDIR in prior steps
EXO_ADDRESS=$(exocored --home $HOMEDIR keys show -a $ACCOUNT_KEY_NAME)
CONSENSUS_KEY=$(exocored --home $HOMEDIR keys consensus-pubkey-to-bytes --output json | jq -r .bytes32)

# Actual transaction
cast send --rpc-url $ETHEREUM_RPC_URL \
    $BOOTSTRAP_ADDR \
    "registerValidator(string,string,(uint256,uint256,uint256),bytes32)" \
    $EXO_ADDRESS \
    "$VALIDATOR_NAME" \
    "($(cast 2w $COMMISSION),$(cast 2w $COMMISSION_MAX_RATE),$(cast 2w $COMMISSION_MAX_CHANGE_RATE))" \
    $CONSENSUS_KEY \
    --private-key $ETH_PRIVATE_KEY

After Network Launch

The Bootstrap contract is not available after the network launches; instead, validator registration happens on Exocore while the assets continue to live on Ethereum. Since Exocore is a restaking L1 supporting multiple AVSs, all validators must first register as “operators” and then opt-in to the Exocore-chain-as-an-AVS as “validators”.

  1. Contact Exocore contributors and provide your exo1 address to obtain some exo native tokens. These tokens will be used to pay for gas on Exocore.

  2. Register the operator.

exocored --home $HOMEDIR tx operator register-operator \
    --meta-info "ValidatorName" \
    --commission-rate 0.5 \
    --commission-max-rate 0.99 \
    --commission-max-change-rate 0.1 \
    --from $ACCOUNT_KEY_NAME \
    --gas-prices 700000000aexo

To check the operator registration, query the node.

exocored --home $HOMEDIR query operator get-operator-info \
    $(exocored --home $HOMEDIR keys show -a $ACCOUNT_KEY_NAME) \
    # the line below may be eliminated to query localhost:26657 (your node)
    --node $EXOCORE_COS_GRPC_URL \
    --output json | jq
# Expected output
{
  "earnings_addr": "<your_exo1_addr>",
  "approve_addr": "<your_exo1_addr>",
  "operator_meta_info": "<your_operator_name>",
  "client_chain_earnings_addr": {
    "earning_info_list": []
  },
  "commission": {
    "commission_rates": {
      "rate": "<your_commission>",
      "max_rate": "<your_commission_max>",
      "max_change_rate": "<your_commission_max_change>"
    },
    "update_time": "<registration_time>"
  }
}
  1. Opt-in to the AVS to register as a validator.

exocored --home $HOMEDIR tx operator opt-into-avs \
    $(exocored --home $HOMEDIR query avs AVSAddrByChainID $CHAIN_ID --output json | jq -r .avs_address) \
    $(exocored --home $HOMEDIR tendermint show-validator) \
    --from $ACCOUNT_KEY_NAME \
    --gas-prices 700000000aexo

To check if your operator is successfully opted in, query the consensus (validator) key for the chain.

exocored --home $HOMEDIR query operator get-operator-cons-key \
    $(exocored --home $HOMEDIR keys show -a $ACCOUNT_KEY_NAME) \
    $CHAIN_ID \
    # the line below may be eliminated to query localhost:26657 (your node)
    --node $EXOCORE_COS_GRPC_URL \
    --output json | jq
# expected output
{
  "public_key": {
    "ed25519": "<sample bytes key>"
  },
  "opting_out": false
}

Your validator is now registered and ready to accept (self-) delegations. As a reminder, a minimum self-delegation of 1,000 USD in token value is required for inclusion in the validator set.

Delegating Tokens

The deposited tokens can now be delegated to your validator.

## Script constants, repeated here for ease of reference
EXO_ETH_ADDR=0x83E6850591425e3C1E263c054f4466838B9Bd9e4
WST_ETH_ADDR=0xB82381A3fBD3FaFA77B3a7bE693342618240067b
GATEWAY_ADDR=0x8fc4E764C2C3B0646ba572AEa958fB5724706412

## User choices, (some) repeated here
ETH_PRIVATE_KEY=0xabcde.....
ETH_ADDRESS=0x123....
# The token to delegate
TOKEN=$EXO_ETH_ADDR
# Quantity of token to deposit (without the 1e18 multiplier)
# Decimal values are supported
QUANTITY=0.5
# Endpoint for Ethereum RPC
ETHEREUM_RPC_URL=https://rpc.ankr.com/eth_sepolia

# Derived value, depending on $ACCOUNT_KEY_NAME and $HOMEDIR in prior steps
EXO_ADDRESS=$(exocored --home $HOMEDIR keys show -a $ACCOUNT_KEY_NAME)

BOOTSTRAPPED=$(cast call --rpc-url $ETHEREUM_RPC_URL $GATEWAY_ADDR "bootstrapped() returns (bool)")
if [ "$BOOTSTRAPPED" = "true" ]; then
    # Generate the LZ messsage to calculate the fees
    DELEGATE_PREFIX="0x03"
    TOKEN_B32=$(cast 2b $TOKEN)
    ETH_ADDRESS_B32=$(cast 2b $ETH_ADDRESS)
    EXO_ADDRESS_BYTES=$(cast fu $EXO_ADDRESS)
    QUANTITY_B32=$(printf "%064s" $(cast 2h $(cast 2w $QUANTITY) | cut -c3-) | tr ' ' '0')
    LZ_MESSAGE=$(cast ch $DELEGATE_PREFIX $TOKEN_B32 $ETH_ADDRESS_B32 $EXO_ADDRESS_BYTES $QUANTITY_B32)
    VALUE=$(cast call --rpc-url $ETHEREUM_RPC_URL $GATEWAY_ADDR "quote(bytes)" $LZ_MESSAGE)
else
    # No LZ fees, since we are in Bootstrap mode
    VALUE=0
fi

# Execute the transaction
cast send --rpc-url $ETHEREUM_RPC_URL \
    $GATEWAY_ADDR \
    "delegateTo(string,address,uint256)" \
    $EXO_ADDRESS \
    $TOKEN \
    $(cast 2w $QUANTITY) \
    --private-key $ETH_PRIVATE_KEY \
    --value $(cast 2d $VALUE)

If the above transaction was executed after the network bootstrap phase, again, LayerZeroScan may be used to query the status of the message. In addition, the withdrawable amount will change on Exocore once the message is delivered.

ETH_LZ_ID=40161 # Sepolia, where our $GATEWAY_ADDR lives
STAKER_ID=$(echo "${ETH_ADDRESS}" | tr '[:upper:]' '[:lower:]')_$(printf '0x%x\n' "${ETH_LZ_ID}")
# our public endpoint is available below, but you can use your own node too
EXOCORE_COS_GRPC_URL=https://api-cosmos-grpc.exocore-restaking.com:443
exocored query assets QueStakerAssetInfos \
    $STAKER_ID --node $EXOCORE_COS_GRPC_URL \
    --output json | jq
# looks like this
{
  "asset_infos": [
    {
      "asset_id": "0x83e6850591425e3c1e263c054f4466838b9bd9e4_0x9ce1",
      "info": {
        "total_deposit_amount": "5000000000000000000",
        "withdrawable_amount": "0", // value of 0
        "pending_undelegation_amount": "0"
      }
    }
  ]
}

Marking a delegation as self-delegation (Post Bootstrap Phase)

This step only applies if the validator registration was done after the Bootstrap phase ended. Otherwise, it is assumed that the delegations made by the Ethereum address, which sent the validator creation transaction, are self-delegations for the validator.

Send the transaction to associate the two addresses: each such association indicates that the delegations made by the Ethereum address are to be considered as self-delegations for the validator.

To prevent slashing for downtime, it is recommended to send the transaction below only after the node is synced.

# User inputs, the first two of which were previously provided
ETH_PRIVATE_KEY=0xabcdefg...
ETH_ADDRESS=0xabcd...

# Constants
EXO_ETH_RPC_URL=https://api-eth.exocore-restaking.com
EXO_COS_GRPC_URL=https://api-cosmos-grpc.exocore-restaking.com:443
ETH_LZ_ID=40161 # Sepolia

# Address of gateway derived from the params of x/assets
EXO_GATEWAY_ADDR=$(exocored query assets Params --output json --node $EXO_COS_GRPC_URL | jq -r .params.exocore_lz_app_address | cast 2a)

# First, fund the ETH_ADDRESS if it is not be the same as the EXO_ADDRESS
# The validator should have gotten only their EXO_ADDRESS funded by us / faucet
ETH_ADDR_BALANCE_IN_EXO=$(cast balance $ETH_ADDRESS --rpc-url $EXO_ETH_RPC_URL)
ONE_ETHER_IN_WEI=$(cast 2w 1 ether)
if [ "$ETH_ADDR_BALANCE_IN_EXO" -lt "$ONE_ETHER_IN_WEI" ]; then
    # we must execute the transfer, which is easier via cast
    EXO_PRIVATE_KEY=$(exocored --home $HOMEDIR \
        keys unsafe-export-eth-key \
        $ACCOUNT_KEY_NAME)
    cast send --rpc-url $EXO_ETH_RPC_URL \
        $ETH_ADDRESS \
        --value $ONE_ETHER_IN_WEI \
        --private-key $EXO_PRIVATE_KEY
fi

# Send the transaction for the association
cast send --rpc-url $EXO_ETH_RPC_URL \
    $EXO_GATEWAY_ADDR \
    "associateOperatorWithEVMStaker(uint32, string)" \
    $ETH_LZ_ID \
    $(exocored --home $HOMEDIR keys show -a $ACCOUNT_KEY_NAME) \
    --private-key $ETH_PRIVATE_KEY

To check whether the above transaction went through, we can query the association.

ETH_LZ_ID=40161 # Sepolia, where our $GATEWAY_ADDR lives
STAKER_ID=$(echo "${ETH_ADDRESS}" | tr '[:upper:]' '[:lower:]')_$(printf '0x%x\n' "${ETH_LZ_ID}")
# our public endpoint is available below, but you can use your own node too
EXOCORE_COS_GRPC_URL=https://api-cosmos-grpc.exocore-restaking.com:443
exocored query delegation QueryAssociatedOperatorByStaker \
    $STAKER_ID --node $EXOCORE_COS_GRPC_URL \
    --output json | jq
# expected output
{
  "operator": "<your_exo_addr>"
}

The validator should now be considered "eligible" to be part of the active validator set if the self-delegation exceeds 1,000 USD. The inclusion in the set happens in the beginning of the next epoch, which may be at most 1 day from now. Only the top 100 validators (by total stake = self-stake + delegated stake) are included in the validator set.

EPOCH_ID=$(exocored query dogfood params --node $EXOCORE_COS_GRPC_URL --output json | jq -r .params.epoch_identifier)
exocored query epochs epoch-infos --node $EXOCORE_COS_GRPC_URL --output json | jq --arg EPOCH_ID "$EPOCH_ID" '
  . as $root 
  | .epochs[]
  | select(.identifier == $EPOCH_ID)
  | .next_epoch_start_time = (
      (.current_epoch_start_time | fromdateiso8601) + (.duration | sub("s"; "") | tonumber)
    )
  | .time_to_go = (
      (.next_epoch_start_time - ($root.block_time | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601))
    | if . < 0 then 0 else . end
    )
  | {identifier, next_epoch_start_time: (.next_epoch_start_time | todate), time_to_go: (.time_to_go | tostring + " seconds")}
' # do include this line, which contains only a single apostrophe
# it prints how much time is left for the next epoch, like the below
{
  "identifier": "day",
  "next_epoch_start_time": "2024-09-28T10:45:00Z",
  "time_to_go": "25503 seconds"
}

Confirm Election Status

Once the network is running you can check if you’re in the current validator set.

exocored --home $HOMEDIR q tendermint-validator-set | grep $(exocored --home $HOMEDIR tendermint show-address)
# prints your `exovalcons` address, corresponding to the consensus key. example below.
- address: exovalcons18z3p42xn8pjk338upvzp794h02wh7p4t7jj9jx

If you recently registered as a validator, it takes at most 1 day (one epoch) for the validator set to update.

Last updated