Sneaky Mercurial Python
High
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/sherlock-audit/2024-12-babylon/blob/main/babylon/x/epoching/abci.go#L75-L133
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: }
NIL
NIL
NIL
- 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
- epoch = 0
- epoch lenght = 5
- at block 5 start,
BeginBlocker()
is called butRecordSealerBlockHashForEpoch()
is not executed - at block 5 end,
EndBlocker()
is called butApplyAndReturnValidatorSetUpdates(ctx)
and other important calls omitted
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: }