Skip to content

Latest commit

 

History

History
147 lines (109 loc) · 5.27 KB

056.md

File metadata and controls

147 lines (109 loc) · 5.27 KB

Slow Misty Iguana

Medium

Finality providers will not be slashed for voting on multiple fork blocks.

Summary

Finality providers will not be slashed for voting on multiple fork blocks.

Root Cause

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

func (ms msgServer) AddFinalitySig(goCtx context.Context, req *types.MsgAddFinalitySig) (*types.MsgAddFinalitySigResponse, error) {
	//skip
	if fp.IsSlashed() {
		return nil, bstypes.ErrFpAlreadySlashed.Wrapf("finality provider public key: %s", fpPK.MarshalHex())
	}

	if fp.IsJailed() {
		return nil, bstypes.ErrFpAlreadyJailed.Wrapf("finality provider public key: %s", fpPK.MarshalHex())
	}

	// ensure the finality provider has voting power at this height
	if ms.GetVotingPower(ctx, fpPK.MustMarshal(), req.BlockHeight) == 0 {
		return nil, types.ErrInvalidFinalitySig.Wrapf("the finality provider %s does not have voting power at height %d", fpPK.MarshalHex(), req.BlockHeight)
	}

	existingSig, err := ms.GetSig(ctx, req.BlockHeight, fpPK)
	if err == nil && existingSig.Equals(req.FinalitySig) {
		ms.Logger(ctx).Debug("Received duplicated finality vote", "block height", req.BlockHeight, "finality provider", req.FpBtcPk)
		// exactly same vote already exists, return error
		// this is to secure the tx refunding against duplicated messages
		return nil, types.ErrDuplicatedFinalitySig
	}

	// find the timestamped public randomness commitment for this height from this finality provider
	prCommit, err := ms.GetTimestampedPubRandCommitForHeight(ctx, req.FpBtcPk, req.BlockHeight)
	if err != nil {
		return nil, err
	}

	// verify the finality signature message w.r.t. the public randomness commitment
	// including the public randomness inclusion proof and the finality signature
	if err := types.VerifyFinalitySig(req, prCommit); err != nil {
		return nil, err
	}
	// the public randomness is good, set the public randomness
	ms.SetPubRand(ctx, req.FpBtcPk, req.BlockHeight, *req.PubRand)

	// verify whether the voted block is a fork or not
@>	if !bytes.Equal(indexedBlock.AppHash, req.BlockAppHash) {
		// the finality provider votes for a fork!

		// construct evidence
		evidence := &types.Evidence{
			FpBtcPk:              req.FpBtcPk,
			BlockHeight:          req.BlockHeight,
			PubRand:              req.PubRand,
			CanonicalAppHash:     indexedBlock.AppHash,
			CanonicalFinalitySig: nil,
			ForkAppHash:          req.BlockAppHash,
			ForkFinalitySig:      req.FinalitySig,
		}

		// if this finality provider has also signed canonical block, slash it
		canonicalSig, err := ms.GetSig(ctx, req.BlockHeight, fpPK)
		if err == nil {
			// set canonial sig
			evidence.CanonicalFinalitySig = canonicalSig
			// slash this finality provider, including setting its voting power to
			// zero, extracting its BTC SK, and emit an event
			ms.slashFinalityProvider(ctx, req.FpBtcPk, evidence)
		}

		// save evidence
		ms.SetEvidence(ctx, evidence)

		// NOTE: we should NOT return error here, otherwise the state change triggered in this tx
		// (including the evidence) will be rolled back
@>		return &types.MsgAddFinalitySigResponse{}, nil
	}

	// this signature is good, add vote to DB
	ms.SetSig(ctx, req.BlockHeight, fpPK, req.FinalitySig)

	// update `HighestVotedHeight` if needed
	if fp.HighestVotedHeight < uint32(req.BlockHeight) {
		fp.HighestVotedHeight = uint32(req.BlockHeight)
		err := ms.BTCStakingKeeper.UpdateFinalityProvider(ctx, fp)
		if err != nil {
			return nil, fmt.Errorf("failed to update the finality provider: %w", err)
		}
	}

	// if this finality provider has signed the canonical block before,
	// slash it via extracting its secret key, and emit an event
	if ms.HasEvidence(ctx, req.FpBtcPk, req.BlockHeight) {
		// the finality provider has voted for a fork before!
		// If this evidence is at the same height as this signature, slash this finality provider

		// get evidence
		evidence, err := ms.GetEvidence(ctx, req.FpBtcPk, req.BlockHeight)
		if err != nil {
			panic(fmt.Errorf("failed to get evidence despite HasEvidence returns true"))
		}

		// set canonical sig to this evidence
		evidence.CanonicalFinalitySig = req.FinalitySig
		ms.SetEvidence(ctx, evidence)

		// slash this finality provider, including setting its voting power to
		// zero, extracting its BTC SK, and emit an event
		ms.slashFinalityProvider(ctx, req.FpBtcPk, evidence)
	}

	// at this point, the finality signature is 1) valid, 2) over a canonical block,
	// and 3) not duplicated.
	// Thus, we can safely consider this message as refundable
	ms.IncentiveKeeper.IndexRefundableMsg(ctx, req)

	return &types.MsgAddFinalitySigResponse{}, nil
}

From the code logic of AddFinalitySig(), we can see that if a finality provider (FP) first votes for a canonical block and then votes for a fork block, or vice versa (first votes for a fork block and then for a canonical block), the FP will be slashed. However, if the FP votes for multiple fork blocks at the same height, it will not be penalized by slashing.

Internal pre-conditions

External pre-conditions

Impact

Malicious finality providers (FPs) can launch attacks by voting for multiple fork blocks, causing the blockchain to fork and resulting in the emergence of multiple chains with passed fork votes.

PoC

Mitigation

Only one vote is allowed at the same height.