Skip to content

Latest commit

 

History

History
147 lines (112 loc) · 5.19 KB

031.md

File metadata and controls

147 lines (112 loc) · 5.19 KB

Sneaky Mercurial Python

High

Honest validators can get slashed due to proposing a vote with a validator that is no longer bonded (or active)

Summary

Proposals can be prepared using validators the have become unbonded (inactive) causing the proposing validators to get slashed.

Root Cause

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/checkpointing/proposal.go#L101

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/checkpointing/proposal.go#L123-L124

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

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/checkpointing/types/types.go#L60-L113

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: 

Internal Pre-conditions

Active validators are gotten from epoch validator set which can contain validators who unbonded within the epoch

External Pre-conditions

NIL

Attack Path

See POC section

Impact

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.

PoC

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

Mitigation

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.