Skip to content

Latest commit

 

History

History
98 lines (69 loc) · 2.97 KB

001.md

File metadata and controls

98 lines (69 loc) · 2.97 KB

Genuine Currant Mantaray

High

If a block does not reach consensus even for a very short amount of time, babylon nodes will crash

Summary

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.

Root Cause

TallyingBlocks:

	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.

Internal Pre-conditions

None

External Pre-conditions

  • A block does not reach consensus immediately

Attack Path

  • 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 to finalizationLoop
  • Since the block at startHeight is now already finalized but we have a fpSet for it, we enter the case case fpSet != nil && ib.Finalized which panics, stopping any node processing the block

Impact

This will cause network nodes to crash and halt the network if enough nodes are affected

PoC

No response

Mitigation

Consider moving the tag finalizationLoop above the call to getNextHeightToFinalize. This would account for already finalized blocks (finalizeBlock calls setNextHeightToFinalize), preventing double-finalization.