Sneaky Mercurial Python
High
Honest validators can get slashed due to proposing a vote with a validator that is no longer bonded (or active)
Proposals can be prepared using validators the have become unbonded (inactive) causing the proposing validators to get slashed.
https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/checkpointing/proposal.go#L101
File: .../babylon/x/checkpointing/proposal.go
068: func (h *ProposalHandler) PrepareProposal() sdk.PrepareProposalHandler {
069: return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
////SNIP
101: @> ckpt, err := h.buildCheckpointFromVoteExtensions(ctx, epoch.EpochNumber, req.LocalLastCommit.Votes)
123: func (h *ProposalHandler) buildCheckpointFromVoteExtensions(ctx sdk.Context, epoch uint64, extendedVotes []abci.ExtendedVoteInfo) (*ckpttypes.RawCheckpointWithMeta, error) {
124: prevBlockID, err := h.findLastBlockHash(extendedVotes)
125: if err != nil {
126: return nil, err
127: }
128: ckpt := ckpttypes.NewCheckpointWithMeta(ckpttypes.NewCheckpoint(epoch, prevBlockID), ckpttypes.Accumulating)
129: validBLSSigs := h.getValidBlsSigs(ctx, extendedVotes, prevBlockID)
130: @> vals := h.ckptKeeper.GetValidatorSet(ctx, epoch)
131: totalPower := h.ckptKeeper.GetTotalVotingPower(ctx, epoch)
/////SNIP
152: @> err = ckpt.Accumulate(vals, signerAddress, signerBlsKey, *sig.BlsSig, totalPower)
The problem is that h.ckptKeeper.GetValidatorSet
returns the the validators in the current epoch whether or not they have been unbonded and become inactive within the epoch and as such, provided the 2/3 total power is achieved, inactive validator signatures are used to build and seal a checkpoint using ckpt.Accumulate()
call
File: ...babylon/x/checkpointing/types/types.go
060: func (cm *RawCheckpointWithMeta) Accumulate(
061: vals epochingtypes.ValidatorSet,
062: signerAddr sdk.ValAddress,
063: signerBlsKey bls12381.PublicKey,
064: sig bls12381.Signature,
065: totalPower int64) error {
066: // the checkpoint should be accumulating
067: if cm.Status != Accumulating {
068: return ErrCkptNotAccumulating
069: }
070:
071: // get validator and its index
072: val, index, err := vals.FindValidatorWithIndex(signerAddr)
073: if err != nil {
074: return err
075: }
076:
077: // return an error if the validator has already voted
078: if bitmap.Get(cm.Ckpt.Bitmap, index) {
079: return ErrCkptAlreadyVoted
080: }
081:
082: // aggregate BLS sig
083: if cm.Ckpt.BlsMultiSig != nil {
084: aggSig, err := bls12381.AggrSig(*cm.Ckpt.BlsMultiSig, sig)
085: if err != nil {
086: return err
087: }
088: cm.Ckpt.BlsMultiSig = &aggSig
089: } else {
090: cm.Ckpt.BlsMultiSig = &sig
091: }
092:
093: // aggregate BLS public key
094: if cm.BlsAggrPk != nil {
095: aggPK, err := bls12381.AggrPK(*cm.BlsAggrPk, signerBlsKey)
096: if err != nil {
097: return err
098: }
099: cm.BlsAggrPk = &aggPK
100: } else {
101: cm.BlsAggrPk = &signerBlsKey
102: }
103:
104: // update bitmap
105: bitmap.Set(cm.Ckpt.Bitmap, index, true)
106:
107: // accumulate voting power and update status when the threshold is reached
108: cm.PowerSum += uint64(val.Power)
109: if int64(cm.PowerSum)*3 > totalPower*2 {
110: cm.Status = Sealed
111: }
112:
113: return nil
114: }
115:
Active validators are gotten from epoch validator set which can contain validators who unbonded within the epoch
NIL
See POC section
This
- breaks core protocol functionality as an unbonded validator's signature is still used for critical evaluations withing the protocol
- can lead to validators getting slashed unjustly because the proposal preparation allows a path for this to happen
All these because
- An honest validator can propose invalid blocks because votes of a validator of a current set is not in the current block ACTIVE validator set.
For simplicity there are 2 validators X and Y
- power of X = 20
- power of Y = 50
- current epoch = 50,
- current epoch's
FirstBlockHeight
= 2001 - current block 2035
- epoch length 40 blocks (epoch 50 ends at block 2040)
- X was unbonded at 2030 but is still in the validator set of the epoch
- the check point is built using the signature of an inactive validator
In a situation where this can trigger the slashing of the proposer, then X can purposely chose to be unbonded in the middle of an active epoch so that so that proposals prepared before the end of the epoch will include its signature since it has become unbonded (inactive) and this can trigger a slashing cascade of all validators the prepare proposal from the point after X unbonded till the end of the epoch
I can't conceive a trivial solution at the moment, but you can consider getting the active validators from the previous block instead of an epoch since an epoch spans a number of blocks during which a validator may have become inactive.