Skip to content

Latest commit

 

History

History
150 lines (116 loc) · 4.95 KB

029.md

File metadata and controls

150 lines (116 loc) · 4.95 KB

Slow Misty Iguana

Medium

totalPower's inclusion of the Jailed provider's voting power may result in a vote never reaching a 2/3 quorum

Summary

totalPower's inclusion of the Jailed provider's voting power may result in a vote never reaching a 2/3 quorum

Root Cause

totalPower's inclusion of the Jailed provider's voting power

Internal Pre-conditions

There are providers being Jailed

External Pre-conditions

Attack Path

Impact

result in a vote never reaching a 2/3 quorum , blocks can never enter Finalize state and chains cannot work.

PoC

TheAddFinalitySig function is used to add the vote, but if the provider is in theJailed state, this function returns an error. Therefore, the Jailed provider cannot vote.

// AddFinalitySig adds a new vote to a given block
func (ms msgServer) AddFinalitySig(goCtx context.Context, req *types.MsgAddFinalitySig) (*types.MsgAddFinalitySigResponse, error) {
	.......
	if fp.IsJailed() {
		return nil, bstypes.ErrFpAlreadyJailed.Wrapf("finality provider public key: %s", fpPK.MarshalHex())
	}
        .......
}

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/finality/keeper/msg_server.go#L116-L118

But the problem is thattotalPower is calculated based onVotingPowerTable and does not subtract Jailed VotingPower:

func (k Keeper) TallyBlocks(ctx context.Context) {
......
	// get the finality provider set of this block
->	fpSet := k.GetVotingPowerTable(ctx, ib.Height)

	switch {
	case fpSet != nil && !ib.Finalized:
		// has finality providers, non-finalised: tally and try to finalise the block
		voterBTCPKs := k.GetVoters(ctx, ib.Height)
->		if tally(fpSet, voterBTCPKs) {
			// if this block gets >2/3 votes, finalise it
			k.finalizeBlock(ctx, ib)
		} else {
			// if not, then this block and all subsequent blocks should not be finalised
			// thus, we need to break here
			break finalizationLoop
		}
......
}

func tally(fpSet map[string]uint64, voterBTCPKs map[string]struct{}) bool {
	totalPower := uint64(0)
	votedPower := uint64(0)
	for pkStr, power := range fpSet {
		totalPower += power
		if _, ok := voterBTCPKs[pkStr]; ok {
			votedPower += power
		}
	}

	return votedPower*3 > totalPower*2
}

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/finality/keeper/tallying.go#L50

AddFinalitySig can vote on multiple blocks, assuming that two blocks are voting at the same time, The current block height is n+2, and the VotingPowerTable now contains the voting power of both blocks n and n+1. provider votes for block n, at which point the provider is Jailed and his vote for block n is valid(block n is fine), But for block n+1 this provider cannot vote, the problem is that totalVotingPower in block n+1 has not subtracted the VotingPower of this provider. If the provider has more VotingPower or if more providers are Jailed, the vote will never reach a 2/3 quorum.

It is very likely that the system will put lazy fp's in jail, and there will be a certain period of time, if there are many fp's in prison in the same period, if the total voting power is more than 2/3, the vote in that period may never pass, because the fp's cannot vote.

x/finality/abci.go/EndBlocker -> HandleLiveness -> HandleFinalityProviderLiveness

func (k Keeper) HandleFinalityProviderLiveness(ctx context.Context, fpPk *types.BIP340PubKey, missed bool, height int64) error {
	......
	updated, signInfo, err := k.updateSigningInfo(ctx, fpPk, missed, height)
	if err != nil {
		return err
	}

	signedBlocksWindow := params.SignedBlocksWindow
	minSignedPerWindow := params.MinSignedPerWindowInt()

	if missed {
		k.Logger(sdkCtx).Debug(
			"absent finality provider",
			"height", height,
			"public_key", fpPk.MarshalHex(),
			"missed", signInfo.MissedBlocksCounter,
			"threshold", minSignedPerWindow,
		)
	}

	minHeight := signInfo.StartHeight + signedBlocksWindow
	maxMissed := signedBlocksWindow - minSignedPerWindow

	// if the number of missed block reaches the threshold within the sliding window
	// jail the finality provider
	if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
		updated = true

		if err := k.jailSluggishFinalityProvider(ctx, fpPk); err != nil {
			return fmt.Errorf("failed to jail sluggish finality provider %s: %w", fpPk.MarshalHex(), err)
		}

->		signInfo.JailedUntil = sdkCtx.HeaderInfo().Time.Add(params.JailDuration)
		// we need to reset the counter & bitmap so that the finality provider won't be
		// immediately jailed after unjailing.
		signInfo.MissedBlocksCounter = 0
		if err := k.DeleteMissedBlockBitmap(ctx, fpPk); err != nil {
			return fmt.Errorf("failed to remove the missed block bit map: %w", err)
		}

		k.Logger(sdkCtx).Info(
			"finality provider is jailed",
			"height", height,
			"public_key", fpPk.MarshalHex(),
		)
	}

	// Set the updated signing info
	if updated {
->		return k.FinalityProviderSigningTracker.Set(ctx, fpPk.MustMarshal(), *signInfo)
	}

	return nil
}

Mitigation

totalPower = totalPower - JailedVotingPower