Module: finality
Description
Babylon's BTC staking protocol introduces an additional finality round for blocks generated in CometBFT. Participants in this round are called finality providers (FPs), and their voting power is derived from delegated staked BTC.
The finality module is responsible for the following functions:
Handling extractable--one-time--signature (EOTS) public-randomness commit requests from FPs
Processing finality-vote submission requests from FPs
Managing the finalization status of blocks
Identifying sluggish FPs who vote slowly
Maintaining and managing evidence of equivocation (double-signing) by FPs
Messages
MsgResumeFinalityProposal
The MsgResumeFinalityProposal
message can only be executed through a governance proposal. It is used to verify FPs' participation in finality voting at a specific block height and to penalize those who have not participated.
When a Babylon node receives MsgResumeFinalityProposal
, it checks whether the specified FPs, identified by their public keys, have participated in finality voting at the given block height. FPs that have not participated are jailed, and their jail duration is set according to JailDuration
from the finality module, starting from the current block time. If an FP has previously missed blocks, its missed block count is reset.
If these checks pass, the message performs the following steps:
Set the voting power of the specified FPs to zero from the given block height up to the current block height.
Mark these FPs as jailed in the
VotingPowerDistCache
of the finality module.Iterate over all blocks from the most recently finalized block up to the current block height. If a block has accumulated more than two-thirds of the total votes, it is marked finalized and its finalization status is updated.
MsgAddFinalitySig
The MsgAddFinalitySig
message is submitted by FPs to provide EOTSs for a specific block during the finality voting process.
When a Babylon node receives MsgAddFinalitySig
, it checks the following conditions:
All required fields (
FpBtcPk
,PubRand
,Proof
,FinalitySig
, andBlockAppHash
) are not nil and have the correct length.The target block height is not before the BTC staking activation block height (retrieved from the finality module).
The target block height has been indexed in the finality module’s
EndBlocker
.The target block belongs to the most recently finalized epoch and is not from an earlier epoch.
The target block is not already finalized.
The FP (identified by
FpBtcPk
) is registered, not jailed or slashed, and has nonzero voting power.The FP has not already submitted a signature for the same block height.
If these checks pass, the system
retrieves the public randomness committed via
MsgCommitPubRandList
that matches the target block height;verifies the finality EOTS signature using the committed public randomness; and
checks whether the provided
BlockAppHash
differs from the storedAppHash
for the indexed block. If it differs, this is considered a fork vote, and the system generates and stores equivocation evidence for potential future slashing.
If the same message with identical parameters is submitted again, the system checks for any existing equivocation evidence. If it exists, the FP is slashed. Otherwise, if the block is not a fork block, the vote is recorded in the store.
MsgCommitPubRandList
The MsgCommitPubRandList
message allows an FP to commit a Merkle tree containing a list of EOTS public-randomness values. This message is typically submitted by the FP's program.
When a Babylon node receives MsgCommitPubRandList
, it checks the following conditions:
The submitted number of EOTS public-randomness values is at least
MinPubRand
(defined in the finality module parameters).The FP is correctly registered on the Babylon chain.
The submitted public randomness does not duplicate any previous commits.
The newly committed public randomness does not overlap with the block range already covered by the FP’s prior commitments.
The FP has signed the public randomness with a valid Schnorr signature.
If all conditions are met, the EOTS public randomness is stored on the Babylon chain along with the current epoch number.
MsgUnjailFinalityProvider
The MsgUnjailFinalityProvider
message is used by a jailed FP to request unjailing.
When a Babylon node receives MsgUnjailFinalityProvider
, it checks the following conditions:
The sender is the same as the FP requesting unjailing.
The FP is currently jailed.
The jail period has ended based on the current block time.
If these checks pass, the FP is unjailed.
MsgUpdateParams
The MsgUpdateParams
message updates the parameters of the finality module. This message can only be executed via a governance proposal.
ABCI++ handlers
BeginBlocker
At the beginning of each block, the finality module does the following:
Retrieves the voting-power distribution from the previous block
Processes events from the prior block that affected FPs (e.g., becoming active, unbonded, expired, jailed, slashed, or unjailed)
Invokes the
processRewardTracker
function from the incentive module to track and allocate rewards
After event processing, the module calculates the cumulative rewards for FPs by
dividing the current total rewards by the total staked Satoshi (
CurrentRewards / TotalActiveSat
) to determine per-Satoshi rewards,storing these reward values as historical data, and
distributing delegation rewards proportionally to FPs based on their delegation ratios.
For providers in ACTIVE
, UNBONDED
, or EXPIRED
states, the module updates the total staked Satoshi accordingly. If a provider has been slashed, all pending rewards are settled and sent to the gauge, and the provider is excluded from future rewards. The active FP list is updated according to the current block height, reflecting any changes in delegation and staking power.
To maintain an optimal number of active providers, the module sorts FPs by voting power and keeps only the top MaxActiveFinalityProviders
. The voting power of these retained providers is then updated, and events related to activated or deactivated providers are emitted. Finally, the module updates FinalityProviderSigningInfo
for each provider, tracking their voting participation and ensuring timely detection of nonparticipation.
EndBlocker
At the end of each block, the finality module does the following:
It indexes the current block height along with its
AppHash
and finalization status to maintain a record of finalized and pending blocks.It checks all nonfinalized blocks to determine if they have reached at least two-thirds of the total voting power; if so, it marks them as finalized in the system.
It verifies if any FPs have failed to vote within
FinalitySigTimeout
. If a provider consistently fails to vote for longer thanSignedBlocksWindow
, the provider is jailed for a duration defined byJailDuration
.It triggers the
RewardBTCStaking
function from the incentive module, for newly finalized blocks, to distribute fees collected in the fee collector. Providers receive their share of rewards minus their commission, with the remainder given to delegators.It cleans up old data by removing the voting-power--distribution cache for blocks that no longer need referencing, optimizing storage and processing for future blocks.
Test coverage
The x/finality package has low test coverage (11.6%), while the keeper module is well-tested (77.6%), and the types module is mostly untested (1.8%).
ok github.com/babylonlabs-io/babylon/x/finality 1.472s coverage: 11.6% of statements
ok github.com/babylonlabs-io/babylon/x/finality/keeper 47.864s coverage: 77.6% of statements
ok github.com/babylonlabs-io/babylon/x/finality/types 2.173s coverage: 1.8% of statements
Attack surface
The finality module exposes several messages, which if incorrectly implemented have the potential of incorrectly affecting the voting power of FPs, allowing FPs to miss blocks and ensuring the liveliness of FPs. Other potential issues that could arise are from incorrect state management in EndBlocker
s; these could affect voting-power issues when dealing with edge cases.