Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

checkpointing: add keeper and core state #27

Merged
merged 21 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions proto/babylon/checkpointing/checkpoint.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "google/protobuf/timestamp.proto";

option go_package = "github.com/babylonchain/babylon/x/checkpointing/types";

// RawCheckpoint wraps the multi sig with meta data.
// RawCheckpoint wraps the bls multi sig with meta data
message RawCheckpoint {
option (gogoproto.equal) = true;

Expand All @@ -19,8 +19,25 @@ message RawCheckpoint {
bytes bitmap = 3;
// bls_multi_sig defines the multi sig that is aggregated from individual bls sigs
bytes bls_multi_sig = 4;
}

// RawCheckpointWithMeta wraps the raw checkpoint with meta data.
message RawCheckpointWithMeta {
RawCheckpoint ckpt = 1;
// status defines the status of the checkpoint
RawCheckpointStatus status = 5;
CheckpointStatus status = 2;
}

// CkptStatus is the status of a checkpoint.
enum CheckpointStatus{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
enum CheckpointStatus{
enum CheckpointStatus {

option (gogoproto.goproto_enum_prefix) = false;

// UNCHECKPOINTED defines a checkpoint that is checkpointed on BTC.
CKPT_STATUS_UNCHECKPOINTED = 0 [(gogoproto.enumvalue_customname) = "Uncheckpointed"];
// UNCONFIRMED defines a validator that is checkpointed on BTC but not confirmed.
CKPT_STATUS_UNCONFIRMED = 1 [(gogoproto.enumvalue_customname) = "Unconfirmed"];
// CONFIRMED defines a validator that is confirmed on BTC.
CKPT_STATUS_CONFIRMED = 2 [(gogoproto.enumvalue_customname) = "Confirmed"];
}

// BlsSig wraps the bls sig with meta data.
Expand All @@ -37,12 +54,3 @@ message BlsSig {
string signer_address = 5;
}

// RawCheckpointStatus defines the status of the raw checkpoint
enum RawCheckpointStatus {
// UNCHECKPOINTED indicates the checkpoint has not appeared on BTC
UNCHECKPOINTED = 0;
// CHECKPOINTED_NOT_CONFIRMED indicates the checkpoint has been checkpointed on BTC but has insufficent confirmation
CHECKPOINTED_NOT_CONFIRMED = 1;
// CONFIRMED indicates the checkpoint has sufficient confirmation depth on BTC
CONFIRMED = 2;
}
2 changes: 1 addition & 1 deletion proto/babylon/checkpointing/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ service Query {
// RPC method.
message QueryRawCheckpointListRequest {
// status defines the status of the raw checkpoints of the query
RawCheckpointStatus status = 1;
CheckpointStatus status = 1;

// pagination defines an optional pagination for the request.
cosmos.base.query.v1beta1.PageRequest pagination = 2;
Expand Down
2 changes: 1 addition & 1 deletion x/btclightclient/keeper/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (s HeadersState) GetHeadersByHeight(height uint64, f func(*wire.BlockHeader
}
stop := f(header)
if stop {
break
return nil
vitsalis marked this conversation as resolved.
Show resolved Hide resolved
}
}
return nil
Expand Down
63 changes: 63 additions & 0 deletions x/checkpointing/keeper/blssig_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package keeper

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

type BlsSigsState struct {
cdc codec.BinaryCodec
blsSigs sdk.KVStore
hashToEpoch sdk.KVStore
}

func (k Keeper) BlsSigsState(ctx sdk.Context) BlsSigsState {
// Build the BlsSigsState storage
store := ctx.KVStore(k.storeKey)
return BlsSigsState{
cdc: k.cdc,
blsSigs: prefix.NewStore(store, types.BlsSigsPrefix),
hashToEpoch: prefix.NewStore(store, types.BlsSigsHashToEpochPrefix),
}
}

// CreateBlsSig inserts the bls sig into the hash->epoch and (epoch, hash)->bls sig storage
func (bs BlsSigsState) CreateBlsSig(sig *types.BlsSig) {
epoch := sig.GetEpochNum()
sigHash := sig.Hash()
blsSigsKey := types.BlsSigsObjectKey(epoch, sigHash)
epochKey := types.BlsSigsEpochKey(sigHash)

// save concrete bls sig object
bs.blsSigs.Set(blsSigsKey, types.BlsSigToBytes(bs.cdc, sig))
// map bls sig to epoch
bs.hashToEpoch.Set(epochKey, sdk.Uint64ToBigEndian(epoch))
}

// GetBlsSigsByEpoch retrieves bls sigs by their epoch
func (bs BlsSigsState) GetBlsSigsByEpoch(epoch uint64, f func(sig *types.BlsSig) bool) error {
store := prefix.NewStore(bs.blsSigs, sdk.Uint64ToBigEndian(epoch))
iter := store.Iterator(nil, nil)
defer iter.Close()

for ; iter.Valid(); iter.Next() {
rawBytes := iter.Value()
blsSig, err := types.BytesToBlsSig(bs.cdc, rawBytes)
if err != nil {
return err
}
stop := f(blsSig)
if stop {
return nil
}
}
return nil
}

// Exists Check whether a bls sig is maintained in storage
func (bs BlsSigsState) Exists(hash types.BlsSigHash) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question: if this is for duplicate checking, don't we have the pre-image of the hash at hand?

Copy link
Contributor Author

@gitferry gitferry Jun 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can use this method to prevent adding duplicate bls sigs in the mempool, can't we?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we could use it for that. What I meant was that you would have a full blown signature in that case, so you can just pass it as-is, and calculate its storage key. Nobody has just the hash, and not the full signature.

Whether we should check this duplicate before the mempool: I'm not sure that's necessary, if the validators only send their signatures once, everyone their own, they would not be duplicated, because the mempool would reject them as identical transactions.

If they actually kept sending their signatures multiple times in different transactions, then we could charge them for it to discourage this behaviour, but only if we let the transactions into the mempool.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense. Thanks!

store := prefix.NewStore(bs.hashToEpoch, types.BlsSigsHashToEpochPrefix)
return store.Has(hash.Bytes())
}
88 changes: 88 additions & 0 deletions x/checkpointing/keeper/ckpt_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package keeper

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

type CheckpointsState struct {
cdc codec.BinaryCodec
checkpoints sdk.KVStore
}

func (k Keeper) CheckpointsState(ctx sdk.Context) CheckpointsState {
// Build the CheckpointsState storage
store := ctx.KVStore(k.storeKey)
return CheckpointsState{
cdc: k.cdc,
checkpoints: prefix.NewStore(store, types.CheckpointsPrefix),
}
}

// CreateRawCkptWithMeta inserts the raw checkpoint with meta into the storage by its epoch number
// a new checkpoint is created with the status of UNCEHCKPOINTED
func (cs CheckpointsState) CreateRawCkptWithMeta(ckpt *types.RawCheckpoint) {
// save concrete ckpt object
ckptWithMeta := types.NewCheckpointWithMeta(ckpt, types.Uncheckpointed)
cs.checkpoints.Set(types.CkptsObjectKey(ckpt.EpochNum), types.CkptWithMetaToBytes(cs.cdc, ckptWithMeta))
Comment on lines +29 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a good idea to return an error if this record already exists, just in case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or even to panic, as it would be a gross programming error in our case.

}

// GetRawCkptWithMeta retrieves a raw checkpoint with meta by its epoch number
func (cs CheckpointsState) GetRawCkptWithMeta(epoch uint64) (*types.RawCheckpointWithMeta, error) {
ckptsKey := types.CkptsObjectKey(epoch)
rawBytes := cs.checkpoints.Get(ckptsKey)
if rawBytes == nil {
return nil, types.ErrCkptDoesNotExist.Wrap("no raw checkpoint with provided epoch")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Wrap doesn't seem to provide any extra info beyond what the ErrCkptDoesNotExist already says.

}

return types.BytesToCkptWithMeta(cs.cdc, rawBytes)
}

// GetRawCkptsWithMetaByStatus retrieves raw checkpoints with meta by their status by the descending order of epoch
func (cs CheckpointsState) GetRawCkptsWithMetaByStatus(status types.CheckpointStatus, f func(sig *types.RawCheckpointWithMeta) bool) error {
store := prefix.NewStore(cs.checkpoints, types.CkptsObjectPrefix)
iter := store.ReverseIterator(nil, nil)
defer iter.Close()

// the iterator starts from the highest epoch number
// once it gets to an epoch where the status is CONFIRMED,
// all the lower epochs will be CONFIRMED
for ; iter.Valid(); iter.Next() {
ckptBytes := iter.Value()
ckptWithMeta, err := types.BytesToCkptWithMeta(cs.cdc, ckptBytes)
if err != nil {
return err
}
// the loop can end if the current status is CONFIRMED but the requested status is not CONFIRMED
if status != types.Confirmed && ckptWithMeta.Status == types.Confirmed {
return nil
}
if ckptWithMeta.Status != status {
continue
}
stop := f(ckptWithMeta)
if stop {
return nil
}
}
return nil
}

// UpdateCkptStatus updates the checkpoint's status
func (cs CheckpointsState) UpdateCkptStatus(ckpt *types.RawCheckpoint, status types.CheckpointStatus) error {
ckptWithMeta, err := cs.GetRawCkptWithMeta(ckpt.EpochNum)
if err != nil {
// the checkpoint should exist
return err
}
if !ckptWithMeta.Ckpt.Hash().Equals(ckpt.Hash()) {
return errors.New("hash not the same with existing checkpoint")
}
ckptWithMeta.Status = status
cs.checkpoints.Set(sdk.Uint64ToBigEndian(ckpt.EpochNum), types.CkptWithMetaToBytes(cs.cdc, ckptWithMeta))

return nil
}
66 changes: 58 additions & 8 deletions x/checkpointing/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ import (

type (
Keeper struct {
cdc codec.BinaryCodec
storeKey sdk.StoreKey
memKey sdk.StoreKey
paramstore paramtypes.Subspace
cdc codec.BinaryCodec
storeKey sdk.StoreKey
memKey sdk.StoreKey
stakingKeeper types.StakingKeeper
epochingKeeper types.EpochingKeeper
paramstore paramtypes.Subspace
}
)

func NewKeeper(
cdc codec.BinaryCodec,
storeKey,
memKey sdk.StoreKey,
stakingKeeper types.StakingKeeper,
epochingKeeper types.EpochingKeeper,
ps paramtypes.Subspace,
) Keeper {
// set KeyTable if it has not already been set
Expand All @@ -32,13 +36,59 @@ func NewKeeper(
}

return Keeper{
cdc: cdc,
storeKey: storeKey,
memKey: memKey,
paramstore: ps,
cdc: cdc,
storeKey: storeKey,
memKey: memKey,
stakingKeeper: stakingKeeper,
epochingKeeper: epochingKeeper,
paramstore: ps,
}
}

func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}

// AddBlsSig add bls signatures into storage and generates a raw checkpoint
// if sufficient sigs are accumulated for a specific epoch
func (k Keeper) AddBlsSig(ctx sdk.Context, sig *types.BlsSig) error {
// TODO: some checks: 1. duplication check 2. epoch check 3. raw ckpt existence check
// TODO: aggregate bls sigs and try to build raw checkpoints
k.BlsSigsState(ctx).CreateBlsSig(sig)
Comment on lines +50 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to keep this design of storing lists of BLS checkpoints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I don't have strong opinions on either way we should choose. I just didn't see any projects that aggregate sigs one by one. I would love to hear it if you have any argument against this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Against what you are doing? My only counter argument would be not to have to maintain and test code to store and retrieve this. The on-the-fly aggregation can be done all in memory by amending a checkpoint record. Perhaps it can even have more statuses, like "accumulation", starting with an empty signature. It's unique key is still the epoch, so it could be:

func (k *Keeper) addBlsSig(sig *BlsSig) error {
  let (ckpt, err) := k.GetEpochCheckpoint(sig.EpochNumber())
  if err != nil {
    return err
  }
  validators := k.GetEpochValidators(ckpt.EpochNumber())

  if err = ckpt.Accumulate(validators, sig), err != nil {
    return err
  }

  k.saveEpochCheckpoint(ckpt)

  return nil
}

And we can put all logic into into Accumulate: status checking, status changes, validator eligibility checks, etc.

Copy link
Contributor

@aakoshh aakoshh Jun 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like

struct CheckpointMeta {
  RawCheckpoint RawCheckpoint,
  Status CheckpointStatus
  TotalPower Power
}

func (ckpt *CheckpointMeta) Accumulate(validators []ValidatorWithBlsKeyAndPower, sig BlsSig) error {
  if ckpt.Status != CKPT_ACCUMULATING {
    return CheckpointNoLongerAccumulating
  }
  validator, index := findValidator(validators, sig.BlsPublicKey)
  if validator == nil {
    return UneligibleValidator
  }
  if !IsValidSignature(sig, ckpt.RawCheckpoint) {
    return InvalidSignature
  }
  if ckpt.Bitmap[index] {
    return AlreadyVoted
  }
  ckpt.TotalPower += validator.Power
  ckpt.Signature  = BlsAggregate(ckpt.Signature, bls.Signature)
  ckpt.Bitmap[index] = true
  if ckpt.TotalPower > totalPower(validators) / 3 {
    ckpt.Status = CKPT_UNCHECKPOINTED
  }
  return nil
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks neat. Thanks, @aakoshh! I agree that avoiding storing bls sigs separately is a big gain of on-the-fly aggregation. Since it includes bls aggregation and verification, I'll do this in a future PR.

return nil
}

// AddRawCheckpoint adds a raw checkpoint into the storage
// this API may not needed since checkpoints are generated internally
func (k Keeper) AddRawCheckpoint(ctx sdk.Context, ckpt *types.RawCheckpoint) {
// NOTE: may remove this API
k.CheckpointsState(ctx).CreateRawCkptWithMeta(ckpt)
}

// CheckpointEpoch verifies checkpoint from BTC and returns epoch number
func (k Keeper) CheckpointEpoch(ctx sdk.Context, rawCkptBytes []byte) (uint64, error) {
ckpt, err := types.BytesToRawCkpt(k.cdc, rawCkptBytes)
if err != nil {
return 0, err
}
err = k.verifyRawCheckpoint(ckpt)
if err != nil {
return 0, err
}
return ckpt.EpochNum, nil
}

func (k Keeper) verifyRawCheckpoint(ckpt *types.RawCheckpoint) error {
// TODO: verify checkpoint basic and bls multi-sig
return nil
}

// UpdateCkptStatus updates the status of a raw checkpoint
func (k Keeper) UpdateCkptStatus(ctx sdk.Context, rawCkptBytes []byte, status types.CheckpointStatus) error {
// TODO: some checks
ckpt, err := types.BytesToRawCkpt(k.cdc, rawCkptBytes)
if err != nil {
return err
}
return k.CheckpointsState(ctx).UpdateCkptStatus(ckpt, status)
}
39 changes: 39 additions & 0 deletions x/checkpointing/types/blssig_set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package types

import (
"github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/tendermint/tendermint/libs/bits"
"github.com/tendermint/tendermint/libs/bytes"
)

type BlsSigSet struct {
epoch uint16
lastCommitHash bytes.HexBytes
validators types.Validators

sum uint64
sigsBitArray *bits.BitArray
}

// NewBlsSigSet constructs a new BlsSigSet struct used to accumulate bls sigs for a given epoch
func NewBlsSigSet(epoch uint16, lastCommitHash bytes.HexBytes, validators types.Validators) *BlsSigSet {
return &BlsSigSet{
epoch: epoch,
lastCommitHash: lastCommitHash,
validators: validators,
sum: 0,
sigsBitArray: bits.NewBitArray(validators.Len()),
}
}

func (bs *BlsSigSet) AddBlsSig(sig *BlsSig) (bool, error) {
return bs.addBlsSig(sig)
}

func (bs *BlsSigSet) addBlsSig(sig *BlsSig) (bool, error) {
panic("implement this!")
}

func (bs *BlsSigSet) MakeRawCheckpoint() *RawCheckpoint {
panic("implement this!")
}
Loading