Skip to content

Latest commit

 

History

History
116 lines (80 loc) · 3.77 KB

043.md

File metadata and controls

116 lines (80 loc) · 3.77 KB

Sneaky Mercurial Python

High

Validator set and SealerBlockHash will not be recorded in the last block of epoch zero

Summary

SealerBlockHash is always recorded in the beginning of the last block of an epoch via RecordSealerBlockHashForEpoch() in the abci::BeginBlocker() execution in the last block of an epoch

Also, Validator set is always recorded in the last block of the epoch via EndBlocker() call

https://github.com/babylonlabs-io/babylon/blob/b5646552a1606e38fcdfa97ed2606549b9a233f7/x/epoching/abci.go#L61-L65

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

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/epoching/abci.go#L75-L133

Root Cause

The problem is that, RecordSealerBlockHashForEpoch() and ApplyAndReturnValidatorSetUpdate() will not be called in the beginning and end of the last block of the first epoch (i.e epoch = 0)

File: babylon/x/epoching/abci.go
28: func BeginBlocker(ctx context.Context, k keeper.Keeper) error {
29: 	defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker)
30: 
31: 	sdkCtx := sdk.UnwrapSDKContext(ctx)
/////SNIP
60: 
61: @>	if epoch.IsLastBlock(ctx) {
62: 		// record the block hash of the last block
63: 		// of the epoch to be sealed
64: 		k.RecordSealerBlockHashForEpoch(ctx)
65: 	}


File: .../babylon/x/epoching/abci.go
075: func EndBlocker(ctx context.Context, k keeper.Keeper) ([]abci.ValidatorUpdate, error) {
076: 	defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker)
077: 
078: 	sdkCtx := sdk.UnwrapSDKContext(ctx)
079: 	validatorSetUpdate := []abci.ValidatorUpdate{}
080: 
081: 	// if reaching an epoch boundary, then
082: 	epoch := k.GetEpoch(ctx)
083: @>	if epoch.IsLastBlock(ctx) {

////SNIP
131: 		// update validator set
132: @>		validatorSetUpdate = k.ApplyAndReturnValidatorSetUpdates(ctx)
133: 		sdkCtx.Logger().Info(fmt.Sprintf("Epoching: validator set update of epoch %d: %v", epoch.EpochNumber, validatorSetUpdate))
134: 

looking at the implementation of IsLastBlock() below, notice that GetLastBlockHeight() returns 0 when called in epoch 0

File: babylon/x/epoching/types/epoching.go
48: func (e Epoch) IsLastBlock(ctx context.Context) bool {
49: 	return e.GetLastBlockHeight() == uint64(sdk.UnwrapSDKContext(ctx).HeaderInfo().Height)
50: }


30: func (e Epoch) GetLastBlockHeight() uint64 {
31: @>	if e.EpochNumber == 0 {
32: 		return 0
33: 	}
34: 	return e.FirstBlockHeight + e.CurrentEpochInterval - 1
35: }

Internal Pre-conditions

NIL

External Pre-conditions

NIL

Attack Path

NIL

Impact

  • This breaks core protocol functionality as SealerBlockHash is not recorded for the first epoch
  • stale validator set is used from the first epoch (epoch = 0) in the epoch = 1 [in a situation where a validator has been jailed or even unbonded going into epoch 1, such validator will still be assumed active in epoch 1]
  • also note that messages are not forwarded as this is part of the EndBlocker() execution flow

PoC

  • epoch = 0
  • epoch lenght = 5
  • at block 5 start, BeginBlocker() is called but RecordSealerBlockHashForEpoch() is not executed
  • at block 5 end, EndBlocker() is called but ApplyAndReturnValidatorSetUpdates(ctx) and other important calls omitted

Mitigation

Modify the GetLastBlockHeight() function as shown below

File: babylon/x/epoching/types/epoching.go

30: func (e Epoch) GetLastBlockHeight() uint64 {
31: @>	if e.EpochNumber == 0 {
+32: 		return e.CurrentEpochInterval
-32: 		return 0
33: 	}
34: 	return e.FirstBlockHeight + e.CurrentEpochInterval - 1
35: }