Skip to content

Latest commit

 

History

History
133 lines (95 loc) · 4.22 KB

042.md

File metadata and controls

133 lines (95 loc) · 4.22 KB

Sneaky Mercurial Python

High

PrepareProposal() always return early after the first epoch (i.e epoch 0)

Summary

Proposals can only be prepared in the first block of the current epoch, however due to a logical flaw, preparing proposals will return early after the first epoch (i.e epoch = 0)

https://github.com/babylonlabs-io/babylon/blob/b5646552a1606e38fcdfa97ed2606549b9a233f7/x/epoching/types/epoching.go#L67-L96

https://github.com/babylonlabs-io/babylon/blob/b5646552a1606e38fcdfa97ed2606549b9a233f7/x/checkpointing/proposal.go#L81-L86

Root Cause

For Context,

  • Votes are extended in the last block of the previous epoch
  • the extended votes

As shown below, there is a check to ensure that proposals are prepared only in the first block of the current epoch

File: proposal.go
68: func (h *ProposalHandler) PrepareProposal() sdk.PrepareProposalHandler {
69: 	return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
70: 		// call default handler first to do basic validation
71: 		res, err := h.defaultPrepareProposalHandler(ctx, req)
72: 		if err != nil {
73: 			return nil, fmt.Errorf("failed in default PrepareProposal handler: %w", err)
74: 		}
75: 
76: 		k := h.ckptKeeper
77: 		proposalTxs := res.Txs
78: 		proposalRes := &abci.ResponsePrepareProposal{Txs: proposalTxs}
79: 
80: 	>>	epoch := k.GetEpoch(ctx)
81: 		// BLS signatures are sent in the last block of the previous epoch,
82: 	>>	// so they should be aggregated in the first block of the new epoch
83: 		// and no BLS signatures are send in epoch 0
84: 	>>	if !epoch.IsVoteExtensionProposal(ctx) {

The problem is that, the implementation of IsVoteExtensionProposal() function will cause the PrepareProposal() function to return early.

func (e Epoch) IsVoteExtensionProposal(ctx context.Context) bool {
	if e.EpochNumber == 0 {
		return false
	}
>>	return e.IsFirstBlockOfNextEpoch(ctx)
}

a look into the IsFirstBlockOfNextEpoch() function shows that it wrongly adds e.CurrentEpochInterval to the FirstBlockHeight hence, whenever the call is made in the first block of a new epoch, false will always be returned causing the function to return early

func (e Epoch) IsFirstBlockOfNextEpoch(ctx context.Context) bool {
	sdkCtx := sdk.UnwrapSDKContext(ctx)
	if e.EpochNumber == 0 {
		return sdkCtx.HeaderInfo().Height == 1
	} else {
		height := uint64(sdkCtx.HeaderInfo().Height)
>>		return e.FirstBlockHeight+e.CurrentEpochInterval == height
	}
}

The IsVoteExtensionProposal() and IsFirstBlockOfNextEpoch() function are used widely in the protocol and this will cause other inconsistencies not mentioned in this report as it will become too lengthy. Other affected core functions are

  • /gen_blocks.go::ApplyEmptyBlockWithSomeInvalidVoteExtensions()
  • ProcessProposal()
  • PreBlocker()

Internal Pre-conditions

IsFirstBlockOfNextEpoch() returns false in the first block of a new epoch

External Pre-conditions

NIL

Attack Path

See POC

Impact

When preparing proposal in the first block of a new epoch,

  • extended votes will not be validated
  • checkpoint will not be built from the extended votes
  • the returned proposalTxs will not contain the injected votes (since the execution flow will not get to the point where the injected tx bytes are built.

This may cause the chain to halt as proposals cannot be prepared due to a logical flaw

PoC

This applies to any succession of consecutive epochs (i, j, where j > i) but I'll use epoch 1 and epoch 2

  • last epoch epoch = 1 (started at Height = 1)
  • assume epoch Lenght = 20
  • current epoch, epoch = 2 (starting at Height = 21)
  • FirstBlockHeight of epoch 2 = 21
  • sdkCtx.HeaderInfo().Height = 21
  • IsFirstBlockOfNextEpoch() evaluates to
>>		return e.FirstBlockHeight+e.CurrentEpochInterval == height
>>		return (21 + 20) == 21 // => false

The PrepareProposal() thus returns early.

This affects

Mitigation

Modify the IsVoteExtensionProposal() function to use IsFirstBlock() instead of IsFirstBlockOfNextEpoch()

func (e Epoch) IsVoteExtensionProposal(ctx context.Context) bool {
	if e.EpochNumber == 0 {
		return false
	}
-	return e.IsFirstBlockOfNextEpoch(ctx)
+	return e.IsFirstBlock(ctx)
}