Petite Fleece Cheetah
High
The finality provider is slashed even if he just voted for the fork without voting for the canonical block
The problem is that the current system slashes FPs that voted twice - both for the fork and for the canonical block.
The root cause lies in a fact that the protocol immediately sets the evidence if the voting was performed for the fork - and in the next lines of the AddFinalitySig()
function slashes the finality provider even if the vote was just for the fork (without signing two blocks).
A FP adds a finality sig.
A FP adds a finality sig and gets unfairly slashed.
Finality providers get slashed even if they voted for a fork.
The current design of the system allows voting for the block but without voting for the fork at the same time:
https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/finality/README.md
If the voted block's AppHash is different from the canonical block at the same height known by the Babylon node, then this means the finality provider has voted for a fork. Babylon node buffers this finality vote to the evidence storage. If the finality provider has also voted for the block at the same height, then this finality provider is slashed, i.e., its voting power is removed, equivocation evidence is recorded, and a slashing event is emitted.
If the voted block's AppHash is same as that of the canonical block at the same height, then this means the finality provider has voted for the canonical block, and the Babylon node will store this finality vote to the finality vote storage. If the finality provider has also voted for a fork block at the same height, then this finality provider will be slashed.
Consider the current functionality:
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
}
First, the function addFinalitySig()
sets the evidence. And then couple lines later:
// 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)
}
The problem is that it may be the first vote for the fork without previously voting for the canonical block and the FP will still get slashed as the evidence is set before.
Change the implementation so the finality providers are not slashed upon the voting for the fork (without votiing for the canonical block as well).