- Preliminary terminology
- Overview
- How does one join the network as a validator
- How does an existing validator exit the network
- How does a validator update its stake
- How does a validator update its signer address
- Query commands
- An
epoch
represents the period until a checkpoint is submitted on Ethereum (i.e. oneepoch
ends when a checkpoint is committed on Ethereum and the next one begins).
The staking
module in Heimdall is responsible for a validator's stake related operations. It primarily aids in
- A node joining the protocol as a validator.
- A node leaving the protocol as a validator.
- Updating an existing validator's stake in the network.
- Updating the signer address of an existing validator.
The node that wants to be a validator stakes its tokens by invoking the stakeFor
method on the StakeManager
contract on L1 (Ethereum), which emits a Staked
event:
/// @param signer validator address.
/// @param validatorId unique integer to identify a validator.
/// @param nonce to synchronize the events in heimdall.
/// @param activationEpoch validator's first epoch as proposer.
/// @param amount staking amount.
/// @param total total staking amount.
/// @param signerPubkey public key of the validator
event Staked(
address indexed signer,
uint256 indexed validatorId,
uint256 nonce,
uint256 indexed activationEpoch,
uint256 amount,
uint256 total,
bytes signerPubkey
);
An existing validator on Heimdall catches this event and sends a MsgValidatorJoin
transaction, which is represented by the data structure:
type MsgValidatorJoin struct {
From hmTypes.HeimdallAddress `json:"from"`
ID hmTypes.ValidatorID `json:"id"`
ActivationEpoch uint64 `json:"activationEpoch"`
Amount sdk.Int `json:"amount"`
SignerPubKey hmTypes.PubKey `json:"pub_key"`
TxHash hmTypes.HeimdallHash `json:"tx_hash"`
LogIndex uint64 `json:"log_index"`
BlockNumber uint64 `json:"block_number"`
Nonce uint64 `json:"nonce"`
}
where
From
represents the address of the validator that initiated theMsgValidatorJoin
transaction on heimdall.ID
represents the id of the new validator.ActivationEpoch
is theepoch
at which the new validator will be activated.Amount
is the total staked amount.SignerPubKey
is the signer public key of the new validator.TxHash
is the hash of the staking transaction on L1.LogIndex
is the index of theStaked
log in the staking transaction receipt.BlockNumber
is the L1 block number in which the staking transaction was included.Nonce
is the count representing all the staking related transactions performed from the new validator's account. This is meant to keep Heimdall and L1 in sync.
Upon broadcasting the message, it goes through HandleMsgValidatorJoin
handler which checks the basic sanity of the transaction (verifying the validator doesn't already exist, voting power, etc.).
The SideHandleMsgValidatorJoin
side-handler in all the existing (honest) validators then ensures the authenticity of staking transaction on L1. It fetches the transaction receipt from L1 contract and validates it with the data provided in the MsgValidatorJoin
transaction. Upon successful validation, YES
is voted.
The PostHandleMsgValidatorJoin
post-handler then initializes the new validator and persists in the state via the keeper:
// create new validator
newValidator := hmTypes.Validator{
ID: msg.ID,
StartEpoch: msg.ActivationEpoch,
EndEpoch: 0,
Nonce: msg.Nonce,
VotingPower: votingPower.Int64(),
PubKey: pubkey,
Signer: hmTypes.BytesToHeimdallAddress(signer.Bytes()),
LastUpdated: "",
}
// update last updated
newValidator.LastUpdated = sequence.String()
// add validator to store
k.Logger(ctx).Debug("Adding new validator to state", "validator", newValidator.String())
if err = k.AddValidator(ctx, newValidator); err != nil {
k.Logger(ctx).Error("Unable to add validator to state", "validator", newValidator.String(), "error", err)
return hmCommon.ErrValidatorSave(k.Codespace()).Result()
}
The EndBlocker
hook, which is executed at the end of a heimdall block, then adds the new validator and updates the validator set once activation epoch
is completed:
// --- Start update to new validators
currentValidatorSet := app.StakingKeeper.GetValidatorSet(ctx)
allValidators := app.StakingKeeper.GetAllValidators(ctx)
ackCount := app.CheckpointKeeper.GetACKCount(ctx)
// get validator updates
setUpdates := helper.GetUpdatedValidators(
¤tValidatorSet, // pointer to current validator set -- UpdateValidators will modify it
allValidators, // All validators
ackCount, // ack count
)
if len(setUpdates) > 0 {
// create new validator set
if err := currentValidatorSet.UpdateWithChangeSet(setUpdates); err != nil {
// return with nothing
logger.Error("Unable to update current validator set", "Error", err)
return abci.ResponseEndBlock{}
}
// validator set change
logger.Debug("[ENDBLOCK] Updated current validator set", "proposer", currentValidatorSet.GetProposer())
// save set in store
if err := app.StakingKeeper.UpdateValidatorSetInStore(ctx, currentValidatorSet); err != nil {
// return with nothing
logger.Error("Unable to update current validator set in state", "Error", err)
return abci.ResponseEndBlock{}
}
// more code
}
The bridge
service in an existing validator's heimdall process polls for Staked
event periodically and generates and broadcasts the transaction once it detects and parses the event. An existing validator on the network can also leverage the CLI to send the transaction:
heimdallcli tx staking validator-join --proposer <PROPOSER_ADDRESS> --tx-hash <ETH_TX_HASH> --signer-pubkey <PUB_KEY> --staked-amount <STAKED_AMOUNT> --activation-epoch <ACTIVATION_EPOCH>
Or the REST server :
curl -X POST "localhost:1317/staking/validator-join?from=<PROPOSER_ADDRESS>&tx-hash=<ETH_TX_HASH>&signer-pubkey=<PUB_KEY>&staked-amount=<STAKED_AMOUNT>&activation-epoch=<ACTIVATION_EPOCH>"
If a validator wishes to exit the network and unbond its stake, it invokes the unstake
function on the StakeManager
contract on L1, which emits an UnstakeInit
event:
/// @param user address of the validator.
/// @param validatorId unique integer to identify a validator.
/// @param nonce to synchronize the events in heimdall.
/// @param deactivationEpoch last epoch for validator.
/// @param amount staking amount.
event UnstakeInit(
address indexed user,
uint256 indexed validatorId,
uint256 nonce,
uint256 deactivationEpoch,
uint256 indexed amount
);
An existing validator on Heimdall catches and parses this event and sends a MsgValidatorExit
transaction, which is represented by the data structure:
type MsgValidatorExit struct {
From hmTypes.HeimdallAddress `json:"from"`
ID hmTypes.ValidatorID `json:"id"`
DeactivationEpoch uint64 `json:"deactivationEpoch"`
TxHash hmTypes.HeimdallHash `json:"tx_hash"`
LogIndex uint64 `json:"log_index"`
BlockNumber uint64 `json:"block_number"`
Nonce uint64 `json:"nonce"`
}
where
From
represents the address of the validator that initiated theMsgValidatorExit
transaction on heimdall.ID
represents the id of the validator to be unstaked.DeactivationEpoch
is the lastepoch
as a validator.TxHash
is the hash of the unstake transaction on L1.LogIndex
is the index of theUnstakeInit
log in the unstake transaction receipt.BlockNumber
is the L1 block number in which the unstake transaction was included.Nonce
is the count representing all the staking related transactions performed from the validator's account.
Upon broadcasting the message, it goes through HandleMsgValidatorExit
handler which checks the basic sanity of the data in the transaction.
The SideHandleMsgValidatorExit
side-handler in all the existing (honest) validators then ensures the authenticity of unstake init transaction on L1. It fetches the transaction receipt from L1 contract and validates it with the data provided in the MsgValidatorExit
transaction. Upon successful validation, YES
is voted.
The PostHandleMsgValidatorExit
post-handler then sets the deactivation epoch for the validator and persists in the state via the keeper:
validator, ok := k.GetValidatorFromValID(ctx, msg.ID)
if !ok {
k.Logger(ctx).Error("Fetching of validator from store failed", "validatorID", msg.ID)
return hmCommon.ErrNoValidator(k.Codespace()).Result()
}
// set end epoch
validator.EndEpoch = msg.DeactivationEpoch
// update last updated
validator.LastUpdated = sequence.String()
// update nonce
validator.Nonce = msg.Nonce
// Add deactivation time for validator
if err := k.AddValidator(ctx, validator); err != nil {
k.Logger(ctx).Error("Error while setting deactivation epoch to validator", "error", err, "validatorID", validator.ID.String())
return hmCommon.ErrValidatorNotDeactivated(k.Codespace()).Result()
}
The EndBlocker
hook then updates the validator set once deactivation epoch is completed.
The bridge
service in an existing validator's heimdall process polls for UnstakeInit
event periodically and generates and broadcasts the transaction once it detects and parses the event. An existing validator on the network can also leverage the CLI to send the transaction:
heimdallcli tx staking validator-exit --proposer <PROPOSER_ADDRESS> --id <VALIDATOR_ID> --tx-hash <ETH_TX_HASH> --nonce <VALIDATOR_NONCE> --log-index <LOG_INDEX> --block-number <BLOCK_NUMBER> --deactivation-epoch <DEACTIVATION_EPOCH>
Or the REST server :
curl -X POST "localhost:1317/staking/validator-exit?from=<PROPOSER_ADDRESS>&id=<VALIDATOR_ID>&tx-hash=<ETH_TX_HASH>&nonce=<VALIDATOR_NONCE>&log-index=<LOG_INDEX>&block-number=<BLOCK_NUMBER>&deactivation-epoch=<DEACTIVATION_EPOCH>"
A validator can update its stake in the network by invoking the restake
function on the StakeManager
contract on L1, which emits an StakeUpdate
event:
/// @param validatorId unique integer to identify a validator.
/// @param nonce to synchronize the events in heimdall.
/// @param newAmount the updated stake amount.
event StakeUpdate(
uint256 indexed validatorId,
uint256 indexed nonce,
uint256 indexed newAmount
);
On Heimdall, this event is parsed and a MsgStakeUpdate
transaction is broadcasted, which is represented by the data structure:
type MsgStakeUpdate struct {
From hmTypes.HeimdallAddress `json:"from"`
ID hmTypes.ValidatorID `json:"id"`
NewAmount sdk.Int `json:"amount"`
TxHash hmTypes.HeimdallHash `json:"tx_hash"`
LogIndex uint64 `json:"log_index"`
BlockNumber uint64 `json:"block_number"`
Nonce uint64 `json:"nonce"`
}
where
From
represents the address of the validator that initiated theMsgStakeUpdate
transaction on heimdall.ID
represents the id of the validator whose stake is to be updated.NewAmount
is the new staked amount.TxHash
is the hash of the stake update transaction on L1.LogIndex
is the index of theStakeUpdate
log in the stake update transaction receipt.BlockNumber
is the L1 block number in which the stake update transaction was included.Nonce
is the count representing all the staking related transactions performed from the validator's account.
Upon broadcasting the message, it goes through HandleMsgStakeUpdate
handler which checks the basic sanity of the data in the transaction.
The SideHandleMsgStakeUpdate
side-handler in all the existing (honest) validators then ensures the authenticity of the stake update transaction on L1. It fetches the transaction receipt from L1 contract and validates it with the data provided in the MsgStakeUpdate
transaction. Upon successful validation, YES
is voted.
The PostHandleMsgStakeUpdate
post-handler then derives the new voting power for the validator and persists in the state via the keeper:
// set validator amount
p, err := helper.GetPowerFromAmount(msg.NewAmount.BigInt())
if err != nil {
return hmCommon.ErrInvalidMsg(k.Codespace(), fmt.Sprintf("Invalid amount %v for validator %v", msg.NewAmount, msg.ID)).Result()
}
validator.VotingPower = p.Int64()
// save validator
err = k.AddValidator(ctx, validator)
if err != nil {
k.Logger(ctx).Error("Unable to update signer", "ValidatorID", validator.ID, "error", err)
return hmCommon.ErrSignerUpdateError(k.Codespace()).Result()
}
The EndBlocker
hook then updates the changes in the validator set.
The bridge
service in an existing validator's heimdall process polls for StakeUpdate
event periodically and generates and broadcasts the transaction once it detects and parses the event. An existing validator on the network can also leverage the CLI to send the transaction:
heimdallcli tx staking stake-update --proposer <PROPOSER_ADDRESS> --id <VALIDATOR_ID> --tx-hash <ETH_TX_HASH> --staked-amount <STAKED_AMOUNT> --nonce <VALIDATOR_NONCE> --log-index <LOG_INDEX> --block-number <BLOCK_NUMBER>
Or the REST server :
curl -X POST "localhost:1317/staking/stake-update?proposer=<PROPOSER_ADDRESS>&id=<VALIDATOR_ID>&tx-hash=<ETH_TX_HASH>&staked-amount=<STAKED_AMOUNT>&nonce=<VALIDATOR_NONCE>&log-index=<LOG_INDEX>&block-number=<BLOCK_NUMBER>"
A validator can update its signer address in the network by invoking the updateSigner
function on the StakeManager
contract on L1, which emits an SignerChange
event:
/// @param validatorId unique integer to identify a validator.
/// @param nonce to synchronize the events in heimdall.
/// @param oldSigner old address of the validator.
/// @param newSigner new address of the validator.
/// @param signerPubkey public key of the validator.
event SignerChange(
uint256 indexed validatorId,
uint256 nonce,
address indexed oldSigner,
address indexed newSigner,
bytes signerPubkey
);
On Heimdall, this event is parsed and a MsgSignerUpdate
transaction is broadcasted, which is represented by the data structure:
type MsgSignerUpdate struct {
From hmTypes.HeimdallAddress `json:"from"`
ID hmTypes.ValidatorID `json:"id"`
NewSignerPubKey hmTypes.PubKey `json:"pubKey"`
TxHash hmTypes.HeimdallHash `json:"tx_hash"`
LogIndex uint64 `json:"log_index"`
BlockNumber uint64 `json:"block_number"`
Nonce uint64 `json:"nonce"`
}
where
From
represents the address of the validator that initiated theMsgSignerUpdate
transaction on heimdall.ID
represents the id of the validator whose signer address is to be updated.NewSignerPubKey
is new public key of the validator.TxHash
is the hash of the signer update transaction on L1.LogIndex
is the index of theSignerChange
log in the signer update transaction receipt.BlockNumber
is the L1 block number in which the signer update transaction was included.Nonce
is the count representing all the staking related transactions performed from the validator's account.
Upon broadcasting the message, it goes through HandleMsgSignerUpdate
handler which checks the basic sanity of the data in the transaction.
The SideHandleMsgSignerUpdate
side-handler in all the existing (honest) validators then ensures the authenticity of the signer update transaction on L1. It fetches the transaction receipt from L1 contract and validates it with the data provided in the MsgSignerUpdate
transaction. Upon successful validation, YES
is voted.
The PostHandleMsgSignerUpdate
post-handler then updates the signer address for the validator, "unstakes" the validator instance with the old signer address and persists the changes in the state via the keeper:
oldValidator := validator.Copy()
// more code
...
// check if we are actually updating signer
if !bytes.Equal(newSigner.Bytes(), validator.Signer.Bytes()) {
// Update signer in prev Validator
validator.Signer = hmTypes.HeimdallAddress(newSigner)
validator.PubKey = newPubKey
k.Logger(ctx).Debug("Updating new signer", "newSigner", newSigner.String(), "oldSigner", oldValidator.Signer.String(), "validatorID", msg.ID)
} else {
k.Logger(ctx).Error("No signer change", "newSigner", newSigner.String(), "oldSigner", oldValidator.Signer.String(), "validatorID", msg.ID)
return hmCommon.ErrSignerUpdateError(k.Codespace()).Result()
}
k.Logger(ctx).Debug("Removing old validator", "validator", oldValidator.String())
// remove old validator from HM
oldValidator.EndEpoch = k.moduleCommunicator.GetACKCount(ctx)
// more code
...
// save old validator
if err := k.AddValidator(ctx, *oldValidator); err != nil {
k.Logger(ctx).Error("Unable to update signer", "validatorId", validator.ID, "error", err)
return hmCommon.ErrSignerUpdateError(k.Codespace()).Result()
}
// adding new validator
k.Logger(ctx).Debug("Adding new validator", "validator", validator.String())
// save validator
err := k.AddValidator(ctx, validator)
if err != nil {
k.Logger(ctx).Error("Unable to update signer", "ValidatorID", validator.ID, "error", err)
return hmCommon.ErrSignerUpdateError(k.Codespace()).Result()
}
The EndBlocker
hook then updates the validator set upon completion of the epoch.
The bridge
service in an existing validator's heimdall process polls for SignerChange
event periodically and generates and broadcasts the transaction once it detects and parses the event. An existing validator on the network can also leverage the CLI to send the transaction:
heimdallcli tx staking signer-update --proposer <PROPOSER_ADDRESS> --id <VALIDATOR_ID> --new-pubkey <NEW_PUBKEY> --tx-hash <ETH_TX_HASH> --nonce <VALIDATOR_NONCE> --log-index <LOG_INDEX> --block-number <BLOCK_NUMBER>
Or the REST server :
curl -X POST "localhost:1317/staking/signer-update?proposer=<PROPOSER_ADDRESS>&id=<VALIDATOR_ID>&new-pubkey=<NEW_PUBKEY>&tx-hash=<ETH_TX_HASH>&nonce=<VALIDATOR_NONCE>&log-index=<LOG_INDEX>&block-number=<BLOCK_NUMBER>"
One can run the following query commands from the staking module :
validator-info
- Query validator information via validator id or validator address.current-validator-set
- Query the current validator set.staking-power
- Query the current staking power.validator-status
- Query the validator status by validator address.proposer
- Fetch the first<TIMES>
validators from the validator set, sorted by priority as a checkpoint proposer.current-proposer
- Fetch the validator info selected as proposer of the current checkpoint.is-old-tx
- Check whether the staking transaction is old.
heimdallcli query staking validator-info --id=<VALIDATOR_ID>
OR
heimdallcli query staking validator-info --validator=<VALIDATOR_ADDRESS>
heimdallcli query staking current-validator-set
heimdallcli query staking staking-power
heimdallcli query staking validator-status --validator=<VALIDATOR_ADDRESS>
heimdallcli query staking proposer --times=<TIMES>
heimdallcli query staking current-proposer
heimdallcli query staking is-old-tx --tx-hash=<ETH_TX_HASH> --log-index=<LOG_INDEX>
curl localhost:1317/staking/validator/<VALIDATOR_ID>
OR
curl localhost:1317/staking/validator/<VALIDATOR_ADDRESS>
curl localhost:1317/staking/validator-set
curl localhost:1317/staking/totalpower
curl localhost:1317/staking/validator-status/<VALIDATOR_ADDRESS>
curl localhost:1317/staking/proposer/<TIMES>
curl "localhost:1317/staking/current-proposer
curl localhost:1317/staking/isoldtx?tx-hash=<ETH_TX_HASH>&log-index=<LOG_INDEX>
Some other utility REST URLs:
- To query validator by signer address:
curl "localhost:1317/staking/signer/<SIGNER_ADDRESS>
- To fetch the first
<TIMES>
validators from the validator set, sorted by priority as a milestone proposer:
curl "localhost:1317/staking/milestoneProposer/<TIMES>