Slow Misty Iguana
Medium
Finality providers will not be slashed for voting on multiple fork blocks.
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.
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.
Only one vote is allowed at the same height.