Skip to main content

Add a Node to the Validator Set

Introduction

The Primary Network is inherent to the Avalanche platform and validates Avalanche’s built-in blockchains. In this tutorial, we’ll add a node to the Primary Network on Avalanche.

The P-Chain manages metadata on Avalanche. This includes tracking which nodes are in which Subnets, which blockchains exist, and which Subnets are validating which blockchains. To add a validator, we’ll issue transactions to the P-Chain.

warning

Note that once you issue the transaction to add a node as a validator, there is no way to change the parameters. You can’t remove your stake early or change the stake amount, node ID, or reward address. Please make sure you’re using the correct values in the API calls below. If you’re not sure, feel free to join our Discord to ask questions.

Requirements

You've completed Run an Avalanche Node and are familiar with Avalanche's architecture. In this tutorial, we use AvalancheJS and Avalanche’s Postman collection to help us make API calls.

In order to ensure your node is well-connected, make sure that your node can receive and send TCP traffic on the staking port (9651 by default) and your node has a public IP address(it's optional to set --public-ip=[YOUR NODE'S PUBLIC IP HERE] when executing the AvalancheGo binary, as by default, the node will attempt to perform NAT traversal to get the node's IP according to its router). Failing to do either of these may jeopardize your staking reward.

Add a Validator with Core extension

First, we show you how to add your node as a validator by using Core web.

Retrieve the Node ID

Get your node’s ID by calling info.getNodeID:

curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"info.getNodeID"
}' -H 'content-type:application/json' 127.0.0.1:9650/ext/info

The response has your node’s ID:

{
"jsonrpc": "2.0",
"result": {
"nodeID": "NodeID-5mb46qkSBj81k9g9e4VFjGGSbaaSLFRzD"
},
"id": 1
}

Add as a Validator

Connect Core extension to Core web, and go the 'Staking' tab. Here, choose 'Validate' from the menu.

Fill out the staking parameters. They are explained in more detail in this doc. When you’ve filled in all the staking parameters and double-checked them, click Submit Validation. Make sure the staking period is at least 2 weeks, the delegation fee rate is at least 2%, and you’re staking at least 2,000 AVAX on Mainnet (1 AVAX on Fuji Testnet). A full guide about this can be found here.

You should see a success message, and your balance should be updated.

Go back to the Stake tab, and you'll see here an overview of your validation, with information like the amount staked, staking time, and more.

Staking Overview

Calling platform.getPendingValidators verifies that your transaction was accepted. Note that this API call should be made before your node's validation start time, otherwise, the return will not include your node's id as it is no longer pending.

You can also call platform.getCurrentValidators to check that your node's id is included in the response.

That’s it!

Add a Validator with AvalancheJS

We can also add a node to the validator set using AvalancheJS.

Install AvalancheJS

To use AvalancheJS, you can clone the repo:

git clone https://github.com/ava-labs/avalanchejs.git
info

The repository cloning method used is HTTPS, but SSH can be used too:

git clone [email protected]:ava-labs/avalanchejs.git

You can find more about SSH and how to use it here.

or add it to an existing project:

yarn add @avalabs/avalanchejs

For this tutorial we will use ts-node to run the example scripts directly from an AvalancheJS directory.

Fuji Workflow

In this section, we will use Fuji Testnet to show how to add a node to the validator set.

Open your AvalancheJS directory and select the examples/platformvm folder to view the source code for the examples scripts.

We will use the buildAddValidatorTx.ts script to add a validator. To learn more about the buildAddValidatorTx API, please click here.

Private Key

Locate this line in the file

const privKey: string = `${PrivateKeyPrefix}${DefaultLocalGenesisPrivateKey}`;

and replace this with a private key that you control. You can use this code to generate a new key.

const privKey: string = "<YOUR-PRIVATE-KEY-HERE>";

Network Setting

The following settings work when using a local node started with --network-id=fuji:

const ip: string = "localhost";
const port: number = 9650;
const protocol: string = "http";
const networkID: number = 5;

However, to connect directly to the Avalanche Fuji Testnet API server, the following changes are needed:

const ip: string = "api.avax-test.network";
const port: number = 443;
const protocol: string = "https";
const networkID: number = 5;

Depending on the networkID passed in when instantiating an Avalanche object in the code, the encoded addresses used will have a distinctive Human Readable Part(HRP) per network.

Example Address: 5 - X-fuji19rknw8l0grnfunjrzwxlxync6zrlu33yxqzg0h

For Fuji Testnet, 5 is the correct value to use.

Details about Bech32 Addresses

The X-Chain and the P-Chain use Bech32 to encode addresses. Note, the C-Chain also uses Bech32 to encode addresses for importing and exporting assets however the EVM, in general, uses hex encoding for addresses. Ex: 0x46f3e64E4e3f5a46Eaf5c292301c6174B9B646Bf.

Each Bech32 address is composed of the following components

  1. A Human-Readable Part (HRP).
  2. The number 1 is a separator (the last digit 1 seen is considered the separator).
  3. Base-32 encoded string for the data part of the address (the 20-byte address itself).
  4. A 6-character base-32 encoded error correction code using the BCH algorithm.

For example the following Bech32 address, X-avax19rknw8l0grnfunjrzwxlxync6zrlu33y2jxhrg, is composed like so:

  1. HRP: avax
  2. Separator: 1
  3. Address: 9rknw8l0grnfunjrzwxlxync6zrlu33y
  4. Checksum: 2jxhrg

Depending on the networkID, the encoded addresses will have a distinctive HRP per each network.

  • 0 - X-custom19rknw8l0grnfunjrzwxlxync6zrlu33yeg5dya
  • 1 - X-avax19rknw8l0grnfunjrzwxlxync6zrlu33y2jxhrg
  • 2 - X-cascade19rknw8l0grnfunjrzwxlxync6zrlu33ypmtvnh
  • 3 - X-denali19rknw8l0grnfunjrzwxlxync6zrlu33yhc357h
  • 4 - X-everest19rknw8l0grnfunjrzwxlxync6zrlu33yn44wty
  • 5 - X-fuji19rknw8l0grnfunjrzwxlxync6zrlu33yxqzg0h
  • 1337 - X-custom19rknw8l0grnfunjrzwxlxync6zrlu33yeg5dya
  • 12345 - X-local19rknw8l0grnfunjrzwxlxync6zrlu33ynpm3qq

Here's the mapping of networkID to bech32 HRP.

  0: "custom",
1: "avax",
2: "cascade",
3: "denali",
4: "everest",
5: "fuji",
1337: "custom",
12345: "local"

Settings for Validation

Next we need to specify the node's validation period and delegation fee.

const nodeID: string = "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg";
const startTime: BN = UnixNow().add(new BN(60 * 1));
const endTime: BN = startTime.add(new BN(26300000));
const delegationFee: number = 10;

Node ID

This is the node ID of the validator being added. See above section on how to retrieve the node id by using API info.getNodeID.

Staking Period

startTime and endTime are required to specify the time of starting/leaving validation. The minimum duration that one can validate the Primary Network is 2 weeks, and the maximum duration is one year. One can start a new validation on the Primary Network after finishing one, it’s just that the maximum continuous duration is one year. startTime and endTime are the Unix times when your validator will start and stop validating the Primary Network, respectively. startTime must be in the future relative to the time the transaction is issued.

The sample code uses const startTime: BN = UnixNow().add(new BN(60 * 1)) and const endTime: BN = startTime.add(new BN(26300000)) to compute the Unix time 1 minute and 304 days in the future (at the time when this article was written) to use as the values of startTime and endTime, respectively.

tip

You can create your own Unix timestamp here or by using the UnixNow() method

To create your own start times, please follow the steps below:

Locate this line in the file

const startTime: BN = UnixNow().add(new BN(60 * 1));
const endTime: BN = startTime.add(new BN(26300000));

Change startTime and endTime to new BN values, for example:

const startTime: BN = new BN(1654656829); // Wed Jun 08 2022 02:53:49 GMT+0000
const endTime: BN = new BN(1662602029); // Thu Sep 08 2022 01:53:49 GMT+0000

Delegation Fee Rate

Avalanche allows for delegation of stake. This parameter is the percent fee this validator charges when others delegate stake to them. For example, if delegationFeeRate is 10 and someone delegates to this validator, then when the delegation period is over, 10% of the reward goes to the validator and the rest goes to the delegator, if this node meets the validation reward requirements.

Stake Amount

Set the proper staking amount in calling pchain.buildAddValidatorTx by replacing stakeAmount.minValidatorStake with a number in the unit of gwei, for example, BN(1e12) which is 10,000 AVAX.

Addresses

By default, the example uses the variable pAddressStrings to define toAddresses, fromAddresses, changeAddresses and rewardAddresses:

const pAddressStrings: string[] = pchain.keyChain().getAddressStrings();

This retrieves the P-Chain addresses that belong to the private key that appears earlier in the example.

No change is needed in the addresses for the default action. For customization, please refer to this section.

Execute the Code

Now that we have made all of the necessary changes to the example script, it's time to add a validator to the Fuji Network.

Run the command:

ts-node examples/platformvm/buildAddValidatorTx.ts

The response has the transaction ID.

Success! TXID: 2ftDVwmss5eJk8HFsNVi6a3vWK9s3szZFhEeSY2HCS8xDb8Cra

We can check the transaction’s status by running the example script: getTxStatus.ts following the steps below:

  1. Ensure that your network settings are correct before running the script.

  2. Locate this line in the file

const main = async (): Promise<any> => {
const txID: string = "x1NLb9JaHkKTXvSRReVSsFwQ38mY7bfD1Ky1BPv721VhrpuSE"
...
}

and replace it with the buildAddValidator TXID

const main = async (): Promise<any> => {
const txID: string = "2ftDVwmss5eJk8HFsNVi6a3vWK9s3szZFhEeSY2HCS8xDb8Cra"
...
}

Run the command:

ts-node examples/platformvm/getTxStatus.ts

This returns:

{ status: 'Committed' }

The status should be Committed, meaning the transaction was successful.

We can see if the node is now in the pending validator set for the Fuji network by using the example:getPendingValidators.ts. Just change the network settings to meet Fuji requirements and then run the script:

ts-node examples/platformvm/getPendingValidators.ts

The response should include the node we just added:

{
"validators": [
{
"nodeID": "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg",
"startTime": "1654656829",
"endtime": "1662602029",
"stakeAmount": "1000000000"
}
],
"delegators": []
}

When the time reaches 1654656829 (Wed Jun 08 2022 02:53:49 GMT+0000), this node will start validating the Primary Network. When it reaches 1662602029 (Thu Sep 08 2022 01:53:49 GMT+0000), this node will stop validating the Primary Network. The staked AVAX and the rewards, if any, will be returned to pAddressStrings.

Customizing Addresses

There are 4 addresses which are needed when calling pchain.buildAddValidatorTx. Only 2 of them can be changed: toAddresses and rewardAddresses. For backward-compatibility reasons, fromAddresses and changeAddresses are just placeholders and are ignored.

toAddresses

An array of addresses who receive the staked tokens at the end of the staking period.

rewardAddresses

When a validator stops validating the Primary Network, they will receive a reward if they are sufficiently responsive and correct while they validated the Primary Network. These tokens are sent to rewardAddresses. The original stake will be sent back to the addresses defined in toAddresses.

A validator’s stake is never slashed, regardless of their behavior they will always receive their stake back when they’re done validating.

Locate this part of the code

let privKey: string = `${PrivateKeyPrefix}${DefaultLocalGenesisPrivateKey}`;
pKeychain.importKey(privKey);

and replace privKey with private keys that you control. To generate a new keypair, we can use the createKeypair.ts example script along with Fuji Network Settings.

let privKey: string =
"PrivateKey-PY2dvfxzvBAe1a5nn7x23wmZMgAYJaS3XAZXzdUa22JtzUvKM";
pKeychain.importKey(privKey);
privKey = "PrivateKey-2Y3Vg9LShMJyUDBHzQqv5WtKDJ8yAVHyM3H5CNCBBmtg3pQEQG";
pKeychain.importKey(privKey);
privKey = "PrivateKey-NaV16owRSfa5TAtxtoU1BPUoM2y1ohttRbwKJG1j7onE4Ge1s";
pKeychain.importKey(privKey);
priKey = "PrivateKey-26JMUsR5RCkf5k9ME8WxKCWEuCK5s2SrALUn7vEa2urwyDDc91";
pKeychain.importKey(privKey);

const pAddressStrings: string[] = pchain.keyChain().getAddressStrings();

This example would create a keychain with 4 addresses:

  "P-fuji1jx644d9y00y5q4hz8cq4wr75a2erne2y4e32xc", // pAddressStrings[0]
"P-fuji1wchdgdp94j8tszlpsp56qvgkvdn20svpmnm8qk", // pAddressStrings[1]
"P-fuji1f36kkpy6yzd7ayrywxvvprns7qlrcu3hwqdya8", // pAddressStrings[2]
"P-fuji1qw7yt3fp43kuwsufff4vhezs2yl00slr09vmh5", // pAddressStrings[3]

Now we can pass in each address according to its slot in the pAddressStrings array:

const unsignedTx: UnsignedTx = await pchain.buildAddValidatorTx(
utxoSet,
[pAddressStrings[0], pAddressStrings[1]], // toAddresses, one or more addresses
[pAddressStrings[0]], // fromAddresses, required for backward-compatibility
[pAddressStrings[0]], // changeAddresses, required for backward-compatibility
nodeID,
startTime,
endTime,
stakeAmount.minValidatorStake,
[pAddressStrings[2], pAddressStrings[3]], //rewardAddresses, one or more addresses
delegationFee,
locktime,
threshold,
memo,
asOf
);

Mainnet Workflow

The Fuji workflow above can be adapted to Mainnet with the following modifications:

Was this page helpful?