stake

package
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: GPL-3.0 Imports: 28 Imported by: 0

README

Stake module

Table of Contents

Overview

This module manages the validators' related transactions and state for Heimdall.
Validators stake their tokens on the Ethereum chain and send the transactions on Heimdall using the necessary parameters to acknowledge the Ethereum stake change.
Once the majority of the validators agree on the change on the stake, this module saves the validator information on Heimdall state.

Stake Flow.png

Flow

The x/stake module manages validator-related transactions and validator set management for Heimdall v2.
Validators stake their tokens on the Ethereum chain to participate in consensus.
To synchronize these changes with Heimdall, the bridge processor broadcasts the corresponding transaction for an Ethereum-emitted event, choosing from one of the following messages each with the necessary parameters:

  • MsgValidatorJoin: This message is triggered when a new validator joins the system by interacting with StakingManager.sol on Ethereum. The action emits a Staked event to recognize and process the validator’s participation.
  • MsgStakeUpdate: Used to handle stake modifications, this message is sent when a validator re-stakes or receives additional delegation. Both scenarios trigger a StakeUpdate event on Ethereum, ensuring Heimdall accurately updates the validator’s stake information.
  • MsgValidatorExit: When a validator decides to exit, they initiate the process on Ethereum, leading to the emission of a UnstakeInit event. This message ensures that Heimdall records the validator’s departure accordingly.
  • MsgSignerUpdate: This message is responsible for processing changes to a validator’s signer key. When a validator updates their signer key on Ethereum, it emits a SignerUpdate event, prompting Heimdall to reflect the new signer key in its records.
    Each of these transactions in Heimdall v2 follows the same processing mechanisms, leveraging ABCI++ phases.
    During the PreCommit phase, side transaction handlers are triggered, and a vote is injected after validating the Ethereum-emitted event and ensuring its alignment with the data in the processed message.
    Once a majority of validators confirm that the action described in the message has occurred on Ethereum, the x/stake module updates the validator’s state in Heimdall during the FinalizeBlock’s PreBlocker execution.
Replay Prevention Mechanism

Heimdall v2 employs a replay prevention mechanism in the post-tx handler functions to ensure that validator update messages derived from Ethereum events are not processed multiple times.
This mechanism prevents replay attacks by assigning a unique sequence number to each transaction and verifying whether it has already been processed.
The sequence number is constructed using the Ethereum block number and log index, following the formula:

  • sequence = (block number × DefaultLogIndexUnit) + log index where:
  • msg.BlockNumber represents the Ethereum block where the event was emitted.
  • msg.LogIndex is the position of the log entry within that block.
  • DefaultLogIndexUnit ensures uniqueness when combining block numbers and log indexes.
    Before processing a transaction, Heimdall checks its stake keeper to determine if the sequence number has been recorded. If the sequence is found, the transaction is rejected as a duplicate.
    Once the post-tx handler completes successfully, the sequence is stored, ensuring that any future message with the same sequence is recognized and ignored.
    This approach guarantees that Heimdall only processes each valid Ethereum signer update once, preventing unintended state changes due to replayed messages.
Updating the Validator Set

In the x/stake EndBlocker, Heimdall updates the validator set (through the ApplyAndReturnValidatorSetUpdates function), ensuring consensus reflects the latest validator changes.
Before any updates, the current block’s validator set is stored as the previous block’s set. The system retrieves all existing validators, the current validator set, and the acknowledgment count from the x/checkpoint state.
Using GetUpdatedValidators, a list of validators that require updates (setUpdates) is identified and applied through UpdateWithChangeSet, storing the new set under CurrentValidatorSetKey.
To maintain fair block proposer selection, Heimdall implements a proposer priority system, ensuring all validators have a fair chance to propose new blocks.
The proposer priority is dynamically adjusted using IncrementProposerPriority(times int), which prevents any validator from monopolizing block proposals.
This function limits priority differences by re-scaling priorities (RescalePriorities(diffMax)) and shifting values based on the average proposer priority (shiftByAvgProposerPriority()).
During each round, the validator with the highest priority is selected as the proposer, after which their priority is adjusted to prevent indefinite accumulation.
These mechanisms collectively ensure efficient and fair validator rotation, maintaining a balanced consensus process while preventing priority overflows and unfair selection biases.

Stake ABCI_Diagram.png

Messages

MsgValidatorJoin

MsgValidatorJoin defines a message for a node to join the network as validator.

Here is the structure for the transaction message:

//  MsgValidatorJoin defines a message for a new validator to join the network
message MsgValidatorJoin {
  option (cosmos.msg.v1.signer) = "from";
  option (amino.name) = "heimdallv2/stake/MsgValidatorJoin";
  option (gogoproto.equal) = false;
  option (gogoproto.goproto_getters) = true;
  string from = 1 [
    (amino.dont_omitempty) = true,
    (cosmos_proto.scalar) = "cosmos.AddressString"
  ];
  uint64 val_id = 2 [ (amino.dont_omitempty) = true ];
  uint64 activation_epoch = 3 [ (amino.dont_omitempty) = true ];
  string amount = 4 [
    (gogoproto.nullable) = false,
    (gogoproto.customtype) = "cosmossdk.io/math.Int",
    (amino.dont_omitempty) = true
  ];
  bytes signer_pub_key = 5 [ (amino.dont_omitempty) = true ];
  bytes tx_hash = 6 [ (amino.dont_omitempty) = true ];
  uint64 log_index = 7 [ (amino.dont_omitempty) = true ];
  uint64 block_number = 8 [ (amino.dont_omitempty) = true ];
  uint64 nonce = 9 [ (amino.dont_omitempty) = true ];
}
MsgStakeUpdate

MsgStakeUpdate defines a message for a validator to perform a stake update on Ethereum network.

message MsgStakeUpdate {
  option (cosmos.msg.v1.signer) = "from";
  option (amino.name) = "heimdallv2/stake/MsgStakeUpdate";
  option (gogoproto.equal) = false;
  option (gogoproto.goproto_getters) = true;
  string from = 1 [
    (amino.dont_omitempty) = true,
    (cosmos_proto.scalar) = "cosmos.AddressString"
  ];
  uint64 val_id = 2 [ (amino.dont_omitempty) = true ];
  string new_amount = 3 [
    (gogoproto.nullable) = false,
    (amino.dont_omitempty) = true,
    (gogoproto.customtype) = "cosmossdk.io/math.Int"
  ];
  bytes tx_hash = 4 [ (amino.dont_omitempty) = true ];
  uint64 log_index = 5 [ (amino.dont_omitempty) = true ];
  uint64 block_number = 6 [ (amino.dont_omitempty) = true ];
  uint64 nonce = 7 [ (amino.dont_omitempty) = true ];
}
MsgSignerUpdate

MsgSignerUpdate defines a message for updating the signer of the existing validator.

message MsgSignerUpdate {
  option (cosmos.msg.v1.signer) = "from";
  option (amino.name) = "heimdallv2/stake/MsgSignerUpdate";
  option (gogoproto.equal) = false;
  option (gogoproto.goproto_getters) = true;
  string from = 1 [
    (amino.dont_omitempty) = true,
    (cosmos_proto.scalar) = "cosmos.AddressString"
  ];
  uint64 val_id = 2 [ (amino.dont_omitempty) = true ];
  bytes new_signer_pub_key = 3 [ (amino.dont_omitempty) = true ];
  bytes tx_hash = 4 [ (amino.dont_omitempty) = true ];
  uint64 log_index = 5 [ (amino.dont_omitempty) = true ];
  uint64 block_number = 6 [ (amino.dont_omitempty) = true ];
  uint64 nonce = 7 [ (amino.dont_omitempty) = true ];
}
MsgValidatorExit

MsgValidatorExit defines a message for a validator to exit the network.

message MsgValidatorExit {
  option (cosmos.msg.v1.signer) = "from";
  option (amino.name) = "heimdallv2/stake/MsgValidatorExit";
  option (gogoproto.equal) = false;
  option (gogoproto.goproto_getters) = true;
  string from = 1 [
    (amino.dont_omitempty) = true,
    (cosmos_proto.scalar) = "cosmos.AddressString"
  ];
  uint64 val_id = 2 [ (amino.dont_omitempty) = true ];
  uint64 deactivation_epoch = 3 [ (amino.dont_omitempty) = true ];
  bytes tx_hash = 4 [ (amino.dont_omitempty) = true ];
  uint64 log_index = 5 [ (amino.dont_omitempty) = true ];
  uint64 block_number = 6 [ (amino.dont_omitempty) = true ];
  uint64 nonce = 7 [ (amino.dont_omitempty) = true ];
}

Interact with the Node

Tx Commands
Validator Join
heimdalld tx stake validator-join --proposer {proposer address} --signer-pubkey {signer pubkey with 04 prefix} --tx-hash {tx hash} --block-number {L1 block number} --staked-amount {total stake amount} --activation-epoch {activation epoch} --home="{path to home}"
Signer Update
heimdalld tx stake signer-update --proposer {proposer address} --id {val id} --new-pubkey {new pubkey with 04 prefix} --tx-hash {tx hash}  --log-index {log index} --block-number {L1 block number} --nonce {nonce} --home="{path to home}"
Stake Update
heimdalld tx stake stake-update [valAddress] [valId] [amount] [txHash] [logIndex] [blockNumber] [nonce]
Validator Exit
heimdalld tx stake validator-exit [valAddress] [valId] [deactivationEpoch] [txHash] [logIndex] [blockNumber] [nonce]
Recovering Missed L1 Events

Under normal operation the bridge listener picks up L1 staking events and broadcasts the corresponding messages to Heimdall automatically. In rare cases the listener can miss an event, leaving Heimdall with no record of it. Typical causes:

  • L1 RPC outage or rate limit during the relevant block range
  • Bridge listener restart with a stale block cursor that skipped past the event
  • Exhausted RabbitMQ task retries
Detection

A missed event leaves no Heimdall transaction. Confirm with:

heimdalld query stake is-old-tx <L1_TX_HASH> <LOG_INDEX>

If the L1 event exists on chain but the query returns is_old=false, the bridge missed it.

Auto-recovery (when self-heal is enabled)

With enable_self_heal = "true" and sub_graph_url set in app.toml, missed StakeUpdate, SignerChange, and UnstakeInit L1 events are replayed automatically by the self-heal loop. (These map to the Heimdall messages MsgStakeUpdate, MsgSignerUpdate, and MsgValidatorExit respectively.) Wait at least one sh_stake_update_interval cycle (default 3h) before falling back to manual recovery. ValidatorJoin is not covered by self-heal; recovery for missed joins is always manual.

Manual recovery

Use the tx commands documented above. Only validator-join decodes the L1 receipt internally — for the other three commands the operator must extract the per-field values from the L1 event before invoking the CLI.

The --proposer (or first positional arg) is just the Cosmos tx signer; any funded Heimdall account works. Side handlers authenticate the underlying L1 event via tx_hash + log_index, not the wrapper account, so the recovery tx does not need to come from the affected validator's own account.

validator-join — receipt-decoding shortcut

Operator only needs the L1 transaction hash, the validator's signer pubkey (with 04 prefix), the staked amount, and the activation epoch. The CLI fetches the receipt, finds the Staked event log, and reads validatorId, nonce, and logIndex from it.

heimdalld tx stake validator-join \
  --proposer <SIGNER> \
  --signer-pubkey <PUBKEY_WITH_04_PREFIX> \
  --tx-hash <L1_HASH> \
  --staked-amount <AMOUNT> \
  --activation-epoch <EPOCH> \
  --block-number <L1_BLOCK_NUMBER> \
  --home "<HEIMDALL_HOME>"
signer-update — full field set required

Operator must extract every field from the L1 SignerChange event: validatorId, the new signer pubkey (with 04 prefix), txHash, logIndex, blockNumber, and nonce. The CLI does not decode the receipt for this command.

heimdalld tx stake signer-update \
  --proposer <SIGNER> \
  --id <VAL_ID> \
  --new-pubkey <NEW_PUBKEY_WITH_04_PREFIX> \
  --tx-hash <L1_HASH> \
  --log-index <LOG_INDEX> \
  --block-number <L1_BLOCK_NUMBER> \
  --nonce <NONCE> \
  --home "<HEIMDALL_HOME>"
stake-update — full field set required (positional args)

Operator must extract valId, amount, txHash, logIndex, blockNumber, and nonce from the L1 StakeUpdate event.

heimdalld tx stake stake-update <PROPOSER> <VAL_ID> <AMOUNT> <L1_HASH> <LOG_INDEX> <L1_BLOCK_NUMBER> <NONCE>
validator-exit — full field set required (positional args)

Operator must extract valId, deactivationEpoch, txHash, logIndex, blockNumber, and nonce from the L1 UnstakeInit event.

heimdalld tx stake validator-exit <PROPOSER> <VAL_ID> <DEACTIVATION_EPOCH> <L1_HASH> <LOG_INDEX> <L1_BLOCK_NUMBER> <NONCE>
Extracting fields from an L1 event

Use any L1 RPC client (cast, ethers, web3) to fetch the receipt and decode the relevant log via the StakingInfo ABI. Example with cast:

cast receipt <L1_HASH> --json | jq '.logs[]'
# match the log by topic[0] (event signature) and topic[1] (validatorId, padded)
# decode `data` per the event's non-indexed field layout
CLI Query Commands

One can run the following query commands from the stake module:

  • current-validator-set - Query all validators that are currently active in the validators' set
  • signer - Query validator info for given validator address
  • validator - Query validator info for a given validator id
  • validator-status - Query validator status for given validator address
  • total-power - Query total power of the validator set
  • is-old-tx - Check if a tx is old (already submitted)
heimdalld query stake current-validator-set
heimdalld query stake signer [val_address]
heimdalld query stake validator [id]
heimdalld query stake validator-status [val_address]
heimdalld query stake total-power
heimdalld query stake is-old-tx [txHash] [logIndex]
GRPC Endpoints

The endpoints and the params are defined in the stake/query.proto file. Please refer to them for more information about the optional params.

grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.stake.Query/GetCurrentValidatorSet
grpcurl -plaintext -d '{"val_address": <>}' localhost:9090 heimdallv2.stake.Query/GetSignerByAddress
grpcurl -plaintext -d '{"id": <>}' localhost:9090 heimdallv2.stake.Query/GetValidatorById
grpcurl -plaintext -d '{"val_address": <>}' localhost:9090 heimdallv2.stake.Query/GetValidatorStatusByAddress
grpcurl -plaintext -d '{}' localhost:9090 heimdallv2.stake.Query/GetTotalPower
grpcurl -plaintext -d '{"tx_hash": <>, "log_index": <>}' localhost:9090 heimdallv2.stake.Query/IsStakeTxOld
grpcurl -plaintest -d '{"times": <>}' localhost:9090 heimdallv2.stake.Query/GetProposersByTimes

REST APIs

The endpoints and the params are defined in the stake/query.proto file. Please refer to them for more information about the optional params.

curl localhost:1317/stake/validators-set
curl localhost:1317/stake/signer/{val_address}
curl localhost:1317/stake/validator/{id}
curl localhost:1317/stake/validator-status/{val_address}
curl localhost:1317/stake/total-power
curl localhost:1317/stake/is-old-tx?tx_hash=<tx-hash>&log_index=<log-index>
# Current "checkpoint proposer" from x/stake's proposer rotation (used by x/checkpoint).
# Distinct from the Bor sprint proposer returned by bor_getCurrentProposer on the EL.
curl localhost:1317/stake/proposers/current
# Next N proposers in the rotation sequence.
curl localhost:1317/stake/proposers/{times}

Documentation

Index

Constants

View Source
const ConsensusVersion = 1

ConsensusVersion defines the current x/stake module consensus version.

Variables

This section is empty.

Functions

func WriteValidators

func WriteValidators(ctx sdk.Context, keeper *keeper.Keeper) (vals []cmttypes.GenesisValidator, returnErr error)

WriteValidators returns a slice of comet genesis validators.

Types

type AppModule

type AppModule struct {
	// contains filtered or unexported fields
}

AppModule implements an application module for the stake module.

func NewAppModule

func NewAppModule(keeper *keeper.Keeper) AppModule

NewAppModule creates a new AppModule object

func (AppModule) AutoCLIOptions

func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions

AutoCLIOptions returns the auto cli options for the module (query and tx)

func (AppModule) ConsensusVersion

func (AppModule) ConsensusVersion() uint64

ConsensusVersion implements AppModule/ConsensusVersion.

func (AppModule) DefaultGenesis

func (AppModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage

DefaultGenesis returns default genesis state as raw bytes for the stake module.

func (AppModule) EndBlock

func (am AppModule) EndBlock(ctx context.Context) ([]abci.ValidatorUpdate, error)

EndBlock returns the end blocker for the stake module. It returns validator updates.

func (AppModule) ExportGenesis

func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage

ExportGenesis returns the exported genesis state as raw bytes for the stake module.

func (AppModule) GenerateGenesisState

func (am AppModule) GenerateGenesisState(simState *module.SimulationState)

func (AppModule) GetTxCmd

func (am AppModule) GetTxCmd() *cobra.Command

GetTxCmd returns the root tx command for the bor module.

func (AppModule) InitGenesis

func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate

InitGenesis performs genesis initialization for the stake module.

func (AppModule) IsAppModule

func (am AppModule) IsAppModule()

IsAppModule implements the appmodule.AppModule interface.

func (AppModule) IsOnePerModuleType

func (am AppModule) IsOnePerModuleType()

IsOnePerModuleType implements the depinject.OnePerModuleType interface.

func (AppModule) Name

func (AppModule) Name() string

Name returns the stake module's name.

func (AppModule) QuerierRoute

func (AppModule) QuerierRoute() string

QuerierRoute returns the stake module's querier route name.

func (AppModule) RegisterGRPCGatewayRoutes

func (AppModule) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *gwruntime.ServeMux)

RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the stake module.

func (AppModule) RegisterInterfaces

func (AppModule) RegisterInterfaces(registry cdctypes.InterfaceRegistry)

RegisterInterfaces registers the module's interface types

func (AppModule) RegisterLegacyAminoCodec

func (AppModule) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino)

RegisterLegacyAminoCodec registers the stake module's types on the given LegacyAmino codec.

func (AppModule) RegisterServices

func (am AppModule) RegisterServices(cfg module.Configurator)

RegisterServices registers module services.

func (AppModule) RegisterSideMsgServices

func (am AppModule) RegisterSideMsgServices(sideCfg sidetxs.SideTxConfigurator)

RegisterSideMsgServices registers side handler module services.

func (AppModule) RegisterStoreDecoder

func (am AppModule) RegisterStoreDecoder(_ simulation.StoreDecoderRegistry)

func (AppModule) ValidateGenesis

func (AppModule) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error

ValidateGenesis performs genesis state validation for the stake module.

func (AppModule) WeightedOperations

func (am AppModule) WeightedOperations(_ module.SimulationState) []simulation.WeightedOperation

type AppModuleBasic

type AppModuleBasic struct{}

AppModuleBasic defines the basic application module used by the stake module.

Directories

Path Synopsis
client
cli
Package testutil is a generated GoMock package.
Package testutil is a generated GoMock package.
Package types is a reverse proxy.
Package types is a reverse proxy.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL