logoAcademy

Registering Validators

Learn how validators are registered on the Avalanche network post-etna.

How does it all work? I mean actually work.

Sending Warp Messages

On the EVM, a warp message is simply an event log with messageID = sha256(messagePayload) Nothing is ever actually “sent” anywhere, which trips people up when first learning how Warp Messaging works.

The log event emitted is from the Warp precompile address (0x0200000000000000000000000000000000000005) The topic of the message will be 0x56600c567728a800c0aa927500f831cb451df66a7af570eb4df4dfbf4674887d which is the output of

cast keccak "SendWarpMessage(address,bytes32,bytes)"

To find a specific warp messageID, one must know the block number or block hash to retrieve the logs, or be monitoring all events, looking for and indexing messages.

When a block is accepted by the network, a precompile hook is called, and the EVM saves the warp message to a local database. The validator node will then be willing to sign any messages in that database upon request, typically via an AppRequest message over the p2p network.

Receiving Warp Messages

On the EVM, any smart contract can call the precompile

which returns the warp message and verifies the attached signature. But what is index? Where does it get the warp message from? The answer is, you! When you send a tx to any contract that eventually calls getVerifiedWarpMessage, you must pass in the signed warp message as a “predicate”. It’s called an AccessList, and typically this is an optional field that can be used by the EVM to pre-fetch storage slots that will be accessed, for efficiency and reducing gas costs. But Warp cleverly (ab)uses this feature of the EVM to verify the signature of the warp message, and then make the warp message available during the transactions lifetime, by calling getVerifiedWarpMessage. You can pass multiple warp messages which is where the index comes in. (All of this is abstracted away by Teleporter for EVM to EVM comms)

Details: Warp README

Teleporter

Teleporter is a set of smart contracts that implements a rich messaging protocol on top of the Warp Messaging primitive, allowing for verified message passing between EVM chains.

Only (currently) supports EVM→EVM comms. Not used for EVM→P-Chain

Relayer

Relayer is an off-chain program that listens for warp messages on a source chain, and delivers them to a destination chain. Messages can choose to be relayed by anyone, or by only privileged relayers. Messages can also pay fees to incentivize relayers and cover gas costs.

Registering ValidatorManager

ACP-77 introduces L1-only Validators, that stake no AVAX, and do not validate the X/C chains. They must connect to the Primary Network and track the P-Chain, and also validate their own chain, and pay a small continuous fee in AVAX to the network.

User submits EVM tx to smart contract NativeTokenStakingManager.sol

function initializeValidatorRegistration(
        ValidatorRegistrationInput calldata registrationInput,
        uint16 delegationFeeBips,
        uint64 minStakeDuration) payable returns (bytes32 validationID)

Notably, this function will:

  • Lock the native tokens in the contract
  • validationID: bytes32 hash of an encoded RegisterSubnetValidatorMessage
  • messageID: bytes32 hash of the warp message
  • Store some data in the contract:
$._pendingRegisterValidationMessages[validationID] = registerSubnetValidatorMessage;
$._registeredValidators[input.nodeID] = validationID;
bytes32 messageID = WARP_MESSENGER.sendWarpMessage(registerSubnetValidatorMessage);
$._validationPeriods[validationID] = Validator({
  status: ValidatorStatus.PendingAdded,
  nodeID: input.nodeID,
  startingWeight: weight,
  messageNonce: 0,
  weight: weight,
  startedAt: 0, // The validation period only starts once the registration is acknowledged.
  endedAt: 0
});
  • Emit ValidationPeriodCreated(validationID, nodeID, messageID, weight, registrationExpiry)
  • The sendWarpMessage precompile will emit a log with sender, messageID, message

At this point, we have submitted a transaction on the blockchain (C-Chain or L1, wherever the staking contract is deployed). All that has happened is we have stored some state in our contract and emitted some events. No changes to validators has occurred.

Nothing else will happen UNLESS an off-chain actor continues the process.

In our case, someone must be running a relayer that is configured with our staking contract as the source and the P-Chain as the destination.

At the moment, the relayer does not support the P-Chain, so messages must be signed and sent manually. The relayer will be updated soon to support the P-Chain.

So, to sign and send the message ourselves, we can use the Signature Aggregator server in the relayer project. This will use the p2p layer of the Avalanche network to connect to validators and send AppRequest messages to request each validator to sign a message.

curl --location 'http://localhost:8080/aggregate-signatures' \
--json '{"message": "<hex encoded unsigned message bytes retrieved from the logs>"}'

This will give us an aggregated BLS signature from at least 67% of the stake weighted validator set. Now we need to POST this to the P-Chain via the RegisterSubnetValidatorTx tx.

The P-Chain, if it accepts the tx, will “send” a Warp Message SubnetValidatorRegistration which acknowledges the change. Note that nothing is actually “sent”, instead the validator node will be willing to sign the unsigned message upon request.

So the next step is to use the Signature Aggregator service again, and provide the unsigned SubnetValidatorRegistration warp message, which the service will then ask validators to sign.

If your Validator Manager contract is on your L1 (instead of C-Chain), as an optimization, you only need to ask 67% of the stake weighted validators of your L1, not the entire P-Chain validator set.

Once an aggregate BLS signature is created, we need to submit the signed message to the C-Chain (or L1) by calling a function on the ValidatorManager contract.

function completeValidatorRegistration(uint32 messageIndex)

Remember, this involves calling the function via eth_sendTransaction and encoding the signed warp message into the AccessList. (If you encode an array of 1 warp message, then messageIndex would be 0)

The function will adjust the state in the contract, including these bits:

delete $._pendingRegisterValidationMessages[validationID];
$._validationPeriods[validationID].status = ValidatorStatus.Active;
$._validationPeriods[validationID].startedAt = uint64(block.timestamp);

And that’s it!

This doc is a WIP and we will eventually cover all the flows and add some diagrams.

On this page