Genuine Currant Mantaray
High
When a block does not gain consensus amongst FPs immediately (>2/3 vote on it), the chain halts due to reaching a panic in tallying.go::TallyBlocks
.
startHeight := k.getNextHeightToFinalize(ctx)
if startHeight < activatedHeight {
startHeight = activatedHeight
}
finalizationLoop:
for i := startHeight; i <= uint64(sdkCtx.HeaderInfo().Height); i++ {
ib, err := k.GetBlock(ctx, i)
if err != nil {
panic(err)
}
fpSet := k.GetVotingPowerTable(ctx, ib.Height)
switch {
case fpSet != nil && !ib.Finalized: // [1] <-----
voterBTCPKs := k.GetVoters(ctx, ib.Height)
if tally(fpSet, voterBTCPKs) {
k.finalizeBlock(ctx, ib)
} else {
break finalizationLoop // [2] <-----
}
case fpSet == nil && !ib.Finalized:
k.setNextHeightToFinalize(ctx, ib.Height+1)
continue
case fpSet != nil && ib.Finalized: // [3] <-----
panic(fmt.Errorf("block %d is finalized, but last finalized height in DB does not reach here", ib.Height))
// [...]
}
tally
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
}
We see at [1]
the normal case of an fpSet
existing at a height and that block not being finalized yet. At [2]
, we see that if the block does not reach consensus (tally
returns false
), we break and go to finalizationLoop
.
Now at [3]
we see that the case fpSet != nil && ib.Finalized
causes a panic.
Putting these together in Attack Path
, this will causes chain nodes to panic.
None
- A block does not reach consensus immediately
TallyBlocks
wants to finalize two blocks (startHeight = (uint64(sdkCtx.HeaderInfo().Height) - 2)
)- 1st block gets finalized (
block.Finalized = true
) - 2nd block has less than
2/3
votes so it cannot be finalized yet - We now start to iterate over BOTH blocks again
- since
startHeight
is the same as before and we jump tofinalizationLoop
- since
- Since the block at
startHeight
is now already finalized but we have afpSet
for it, we enter the casecase fpSet != nil && ib.Finalized
which panics, stopping any node processing the block
This will cause network nodes to crash and halt the network if enough nodes are affected
No response
Consider moving the tag finalizationLoop
above the call to getNextHeightToFinalize
. This would account for already finalized blocks (finalizeBlock
calls setNextHeightToFinalize
), preventing double-finalization.