Skip to content

Commit

Permalink
epoching/checkpointing: checkpoint-assisted unbonding (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastianElvis authored Sep 20, 2022
1 parent 42c34ac commit c48a6b3
Show file tree
Hide file tree
Showing 18 changed files with 865 additions and 190 deletions.
3 changes: 3 additions & 0 deletions cmd/babylond/cmd/testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,9 @@ func initGenFiles(
var stakingGenState stakingtypes.GenesisState
clientCtx.Codec.MustUnmarshalJSON(appGenState[stakingtypes.ModuleName], &stakingGenState)
stakingGenState.Params.MaxValidators = maxActiveValidators
// Babylon should enforce this value to be 0. However Cosmos enforces it to be positive so we use the smallest value 1
// Instead the timing of unbonding is decided by checkpoint states
stakingGenState.Params.UnbondingTime = 1
appGenState[stakingtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&stakingGenState)

appGenStateJSON, err := json.MarshalIndent(appGenState, "", " ")
Expand Down
5 changes: 5 additions & 0 deletions proto/babylon/epoching/v1/epoching.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package babylon.epoching.v1;

import "google/protobuf/timestamp.proto";
import "gogoproto/gogo.proto";
import "tendermint/types/types.proto";
import "cosmos/staking/v1beta1/tx.proto";

option go_package = "github.com/babylonchain/babylon/x/epoching/types";
Expand All @@ -11,6 +12,10 @@ message Epoch {
uint64 epoch_number = 1;
uint64 current_epoch_interval = 2;
uint64 first_block_height = 3;
// last_block_header is the header of the last block in this epoch.
// Babylon needs to remember the last header of each epoch to complete unbonding validators/delegations when a previous epoch's checkpoint is finalised.
// The last_block_header field is nil in the epoch's beginning, and is set upon the end of this epoch.
tendermint.types.Header last_block_header = 4;
}

// QueuedMessage is a message that can change the validator set and is delayed to the epoch boundary
Expand Down
13 changes: 13 additions & 0 deletions proto/babylon/epoching/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ service Query {
option (google.api.http).get = "/babylon/epoching/v1/params";
}

// EpochInfo queries the information of a given epoch
rpc EpochInfo(QueryEpochInfoRequest) returns (QueryEpochInfoResponse) {
option (google.api.http).get = "/babylon/epoching/v1/epochs/{epoch_num=*}";
}

// CurrentEpoch queries the current epoch
rpc CurrentEpoch(QueryCurrentEpochRequest) returns (QueryCurrentEpochResponse) {
option (google.api.http).get = "/babylon/epoching/v1/current_epoch";
Expand Down Expand Up @@ -51,6 +56,14 @@ message QueryParamsResponse {
babylon.epoching.v1.Params params = 1 [ (gogoproto.nullable) = false ];
}

message QueryEpochInfoRequest {
uint64 epoch_num = 1;
}

message QueryEpochInfoResponse {
babylon.epoching.v1.Epoch epoch = 1;
}

// QueryCurrentEpochRequest is the request type for the Query/CurrentEpoch RPC method
message QueryCurrentEpochRequest {}

Expand Down
16 changes: 14 additions & 2 deletions testutil/mocks/checkpointing_expected_keepers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions x/checkpointing/keeper/grpc_query_checkpoint_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package keeper_test

import (
"math/rand"
"testing"

"github.com/babylonchain/babylon/testutil/mocks"
"github.com/babylonchain/babylon/x/checkpointing/types"
epochingtypes "github.com/babylonchain/babylon/x/epoching/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/golang/mock/gomock"
"math/rand"
"testing"

"github.com/babylonchain/babylon/testutil/datagen"
testkeeper "github.com/babylonchain/babylon/testutil/keeper"
Expand Down Expand Up @@ -54,7 +55,7 @@ func FuzzQueryStatusCount(f *testing.F) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ek := mocks.NewMockEpochingKeeper(ctrl)
ek.EXPECT().GetEpoch(gomock.Any()).Return(epochingtypes.Epoch{EpochNumber: tipEpoch + 1})
ek.EXPECT().GetEpoch(gomock.Any()).Return(&epochingtypes.Epoch{EpochNumber: tipEpoch + 1})
ckptKeeper, ctx, _ := testkeeper.CheckpointingKeeper(t, ek, nil, client.Context{})
sdkCtx := sdk.WrapSDKContext(ctx)
expectedCounts := make(map[string]uint64)
Expand Down
5 changes: 4 additions & 1 deletion x/checkpointing/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"errors"
"fmt"

"github.com/babylonchain/babylon/crypto/bls12381"
"github.com/babylonchain/babylon/x/checkpointing/types"
epochingtypes "github.com/babylonchain/babylon/x/epoching/types"
Expand Down Expand Up @@ -252,6 +253,8 @@ func (k Keeper) SetCheckpointFinalized(ctx sdk.Context, epoch uint64) {
if err != nil {
ctx.Logger().Error("failed to emit checkpoint finalized event for epoch %v", ckpt.Ckpt.EpochNum)
}
// finalise all unbonding validators/delegations in this epoch
k.epochingKeeper.ApplyMatureUnbonding(ctx, epoch)
}

func (k Keeper) SetCheckpointForgotten(ctx sdk.Context, epoch uint64) {
Expand Down Expand Up @@ -299,7 +302,7 @@ func (k Keeper) GetBlsPubKey(ctx sdk.Context, address sdk.ValAddress) (bls12381.
return k.RegistrationState(ctx).GetBlsPubKey(address)
}

func (k Keeper) GetEpoch(ctx sdk.Context) epochingtypes.Epoch {
func (k Keeper) GetEpoch(ctx sdk.Context) *epochingtypes.Epoch {
return k.epochingKeeper.GetEpoch(ctx)
}

Expand Down
14 changes: 11 additions & 3 deletions x/checkpointing/keeper/keeper_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package keeper_test

import (
"math/rand"
"testing"

"github.com/babylonchain/babylon/testutil/datagen"
testkeeper "github.com/babylonchain/babylon/testutil/keeper"
"github.com/babylonchain/babylon/testutil/mocks"
"github.com/babylonchain/babylon/x/checkpointing/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"math/rand"
"testing"
)

/*
Expand Down Expand Up @@ -59,7 +62,12 @@ func FuzzKeeperSetCheckpointStatus(f *testing.F) {
datagen.AddRandomSeedsToFuzzer(f, 1)
f.Fuzz(func(t *testing.T, seed int64) {
rand.Seed(seed)
ckptKeeper, ctx, _ := testkeeper.CheckpointingKeeper(t, nil, nil, client.Context{})

ctrl := gomock.NewController(t)
defer ctrl.Finish()
ek := mocks.NewMockEpochingKeeper(ctrl)
ek.EXPECT().ApplyMatureUnbonding(gomock.Any(), gomock.Any()).Return() // make ApplyMatureUnbonding do nothing
ckptKeeper, ctx, _ := testkeeper.CheckpointingKeeper(t, ek, nil, client.Context{})

mockCkptWithMeta := datagen.GenRandomRawCheckpointWithMeta()
mockCkptWithMeta.Status = types.Accumulating
Expand Down
3 changes: 2 additions & 1 deletion x/checkpointing/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ type BankKeeper interface {

// EpochingKeeper defines the expected interface needed to retrieve epoch info
type EpochingKeeper interface {
GetEpoch(ctx sdk.Context) epochingtypes.Epoch
GetEpoch(ctx sdk.Context) *epochingtypes.Epoch
EnqueueMsg(ctx sdk.Context, msg epochingtypes.QueuedMessage)
GetValidatorSet(ctx sdk.Context, epochNumer uint64) epochingtypes.ValidatorSet
GetTotalVotingPower(ctx sdk.Context, epochNumber uint64) int64
ApplyMatureUnbonding(ctx sdk.Context, epochNumber uint64)
}

// Event Hooks
Expand Down
5 changes: 5 additions & 0 deletions x/epoching/abci.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package epoching

import (
"fmt"
"time"

"github.com/babylonchain/babylon/x/epoching/keeper"
Expand Down Expand Up @@ -60,6 +61,8 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate {
// if reaching an epoch boundary, then
epoch := k.GetEpoch(ctx)
if epoch.IsLastBlock(ctx) {
// finalise this epoch, i.e., record the current header
k.RecordLastBlockHeader(ctx)
// get all msgs in the msg queue
queuedMsgs := k.GetCurrentEpochMsgs(ctx)
// forward each msg in the msg queue to the right keeper
Expand Down Expand Up @@ -105,6 +108,8 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate {

// update validator set
validatorSetUpdate = k.ApplyAndReturnValidatorSetUpdates(ctx)
ctx.Logger().Info(fmt.Sprintf("Epoching: validator set update of epoch %d: %v", epoch.EpochNumber, validatorSetUpdate))

// trigger AfterEpochEnds hook
k.AfterEpochEnds(ctx, epoch.EpochNumber)
// emit EndEpoch event
Expand Down
78 changes: 69 additions & 9 deletions x/epoching/keeper/epochs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"github.com/babylonchain/babylon/x/epoching/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
)

Expand All @@ -17,29 +18,88 @@ func (k Keeper) setEpochNumber(ctx sdk.Context, epochNumber uint64) {
store.Set(types.EpochNumberKey, epochNumberBytes)
}

func (k Keeper) getEpochNumber(ctx sdk.Context) uint64 {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.EpochNumberKey)
if bz == nil {
panic(types.ErrUnknownEpochNumber)
}
epochNumber := sdk.BigEndianToUint64(bz)
return epochNumber
}

func (k Keeper) setEpochInfo(ctx sdk.Context, epochNumber uint64, epoch *types.Epoch) {
store := k.epochInfoStore(ctx)
epochNumberBytes := sdk.Uint64ToBigEndian(epochNumber)
epochBytes := k.cdc.MustMarshal(epoch)
store.Set(epochNumberBytes, epochBytes)
}

func (k Keeper) getEpochInfo(ctx sdk.Context, epochNumber uint64) (*types.Epoch, error) {
store := k.epochInfoStore(ctx)
epochNumberBytes := sdk.Uint64ToBigEndian(epochNumber)
bz := store.Get(epochNumberBytes)
if bz == nil {
return nil, types.ErrUnknownEpochNumber
}
var epoch types.Epoch
k.cdc.MustUnmarshal(bz, &epoch)
return &epoch, nil
}

// InitEpoch sets the zero epoch number to DB
func (k Keeper) InitEpoch(ctx sdk.Context) {
header := ctx.BlockHeader()
if header.Height > 0 {
panic("InitEpoch can be invoked only at genesis")
}
epochInterval := k.GetParams(ctx).EpochInterval
epoch := types.NewEpoch(0, epochInterval, &header)
k.setEpochInfo(ctx, 0, &epoch)

k.setEpochNumber(ctx, 0)
}

// GetEpoch fetches the current epoch
func (k Keeper) GetEpoch(ctx sdk.Context) types.Epoch {
store := ctx.KVStore(k.storeKey)
func (k Keeper) GetEpoch(ctx sdk.Context) *types.Epoch {
epochNumber := k.getEpochNumber(ctx)
epoch, err := k.getEpochInfo(ctx, epochNumber)
if err != nil {
panic(err)
}
return epoch
}

bz := store.Get(types.EpochNumberKey)
if bz == nil {
panic(types.ErrUnknownEpochNumber)
func (k Keeper) GetHistoricalEpoch(ctx sdk.Context, epochNumber uint64) (*types.Epoch, error) {
epoch, err := k.getEpochInfo(ctx, epochNumber)
return epoch, err
}

func (k Keeper) RecordLastBlockHeader(ctx sdk.Context) *types.Epoch {
epoch := k.GetEpoch(ctx)
if !epoch.IsLastBlock(ctx) {
panic("RecordLastBlockHeader can only be invoked at the last block of an epoch")
}
epochNumber := sdk.BigEndianToUint64(bz)
epochInterval := k.GetParams(ctx).EpochInterval
return types.NewEpoch(epochNumber, epochInterval)
header := ctx.BlockHeader()
epoch.LastBlockHeader = &header
k.setEpochInfo(ctx, epoch.EpochNumber, epoch)
return epoch
}

// IncEpoch adds epoch number by 1
func (k Keeper) IncEpoch(ctx sdk.Context) types.Epoch {
epochNumber := k.GetEpoch(ctx).EpochNumber
incrementedEpochNumber := epochNumber + 1
k.setEpochNumber(ctx, incrementedEpochNumber)

epochInterval := k.GetParams(ctx).EpochInterval
return types.NewEpoch(incrementedEpochNumber, epochInterval)
newEpoch := types.NewEpoch(incrementedEpochNumber, epochInterval, nil)
k.setEpochInfo(ctx, incrementedEpochNumber, &newEpoch)

return newEpoch
}

func (k Keeper) epochInfoStore(ctx sdk.Context) prefix.Store {
store := ctx.KVStore(k.storeKey)
return prefix.NewStore(store, types.EpochInfoKey)
}
13 changes: 13 additions & 0 deletions x/epoching/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ func (k Keeper) CurrentEpoch(c context.Context, req *types.QueryCurrentEpochRequ
return resp, nil
}

// EpochInfo handles the QueryEpochInfoRequest query
func (k Keeper) EpochInfo(c context.Context, req *types.QueryEpochInfoRequest) (*types.QueryEpochInfoResponse, error) {
ctx := sdk.UnwrapSDKContext(c)
epoch, err := k.GetHistoricalEpoch(ctx, req.EpochNum)
if err != nil {
return nil, err
}
resp := &types.QueryEpochInfoResponse{
Epoch: epoch,
}
return resp, nil
}

// EpochMsgs handles the QueryEpochMsgsRequest query
func (k Keeper) EpochMsgs(c context.Context, req *types.QueryEpochMsgsRequest) (*types.QueryEpochMsgsResponse, error) {
ctx := sdk.UnwrapSDKContext(c)
Expand Down
Loading

0 comments on commit c48a6b3

Please sign in to comment.