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

feat: define checkpointing registration proto and state #46

Merged
merged 7 commits into from
Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
25 changes: 25 additions & 0 deletions proto/babylon/checkpointing/bls_key.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
syntax = "proto3";
package babylon.checkpointing.v1;

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

// BlsPubKey wraps BLS public key with meta data
message BlsPubKey {
// key is the BLS public key of a validator
bytes key = 1;
aakoshh marked this conversation as resolved.
Show resolved Hide resolved

// address is validator's address
string address = 2;
}

// ProofOfPossession defines proof for the ownership of Ed25519 and BLS private keys
message ProofOfPossession {
// ed25519_pk is the Ed25519 public key of the validator
bytes ed25519_pk = 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this in the message, or can we infer it from the transaction? Do validators sign transactions with their Ed25519 keys? I'm not sure now that @SebastianElvis pointed me at some Secp256k1 documentation as well.

Copy link
Member

Choose a reason for hiding this comment

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

The validator type includes the validator operator (i.e., the user)'s address (which has to be associated with a Secp256{k/r}1 key pair) and the validator's PK (which has to be associated with an Ed25519 key pair). Here perhaps we can find out the validator's Ed25519 PK from the CreateValidator message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In my understanding, the transaction is signed by Secp256 instead of Ed25519. Ed25519 is only for consensus messages. I agree with @SebastianElvis here that the ed25519_pk is included in the MsgCreateValidator that is wrapped in the MsgWrappedCreateValidator, so I maybe can remove it from PoP.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a good idea too.

So, is this what's happening?

  • The user has an account with a Secp256k1 key. They want to become a validator
  • The user creates a staking message cthat ontains this Secp256k1 key (hopefully enforced!) and an Ed25519 validator key
  • We wrap this message
  • The user attaches a BLS public key to the message
  • The user attaches a PoP to the message
  • The user signs the transaction with Secp256k1

And if so, would the following be a correct PoP:

PoP =  encrypt(key = BLS_sk, data = encrypt(key = Ed25519_sk, data = Secp256k1_pk))
msg = { BLS_pk, Ed25519_pk, PoP }
sig = sign(key = Secp256k1_sk, data = msg)
tx = { Secp256k1_pk, msg, sig }

check(tx) = 
  validate(key = tx.Secp256k1_pk, sig = tx.sig, data = tx.msg) &&
  tx.Secp256k1_pk == decrypt(key = tx.msg.Ed25519_pk, data = decrypt(key = tx.msg.BLS_pk, data = tx.msg.PoP))

Notably I replaced the innermost message with the user key, rather than the Ed25519 of the validator, so that another user cannot just take the PoP and try to register with the same keys, because they would not be able to produce a valid PoP without having access to the *_sk private keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe this could work. Thanks! I'll use this version of PoP then and update the spec accordingly.


// bls_sig is the BLS signature over ed25519_pk
bytes bls_sig = 2;

// ed25519_sig is the Ed25519 signature over bls_sig
bytes ed25519_sig = 3;
aakoshh marked this conversation as resolved.
Show resolved Hide resolved
}
19 changes: 19 additions & 0 deletions proto/babylon/checkpointing/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ package babylon.checkpointing.v1;

import "gogoproto/gogo.proto";
import "babylon/checkpointing/checkpoint.proto";
import "babylon/checkpointing/bls_key.proto";
import "cosmos/staking/v1beta1/tx.proto";

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

// Msg defines the checkpointing Msg service.
service Msg {
// AddBlsSig defines a method for accumulating BLS signatures
rpc AddBlsSig(MsgAddBlsSig) returns (MsgAddBlsSigResponse);

// WrappedCreateValidator defines a method for registering a new validator
rpc WrappedCreateValidator(MsgWrappedCreateValidator) returns (MsgWrappedCreateValidatorResponse);
}

// MsgAddBlsSig defines a message to add a bls signature from a
Expand All @@ -22,3 +28,16 @@ message MsgAddBlsSig {

// MsgAddBlsSigResponse defines the MsgAddBlsSig response type.
message MsgAddBlsSigResponse {}

// MsgWrappedCreateValidator defines a wrapped message to create a validator
message MsgWrappedCreateValidator {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

BlsPubKey pubkey = 1;
ProofOfPossession pop = 2;
cosmos.staking.v1beta1.MsgCreateValidator msg_staking = 3;
}

// MsgWrappedCreateValidatorResponse defines the MsgWrappedCreateValidator response type
message MsgWrappedCreateValidatorResponse {}
10 changes: 5 additions & 5 deletions x/checkpointing/keeper/blssig_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,25 @@ func (k Keeper) BlsSigsState(ctx sdk.Context) BlsSigsState {
store := ctx.KVStore(k.storeKey)
return BlsSigsState{
cdc: k.cdc,
blsSigs: prefix.NewStore(store, types.BlsSigsPrefix),
blsSigs: prefix.NewStore(store, types.BlsSigsObjectPrefix),
hashToEpoch: prefix.NewStore(store, types.BlsSigsHashToEpochPrefix),
}
}

// CreateBlsSig inserts the bls sig into the hash->epoch and (epoch, hash)->bls sig storage
// 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
// 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
// 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)
Expand All @@ -56,7 +56,7 @@ func (bs BlsSigsState) GetBlsSigsByEpoch(epoch uint64, f func(sig *types.BlsSig)
return nil
}

// Exists Check whether a bls sig is maintained in storage
// Exists Checks whether a BLS sig is maintained in storage
func (bs BlsSigsState) Exists(hash types.BlsSigHash) bool {
store := prefix.NewStore(bs.hashToEpoch, types.BlsSigsHashToEpochPrefix)
return store.Has(hash.Bytes())
Expand Down
6 changes: 5 additions & 1 deletion x/checkpointing/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package keeper

import (
"fmt"

stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/tendermint/tendermint/libs/log"

"github.com/babylonchain/babylon/x/checkpointing/types"
Expand Down Expand Up @@ -99,3 +99,7 @@ func (k Keeper) UpdateCkptStatus(ctx sdk.Context, rawCkptBytes []byte, status ty
}
return k.CheckpointsState(ctx).UpdateCkptStatus(ckpt, status)
}

func (k Keeper) CreateRegistration(ctx sdk.Context, blsPubKey *types.BlsPubKey, msg *stakingtypes.MsgCreateValidator) error {
return k.RegistrationState(ctx).CreateRegistration(blsPubKey, msg)
}
23 changes: 18 additions & 5 deletions x/checkpointing/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"context"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/babylonchain/babylon/x/checkpointing/types"
)
Expand All @@ -10,15 +11,27 @@ type msgServer struct {
k Keeper
}

func (m msgServer) AddBlsSig(ctx context.Context, header *types.MsgAddBlsSig) (*types.MsgAddBlsSigResponse, error) {
//TODO implement me
panic("implement me")
}

// NewMsgServerImpl returns an implementation of the MsgServer interface
// for the provided Keeper.
func NewMsgServerImpl(keeper Keeper) types.MsgServer {
return &msgServer{keeper}
}

var _ types.MsgServer = msgServer{}

func (m msgServer) AddBlsSig(goCtx context.Context, header *types.MsgAddBlsSig) (*types.MsgAddBlsSigResponse, error) {
panic("TODO: implement me")
}

// WrappedCreateValidator stores validator's BLS public key as well as corresponding MsgCreateValidator message
func (m msgServer) WrappedCreateValidator(goCtx context.Context, msg *types.MsgWrappedCreateValidator) (*types.MsgWrappedCreateValidatorResponse, error) {
// TODO: verify pop
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the same as this

If the PoP was validated in ValidateBasic, would it have to be done here as well?

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 is a very good question. It makes more sense that all the stateless verification is done in ValidateBasic.
ValidateBasic is called before the message is handed to the handler.

I'll worry about this in a future PR. Thanks for the reminder though.

ctx := sdk.UnwrapSDKContext(goCtx)

err := m.k.CreateRegistration(ctx, msg.Pubkey, msg.MsgStaking)
if err != nil {
return nil, err
}

return &types.MsgWrappedCreateValidatorResponse{}, nil
}
76 changes: 76 additions & 0 deletions x/checkpointing/keeper/registration_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

type RegistrationState struct {
cdc codec.BinaryCodec
blsKeys sdk.KVStore
msgCreateValidators sdk.KVStore
}

func (k Keeper) RegistrationState(ctx sdk.Context) RegistrationState {
// Build the RegistrationState storage
store := ctx.KVStore(k.storeKey)
return RegistrationState{
cdc: k.cdc,
blsKeys: prefix.NewStore(store, types.BlsKeysObjectPrefix),
msgCreateValidators: prefix.NewStore(store, types.MsgCreateValidatorsPrefix),
}
}

// CreateRegistration inserts the BLS key as well as a corresponding MsgCreateValidator message into the storage
func (rs RegistrationState) CreateRegistration(key *types.BlsPubKey, msg *stakingtypes.MsgCreateValidator) error {
if rs.Exists(key.Address) {
return types.ErrBlsKeyAlreadyExist.Wrapf("existed public key: %x", key.Key)
aakoshh marked this conversation as resolved.
Show resolved Hide resolved
}

aakoshh marked this conversation as resolved.
Show resolved Hide resolved
blsKeysKey := types.BlsKeysObjectKey(key.Address)
msgKey := types.MsgCreateValidatorsKey(key.Address)

// save concrete BLS public key object and msgCreateValidator
rs.blsKeys.Set(blsKeysKey, types.BlsPubKeyToBytes(rs.cdc, key))
rs.msgCreateValidators.Set(msgKey, rs.cdc.MustMarshal(msg))
aakoshh marked this conversation as resolved.
Show resolved Hide resolved

return nil
}

// GetBlsPubKey retrieves BLS public key by validator's address
func (rs RegistrationState) GetBlsPubKey(addr string) (*types.BlsPubKey, error) {
aakoshh marked this conversation as resolved.
Show resolved Hide resolved
pubKeyKey := types.BlsKeysObjectKey(addr)
rawBytes := rs.blsKeys.Get(pubKeyKey)
if rawBytes == nil {
return nil, types.ErrBlsKeyDoesNotExist.Wrapf("BLS public key does not exist with address %s", addr)
}

return types.BytesToBlsPubKey(rs.cdc, rawBytes)
}

// RemoveBlsPubKey removes a BLS public key
func (rs RegistrationState) RemoveBlsPubKey(addr string) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this? As I understand you possibly wanted this to happen if the delayed transaction fails, but:

  • the epoching module doesn't know it has to do a callback with the results
  • what if it was some kind of a duplicate and the initial registration didn't fail, in which case we might remove the BLS key but leave the validator staked

Although your model of cleaning up after failed registrations is nice too. It would for sure need a callback mechanism. @SebastianElvis what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. Adding this cleaning mechanism probably will not work since we don't know whether the staking message is executed successfully. We should allow the users to retry registration. One note is that a valid bls-sig should be from a validator that is staked (only registering BLS key is not enough).

Copy link
Contributor

Choose a reason for hiding this comment

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

Well there's your 🐔 🥚 problem:

  • the validator will be registered by the delayed mechanism
  • a validator without a BLS key is useless to us, they can't sign a checkpoint
  • a BLS key without a registered validator just takes up space, but we can ignore it completely

So while it's not enough to have a BLS key, it's also not enough to have a registered validator without one, but while the former is benign, the latter will be able to propose Tendermint blocks.

Ideally we'd do the two together, but as discussed doing it up front allows us to reject at least some of it with a clear error message associated with the transaction, if there's a problem. You could check the current account balance for example, or even take it into a holding pool, if you are really fancy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just realized that we should keep this method because when a validator is removed by the staking module, we should remove the corresponding BLS key, too. The staking module has a hook called AfterValidatorRemoved. Maybe we should implement this hook by calling RemoveBlsPubKey()

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe they mean it as in the validator is removed from the active validator set, but they are not entirely forgotten, at least not by Tendermint: https://docs.tendermint.com/master/rpc/#/Info/validators

if !rs.Exists(addr) {
return types.ErrBlsKeyDoesNotExist.Wrapf("BLS public key does not exist with address %s", addr)
}

// delete BLS public key and corresponding msgCreateValidator from storage
rs.blsKeys.Delete(types.BlsKeysObjectKey(addr))
rs.msgCreateValidators.Delete(types.MsgCreateValidatorsKey(addr))

return nil
}

// RemoveMsgCreateValidator removes a MsgCreateValidator
func (rs RegistrationState) RemoveMsgCreateValidator(addr string) {
rs.msgCreateValidators.Delete(types.MsgCreateValidatorsKey(addr))
}

// Exists checks whether a BLS key exists
func (rs RegistrationState) Exists(addr string) bool {
blsKeysKey := types.BlsKeysObjectKey(addr)
return rs.blsKeys.Has(blsKeysKey)
}
Loading