Babylon implements epoched staking to reduce and parameterize the frequency of updating the validator set in Babylon. This reduces the frequency of Babylon sending checkpoints to Bitcoin, thereby reducing Babylon's operation cost and its footprint on Bitcoin.
In the epoched staking design, the blockchain is divided into epochs, each of which contains a fixed number of consecutive blocks. Messages that affect the validator set's stake distribution are delayed to the end of each epoch for execution, such that the validator set remains unchanged during each epoch. The Epoching module is responsible for implementing the epoched staking design, including:
- tracking the current epoch number of the blockchain;
- recording the metadata of each epoch;
- delaying the execution of messages that affect the validator set's stake distribution to the end of each epoch; and
- finishing all unbonding requests for epochs that have a Bitcoin checkpoint with sufficient confirmations.
- Table of contents
- Concepts
- States
- Messages
- BeginBlocker and EndBlocker
- BeginBlocker
- EndBlocker
- Hooks
- Events
- Queries
In the Cosmos SDK, the validator set can change with every block, impacting stake distribution through various staking-related actions (e.g., bond/unbond, delegate/undelegate/redelegate, slash). This frequent updating poses challenges, as
- Babylon's Bitcoin Timestamping protocol requires checkpointing the validator set to Bitcoin upon every validator set update;
- Bitcoin's 10-minute block interval makes checkpointing every new block impractical; and
- frequent validator set updates complicate the implementation of threshold cryptography, light clients, fair leader election, and staking derivatives, as highlighted in ADR-039.
We introduce the concept of epoching to reduce the frequency of validator set update, thereby addressing the above challenges. In the epoching design, the blockchain is divided into epochs, and updating the validator set once per epoch. This approach, detailed in ADR-039, has been pursued by multiple efforts (e.g., here, here, and here) but was not fully implemented. Babylon has implemented the Epoching module, catering to specific design goals such as checkpointing epochs. In addition, Babylon implements Bitcoin-assisted unbonding, where unbonding requests in an epoch will be finished when the epoch they were created in is checkpointed on Bitcoin with sufficient confirmations.
Babylon implements the Epoching module in order to reduce the frequency of validator set updates, and thus the frequency of checkpointing to Bitcoin. Specifically, the Epoching module is responsible for the following tasks:
- Dividing the blockchain into epochs.
- Disabling some functionalities of the Staking module.
- Disabling messages of the Staking module.
- Delaying staking-related messages till the end of the epoch.
- Bitcoin-assisted unbonding.
Dividing the blockchain into epochs. The epoching mechanism introduces the concept of epochs. The blockchain is divided into epochs, each consisting of a fixed number of consecutive blocks. The number of blocks in an epoch is called epoch interval, which is a system parameter.
Disabling functionalities of the Staking module. Babylon disables two functionalities of the Staking module, namely the validator set update mechanism and the 21-day unbonding mechanism.
In Cosmos SDK, the Staking module handles staking-related messages and updates the validator set upon every block. Consequently, the Staking module updates the validator set upon every block. In order to reduce the frequency of validator set updates to once per epoch, Babylon disables the validator set update mechanism of the Staking module.
In addition, the Staking module enforces the "21-day unbonding rule": unbonding validators and delegations will become unbonded after 21 days (in the default case). The long unbonding period aims to circumvent the long-range attack, at the cost of capital efficiency. Babylon departs from Cosmos SDK by employing Bitcoin-assisted unbonding, where unbonding validators and delegations become unbonded once the corresponding epoch has been checkpointed on Bitcoin. Babylon disables the 21-day unbonding mechanism to this end.
In order to disable the two functionalities, Babylon disables Staking module's
EndBlocker
function that updates validator sets and unbonds mature validators
upon a block ends. Instead, upon an epoch that has ended, the Epoching module
will invoke the Staking module's functionality that updates the validator set.
In addition, upon an epoch that has been checkpointed to Bitcoin, the Epoching
module will invoke the Staking module's functionality that unbonds mature
validators.
Disabling messages of the Staking module. In order to keep the validator set unchanged during each epoch, the Epoching module intercepts and rejects staking-related messages that affect the validator set's stake distribution via the AnteHandler, Instead, the Epoching module defines wrapped versions of them and forwards their unwrapped forms to the Staking module upon the end of an epoch. In the Staking module, these messages include
MsgCreateValidator
for creating a new validatorMsgDelegate
for delegating coins from a delegator to a validatorMsgBeginRedelegate
for redelegating coins from a delegator and source validator to a destination validator.MsgUndelegate
for undelegating from a delegator and a validator.MsgCancelUnbondingDelegation
for cancelling an unbonding delegation of a delegator.
The above messages affect the validator set's stake distribution. The Epoching
module implements an AnteHandler
to reject these messages, while also
implementing wrapped versions of them together with the Checkpointing module:
MsgWrappedCreateValidator
, MsgWrappedDelegate
, MsgWrappedBeginRedelegate
,
and MsgWrappedUndelegate
. The Epoching module receives these messages at any
time, but will only process them at the end of each epoch.
Delaying wrapped messages to the end of epochs. The Epoching module maintains a message queue for each epoch. Upon each wrapped message, the Epoching module performs basic sanity checks, then enqueues the message to the message queue. When the epoch ends, the Epoching module will forward queued messages to the Staking module. Consequently, the Staking module receives and handles staking-related messages, and performs validator set updates.
Bitcoin-assisted Unbonding. Babylon implements the Bitcoin-assisted
unbonding mechanism by invoking the Staking module upon a checkpointed epoch .
Specifically, the Staking module's BlockValidatorUpdates
function
is responsible for identifying and unbonding mature validators and delegations
that have been unbonding for 21 days, and is invoked upon every block. Babylon
has disabled the invocation of BlockValidatorUpdates
per block, and implements
the state management for epochs. When an epoch has a checkpoint that is
sufficiently deep in Bitcoin, the Epoching module will invoke
BlockValidatorUpdates
to finish all unbonding validators and delegations.
The Epoching module maintains the following KV stores.
The parameter storage maintains the Epoching module's
parameters. The Epoching module's parameters are represented as a Params
object defined as follows:
// Params defines the parameters for the module.
message Params {
option (gogoproto.equal) = true;
// epoch_interval is the number of consecutive blocks to form an epoch
uint64 epoch_interval = 1
[ (gogoproto.moretags) = "yaml:\"epoch_interval\"" ];
}
The epoch storage maintains the metadata of each epoch.
The key is the epoch number, and the value is an Epoch
object representing the epoch
metadata.
// Epoch is a structure that contains the metadata of an epoch
message Epoch {
// epoch_number is the number of this epoch
uint64 epoch_number = 1;
// current_epoch_interval is the epoch interval at the time of this epoch
uint64 current_epoch_interval = 2;
// first_block_height is the height of the first block in this epoch
uint64 first_block_height = 3;
// last_block_time is the time of the last block in this epoch.
// Babylon needs to remember the last header's time of each epoch to complete
// unbonding validators/delegations when a previous epoch's checkpoint is
// finalised. The last_block_time field is nil in the epoch's beginning, and
// is set upon the end of this epoch.
google.protobuf.Timestamp last_block_time = 4 [ (gogoproto.stdtime) = true ];
// app_hash_root is the Merkle root of all AppHashs in this epoch
// It will be used for proving a block is in an epoch
bytes app_hash_root = 5;
// sealer is the last block of the sealed epoch
// sealer_app_hash points to the sealer but stored in the 1st header
// of the next epoch
bytes sealer_app_hash = 6;
// sealer_block_hash is the hash of the sealer
// the validator set has generated a BLS multisig on the hash,
// i.e., hash of the last block in the epoch
bytes sealer_block_hash = 7;
}
The Epoching module implements a message queue to delay the execution of
messages that affect the validator set's stake distribution to the end of each
epoch. This ensures that during an epoch, the validator set's stake distribution
will remain unchanged, except for slashed validators. The epoch message queue
storage maintains the queue of these
staking-related messages. The key is the epoch number concatenated with the
index of the queued message, and the value is a QueuedMessage
object representing this
queued message.
// QueuedMessage is a message that can change the validator set and is delayed
// to the end of an epoch
message QueuedMessage {
// tx_id is the ID of the tx that contains the message
bytes tx_id = 1;
// msg_id is the original message ID, i.e., hash of the marshaled message
bytes msg_id = 2;
// block_height is the height when this msg is submitted to Babylon
uint64 block_height = 3;
// block_time is the timestamp when this msg is submitted to Babylon
google.protobuf.Timestamp block_time = 4 [ (gogoproto.stdtime) = true ];
// msg is the actual message that is sent by a user and is queued by the
// Epoching module
oneof msg {
cosmos.staking.v1beta1.MsgCreateValidator msg_create_validator = 5;
cosmos.staking.v1beta1.MsgDelegate msg_delegate = 6;
cosmos.staking.v1beta1.MsgUndelegate msg_undelegate = 7;
cosmos.staking.v1beta1.MsgBeginRedelegate msg_begin_redelegate = 8;
cosmos.staking.v1beta1.MsgCancelUnbondingDelegation msg_cancel_unbonding_delegation = 9;
}
}
In the Cosmos SDK, the MsgCreateValidator
, MsgDelegate
, MsgUndelegate
,
MsgBeginRedelegate
, MsgCancelUnbondingDelegation
messages of the Staking
module might affect the validator set, and are thus wrapped into QueuedMessage
objects. Their execution is delayed to the end of an epoch for execution.
The epoch validator set storage maintains the
validator set at the beginning of each epoch. The validator set will remain the
same throughout the epoch, unless some validators get slashed during this epoch.
The key is the epoch number concatenated with the validator's address, and the
value is this validator's voting power (in sdk.Int
) at this epoch.
The Epoching module implements the epoched staking mechanism by using an AnteHandler to intercept messages that affect the validator set's stake distribution, and implements the messages' epoched counterparts.
In Cosmos SDK, the AnteHandler is a component responsible for pre-processing transactions. It functions prior to the execution of transaction logic, performing crucial tasks such as validating signatures, ensuring sufficient account funds for transaction fees, and setting up the necessary context for transaction processing. This offers flexibility in the sense that developers can customize AnteHandlers to suit the specific needs and rules of their applications.
The Epoching module implements an
AnteHandler
DropValidatorMsgDecorator
in order to intercept messages that affect the
validator set's stake distribution in Cosmos SDK's Staking module. The messages
include MsgCreateValidator
, MsgDelegate
, MsgUndelegate
,
MsgBeginRedelegate
, MsgCancelUnbondingDelegation
.
The epoched staking messages in the Epoching module are defined at proto/babylon/epoching/v1/tx.proto. They are simply wrappers of the corresponding messages in Cosmos SDK's Staking module.
// MsgWrappedDelegate is the message for delegating stakes
message MsgWrappedDelegate {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (cosmos.msg.v1.signer) = "msg";
cosmos.staking.v1beta1.MsgDelegate msg = 1;
}
// MsgWrappedUndelegate is the message for undelegating stakes
message MsgWrappedUndelegate {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (cosmos.msg.v1.signer) = "msg";
cosmos.staking.v1beta1.MsgUndelegate msg = 1;
}
// MsgWrappedDelegate is the message for moving bonded stakes from a
// validator to another validator
message MsgWrappedBeginRedelegate {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (cosmos.msg.v1.signer) = "msg";
cosmos.staking.v1beta1.MsgBeginRedelegate msg = 1;
}
// MsgWrappedCancelUnbondingDelegation is the message for cancelling
// an unbonding delegation
message MsgWrappedCancelUnbondingDelegation {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (cosmos.msg.v1.signer) = "msg";
cosmos.staking.v1beta1.MsgCancelUnbondingDelegation msg = 1;
}
The handlers of the epoched staking messages in the Epoching module are defined at x/epoching/keeper/msg_server.go. Each handler performs the same verification logics of the corresponding message as the ones performed by the Cosmos SDK's Staking module, and then inserts the message to the epoch message queue storage.
The MsgUpdateParams
message is used for updating the module parameters for the
Epoching module. It can only be executed via a governance proposal.
// MsgUpdateParams defines a message for updating Epoching module parameters.
message MsgUpdateParams {
option (cosmos.msg.v1.signer) = "authority";
// authority is the address of the governance account.
// just FYI: cosmos.AddressString marks that this field should use type alias
// for AddressString instead of string, but the functionality is not yet implemented
// in cosmos-proto
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// params defines the epoching parameters to update.
//
// NOTE: All parameters must be supplied.
Params params = 2 [(gogoproto.nullable) = false];
}
Babylon disables the Staking module's EndBlocker to avoid validator set updates
upon each block. The Epoching module implements BeginBlocker
to initialize an
epoch upon the beginning of an epoch, and implements EndBlocker
to execute all
messages and update the validator set upon the end of an epoch.
Cosmos SDK's Staking module updates the validator
set
upon EndBlocker
of every block. In order to implement the epoching mechanism,
Babylon disables the Staking module's EndBlocker
as
follows.
// Babylon does not want EndBlock processing in staking
app.ModuleManager.OrderEndBlockers = append(app.ModuleManager.OrderEndBlockers[:2], app.ModuleManager.OrderEndBlockers[2+1:]...) // remove stakingtypes.ModuleName
Upon BeginBlocker
, the Epoching module of each Babylon node will execute the
following:
- If at the first block of the next epoch, then do the following:
- Enter a new epoch, i.e., create a new
Epoch
object and save it to the epoch metadata storage. - Record the current
AppHash
as the sealer Apphash for the previous epoch. The entireAppState
till the end of the last epoch commits to thisAppHash
, hence the name "sealer AppHash". - Initialize the epoch message queue for the current epoch.
- Save the current validator set to the epoch validator set storage.
- Trigger hooks and emit events that the chain has entered a new epoch.
- Enter a new epoch, i.e., create a new
- If at the last block of the current epoch, then record the current
BlockHash
as the sealer BlockHash for the current epoch. The entire blockchain so far commits to thisBlockHash
via a hash chain, hence the name "sealer BlockHash".
Upon EndBlocker
, the Epoching module of each Babylon node will execute the
following if at the last block of the current epoch:
- Get all queued messages of this epoch in the epoch message queue storage.
- Forward each of the queued messages to the corresponding message handler in the Staking module.
- Emit events about the execution results of the messages.
- Invoke the Staking module to update the validator set.
- Trigger hooks and emit events that the chain has ended the current epoch.
The Epoching module implements a set of hooks to notify other modules about
certain events, and utilizes the AfterRawCheckpointFinalized
hook in the Checkpointing module for
Bitcoin-assisted unbonding.
// EpochingHooks event hooks for epoching validator object (noalias)
type EpochingHooks interface {
AfterEpochBegins(ctx context.Context, epoch uint64) // Must be called after an epoch begins
AfterEpochEnds(ctx context.Context, epoch uint64) // Must be called after an epoch ends
BeforeSlashThreshold(ctx context.Context, valSet ValidatorSet) // Must be called before a certain threshold (1/3 or 2/3) of validators are slashed in a single epoch
}
The Epoching module subscribes to the Checkpointing module's
AfterRawCheckpointFinalized
hook for
Bitcoin-assisted unbonding. The AfterRawCheckpointFinalized
hook is triggered
upon a checkpoint becoming finalized, i.e., Bitcoin transactions of the
checkpoint become w
-deep in Bitcoin's canonical chain, where w
is the
checkpoint_finalization_timeout
parameter in the
BTCCheckpoint module. Upon AfterRawCheckpointFinalized
, the Epoching module
will finish all unbonding validators and delegations till the epoch associated
with the finalized checkpoint, including the following:
- Find the metadata
Epoch
of the epoch associated with the finalized checkpoint. - Find the timestamp of the last block of this epoch from
Epoch
. - Notify the Staking module to finish all unbonding validators and delegations before this timestamp.
The Epoching module defines a set of events about the state updates of epochs, validators, and delegations.
// EventBeginEpoch is the event emitted when an epoch has started
message EventBeginEpoch { uint64 epoch_number = 1; }
// EventEndEpoch is the event emitted when an epoch has ended
message EventEndEpoch { uint64 epoch_number = 1; }
// EventHandleQueuedMsg is the event emitted when a queued message has been handled
message EventHandleQueuedMsg {
string original_event_type = 1;
uint64 epoch_number = 2;
uint64 height = 3;
bytes tx_id = 4;
bytes msg_id = 5;
repeated bytes original_attributes = 6
[ (gogoproto.customtype) =
"github.com/cometbft/cometbft/abci/types.EventAttribute" ];
string error = 7;
}
// EventSlashThreshold is the event emitted when a set of validators have been slashed
message EventSlashThreshold {
int64 slashed_voting_power = 1;
int64 total_voting_power = 2;
repeated bytes slashed_validators = 3;
}
// EventWrappedDelegate is the event emitted when a MsgWrappedDelegate has been queued
message EventWrappedDelegate {
string delegator_address = 1;
string validator_address = 2;
uint64 amount = 3;
string denom = 4;
uint64 epoch_boundary = 5;
}
// EventWrappedUndelegate is the event emitted when a MsgWrappedUndelegate has been queued
message EventWrappedUndelegate {
string delegator_address = 1;
string validator_address = 2;
uint64 amount = 3;
string denom = 4;
uint64 epoch_boundary = 5;
}
// EventWrappedBeginRedelegate is the event emitted when a MsgWrappedBeginRedelegate has been queued
message EventWrappedBeginRedelegate {
string delegator_address = 1;
string source_validator_address = 2;
string destination_validator_address = 3;
uint64 amount = 4;
string denom = 5;
uint64 epoch_boundary = 6;
}
// EventWrappedCancelUnbondingDelegation is the event emitted when a MsgWrappedCancelUnbondingDelegation has been queued
message EventWrappedCancelUnbondingDelegation {
string delegator_address = 1;
string validator_address = 2;
uint64 amount = 3;
int64 creation_height = 4;
uint64 epoch_boundary = 5;
}
The Epoching module provides a set of queries about epochs, validators and delegations, listed at docs.babylonchain.io.