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(operator): load genesis state #27

Merged
merged 10 commits into from
Apr 9, 2024
91 changes: 91 additions & 0 deletions proto/exocore/operator/v1/genesis.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
syntax = "proto3";
package exocore.operator.v1;

import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";

import "exocore/operator/v1/tx.proto";

option go_package = "github.com/ExocoreNetwork/exocore/x/operator/types";

// GenesisState defines the operator module's genesis state.
message GenesisState {
// there are no params for this module.
// operators is a list of the registered operators.
repeated OperatorInfo operators = 1 [(gogoproto.nullable) = false];
// operator_records refers to a list of operator records. each record
// contains an operator address and a list of chain id +
// cons key combination.
repeated OperatorConsKeyRecord operator_records = 2
[(gogoproto.nullable) = false];
// TODO: add other AVS opt-in information for exporting / importing.
// Although it is not necessary for the bootstrapped genesis, it is
// necessary for chain restarts.
}

// OperatorConsKeyRecord is a helper structure for the genesis state. Each record
// contains an operator address and a list of chain id + cons key combination.
message OperatorConsKeyRecord {
// operator_address is the address of the operator as the bech32
// encoded version of sdk.AccAddress.
string operator_address = 1;
// chains is a list of chain id + consensus key combination.
repeated ChainDetails chains = 2 [(gogoproto.nullable) = false];
}

// ChainDetails is a helper structure for the genesis state. Each record
// contains a chain id and a consensus key.
message ChainDetails {
// chain_id is the unique identifier of the chain.
string chain_id = 1 [(gogoproto.customname) = "ChainID"];
// consensus_key is the consensus key of the operator on the chain.
// the length of this key should be exactly 32 bytes, and must be enforced
// outside of protobuf.
string consensus_key = 2;
}

// all operators in the genesis (during bootstrap) are assumed to have
// opted into validating Exocore. however, we still need to set their opt-in
// data. we can do this by calling k.OptIn(ctx, sdk.AccAddress, ctx.ChainID())

// this will then allow us to call
// k.UpdateOptedInAssetsState(ctx, staker, assetID, operator, stakedValue)
// for now, we keep this data in the genesis as the order stored, but
// it would be trivial to alter the order if deemed necessary.
// this relies in GetSpecifiedAssetsPrice, GetStakingAssetInfo, GetAvsSupportedAssets
// the first and third need to be set up and done before this genesis.
// the second is already set up before this genesis.

// StakerRecord is a helper structure for the genesis state. Each record
// contains a staker address and a list of asset IDs with their operator +
// amount combination.
message StakerRecord {
// staker_id denotes the address + l0id of the staker.
string staker_id = 1 [(gogoproto.customname) = "StakerID"];
// staker_details is a list of asset ID + operator + amount combination.
repeated StakerDetails staker_details = 2 [(gogoproto.nullable) = false];
}

// StakerDetails is a helper structure for the genesis state. Each record
// contains an asset ID and a list of operator + amount combination.
message StakerDetails {
// asset_id is the unique identifier of the asset.
string asset_id = 1 [(gogoproto.customname) = "AssetID"];
// details is a list of operator + amount combination.
repeated AssetDetails details = 2 [(gogoproto.nullable) = false];
}

// AssetDetails is a helper structure for the genesis state. Each record
// contains an operator and an amount.
message AssetDetails {
// operator_address is the address of the operator as the bech32
// version of sdk.AccAddress.
string operator_address = 1;
// amount is the amount of the asset staked by the staker for this
// asset and operator.
string amount = 2 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
}
95 changes: 56 additions & 39 deletions x/operator/keeper/consensus_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,43 +24,56 @@ func (k *Keeper) Logger(ctx sdk.Context) log.Logger {
// and chain id. By doing this, an operator is consenting to be an operator on the given chain.
// If a key already exists, it will be overwritten and the change in voting power will flow
// through to the validator set.
// TODO: Rationalize with k.OptIn
func (k *Keeper) SetOperatorConsKeyForChainID(
ctx sdk.Context,
opAccAddr sdk.AccAddress,
chainID string,
// should be tm-ed25519
consKey tmprotocrypto.PublicKey,
consKey *tmprotocrypto.PublicKey,
) error {
return k.setOperatorConsKeyForChainID(ctx, opAccAddr, chainID, consKey, false)
}

// setOperatorConsKeyForChainID is the private version of SetOperatorConsKeyForChainID.
// it is used with a boolean flag to indicate that the call is from genesis.
// if so, operator freeze status is not checked and hooks are not called.
func (k *Keeper) setOperatorConsKeyForChainID(
ctx sdk.Context,
opAccAddr sdk.AccAddress,
chainID string,
consKey *tmprotocrypto.PublicKey,
genesis bool,
) error {
// check if we are an operator
if !k.IsOperator(ctx, opAccAddr) {
return delegationtypes.ErrOperatorNotExist
}
// check for slashing
if k.slashKeeper.IsOperatorFrozen(ctx, opAccAddr) {
if !genesis && k.slashKeeper.IsOperatorFrozen(ctx, opAccAddr) {
return delegationtypes.ErrOperatorIsFrozen
}
// check if the chain id is valid
if !k.assetsKeeper.AppChainInfoIsExist(ctx, chainID) {
return assetstypes.ErrUnknownAppChainID
}
// if opting out, do not allow key replacement
// TODO: merge with the functionality in state_update.go
if k.IsOperatorOptingOutFromChainID(ctx, opAccAddr, chainID) {
return types.ErrAlreadyOptingOut
}
// convert to bytes
bz, err := consKey.Marshal()
if err != nil {
return errorsmod.Wrap(
err,
"SetOperatorConsKeyForChainID: error occurred when marshal public key",
err, "SetOperatorConsKeyForChainID: cannot marshal public key",
)
}
// convert to address for reverse lookup
consAddr, err := types.TMCryptoPublicKeyToConsAddr(consKey)
if err != nil {
return errorsmod.Wrap(
err,
"SetOperatorConsKeyForChainID: error occurred when convert public key to consensus address",
err, "SetOperatorConsKeyForChainID: cannot convert pub key to consensus address",
)
}
// check if the key is already in use by another operator
Expand Down Expand Up @@ -94,24 +107,34 @@ func (k *Keeper) SetOperatorConsKeyForChainID(
panic(err)
}
if !alreadyRecorded {
if err := k.setOperatorPrevConsKeyForChainID(ctx, opAccAddr, chainID, prevKey); err != nil {
if err := k.setOperatorPrevConsKeyForChainID(
ctx, opAccAddr, chainID, prevKey,
); err != nil {
// this should not happen
panic(err)
}
}
}
// k.setOperatorConsKeyForChainID(ctx, opAccAddr, chainID, bz)
// return nil
// }
k.setOperatorConsKeyForChainIDUnchecked(ctx, opAccAddr, consAddr, chainID, bz)
if !genesis {
if found {
if !alreadyRecorded {
k.Hooks().AfterOperatorKeyReplacement(ctx, opAccAddr, prevKey, consKey, chainID)
}
} else {
k.Hooks().AfterOperatorOptIn(ctx, opAccAddr, chainID, consKey)
}
}
return nil
}

// // setOperatorConsKeyForChainID is the internal private version. It performs
// // no error checking of the input.
// func (k Keeper) setOperatorConsKeyForChainID(
// ctx sdk.Context,
// opAccAddr sdk.AccAddress,
// chainID string,
// bz []byte,
// ) {
// setOperatorConsKeyForChainIDUnchecked is the internal private version. It performs
// no error checking of the input. The caller must do the error checking
// and then call this function.
func (k Keeper) setOperatorConsKeyForChainIDUnchecked(
ctx sdk.Context, opAccAddr sdk.AccAddress, consAddr sdk.ConsAddress,
chainID string, bz []byte,
) {
store := ctx.KVStore(k.storeKey)
// forward lookup
// given operator address and chain id, find the consensus key,
Expand All @@ -132,14 +155,6 @@ func (k *Keeper) SetOperatorConsKeyForChainID(
// this pruning will be triggered by the app chain module and will not be
// recorded here.
store.Set(types.KeyForChainIDAndConsKeyToOperator(chainID, consAddr), opAccAddr.Bytes())
if found {
if !alreadyRecorded {
k.Hooks().AfterOperatorKeyReplacement(ctx, opAccAddr, prevKey, consKey, chainID)
}
} else {
k.Hooks().AfterOperatorOptIn(ctx, opAccAddr, chainID, consKey)
}
return nil
}

// setOperatorPrevConsKeyForChainID sets the previous (consensus) public key for the given
Expand All @@ -150,7 +165,7 @@ func (k *Keeper) setOperatorPrevConsKeyForChainID(
ctx sdk.Context,
opAccAddr sdk.AccAddress,
chainID string,
prevKey tmprotocrypto.PublicKey,
prevKey *tmprotocrypto.PublicKey,
) error {
bz, err := prevKey.Marshal()
if err != nil {
Expand All @@ -169,7 +184,7 @@ func (k *Keeper) setOperatorPrevConsKeyForChainID(
// to 0 in the validator update.
func (k *Keeper) GetOperatorPrevConsKeyForChainID(
ctx sdk.Context, opAccAddr sdk.AccAddress, chainID string,
) (found bool, key tmprotocrypto.PublicKey, err error) {
) (found bool, key *tmprotocrypto.PublicKey, err error) {
// check if we are an operator
if !k.IsOperator(ctx, opAccAddr) {
err = delegationtypes.ErrOperatorNotExist
Expand All @@ -184,19 +199,19 @@ func (k *Keeper) GetOperatorPrevConsKeyForChainID(
return
}

// getOperatorPrevConsKeyForChainID is the internal version of
// GetOperatorPrevConsKeyForChainID.
// getOperatorPrevConsKeyForChainID is the internal version of GetOperatorPrevConsKeyForChainID.
// It performs no error checking of the input.
func (k *Keeper) getOperatorPrevConsKeyForChainID(
ctx sdk.Context,
opAccAddr sdk.AccAddress,
chainID string,
) (found bool, key tmprotocrypto.PublicKey, err error) {
) (found bool, key *tmprotocrypto.PublicKey, err error) {
store := ctx.KVStore(k.storeKey)
res := store.Get(types.KeyForOperatorAndChainIDToPrevConsKey(opAccAddr, chainID))
if res == nil {
return
}
key = &tmprotocrypto.PublicKey{}
if err = key.Unmarshal(res); err != nil {
return
}
Expand All @@ -210,7 +225,7 @@ func (k *Keeper) GetOperatorConsKeyForChainID(
ctx sdk.Context,
opAccAddr sdk.AccAddress,
chainID string,
) (found bool, key tmprotocrypto.PublicKey, err error) {
) (found bool, key *tmprotocrypto.PublicKey, err error) {
// check if we are an operator
if !k.IsOperator(ctx, opAccAddr) {
err = delegationtypes.ErrOperatorNotExist
Expand All @@ -232,25 +247,27 @@ func (k *Keeper) getOperatorConsKeyForChainID(
ctx sdk.Context,
opAccAddr sdk.AccAddress,
chainID string,
) (found bool, key tmprotocrypto.PublicKey, err error) {
) (found bool, key *tmprotocrypto.PublicKey, err error) {
store := ctx.KVStore(k.storeKey)
res := store.Get(types.KeyForOperatorAndChainIDToConsKey(opAccAddr, chainID))
if res == nil {
return
}
key = &tmprotocrypto.PublicKey{}
if err = key.Unmarshal(res); err != nil {
return
}
return true, key, nil
}

// GetChainIDsAndKeysForOperator gets the chain ids for which the given operator address has set a
// GetChainIDsAndKeysForOperator gets the chain ids for which the given operator address has set
// a
// (consensus) public key. TODO: would it be better to make this a key per operator?
// This is intentionally an array of strings because I don't see the utility for the vote power
// or the public key here. If we need it, we can add it later.
func (k *Keeper) GetChainIDsAndKeysForOperator(
ctx sdk.Context, opAccAddr sdk.AccAddress,
) (chainIDs []string, consKeys []tmprotocrypto.PublicKey) {
) (chainIDs []string, consKeys []*tmprotocrypto.PublicKey) {
// check if we are an operator
if !k.IsOperator(ctx, opAccAddr) {
return
Expand All @@ -268,7 +285,7 @@ func (k *Keeper) GetChainIDsAndKeysForOperator(
for ; iterator.Valid(); iterator.Next() {
// the key returned is the full key, with the prefix. drop the prefix and the length.
chainID := string(iterator.Key()[len(prefix)+8:])
var key tmprotocrypto.PublicKey
var key *tmprotocrypto.PublicKey
if err := key.Unmarshal(iterator.Value()); err != nil {
// grave error because we are the ones who stored this information in the first
// place
Expand All @@ -285,7 +302,7 @@ func (k *Keeper) GetChainIDsAndKeysForOperator(
// jailed or frozen operators.
func (k *Keeper) GetOperatorsForChainID(
ctx sdk.Context, chainID string,
) (addrs []sdk.AccAddress, pubKeys []tmprotocrypto.PublicKey) {
) (addrs []sdk.AccAddress, pubKeys []*tmprotocrypto.PublicKey) {
if !k.assetsKeeper.AppChainInfoIsExist(ctx, chainID) {
return nil, nil
}
Expand All @@ -306,7 +323,7 @@ func (k *Keeper) GetOperatorsForChainID(
// so just drop it and convert to sdk.AccAddress
addr := iterator.Key()[len(prefix):]
res := iterator.Value()
var ret tmprotocrypto.PublicKey
var ret *tmprotocrypto.PublicKey
if err := ret.Unmarshal(res); err != nil {
// grave error
panic(err)
Expand Down Expand Up @@ -426,6 +443,6 @@ func (k *Keeper) Jail(sdk.Context, sdk.ConsAddress, string) {}

func (k *Keeper) GetActiveOperatorsForChainID(
sdk.Context, string,
) ([]sdk.AccAddress, []tmprotocrypto.PublicKey) {
) ([]sdk.AccAddress, []*tmprotocrypto.PublicKey) {
return nil, nil
}
47 changes: 47 additions & 0 deletions x/operator/keeper/genesis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package keeper

import (
"github.com/ExocoreNetwork/exocore/x/operator/types"
abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

func (k Keeper) InitGenesis(ctx sdk.Context, state types.GenesisState) []abci.ValidatorUpdate {
for _, op := range state.Operators {
op := op
if err := k.SetOperatorInfo(ctx, op.EarningsAddr, &op); err != nil {
panic(err)
}
}
for _, record := range state.OperatorRecords {
addr := record.OperatorAddress
// #nosec G703 // already validated
operatorAddr, _ := sdk.AccAddressFromBech32(addr)
bootstrapping := false
for _, detail := range record.Chains {
// opt into the specified chain (TODO: avs address format)
if err := k.OptIn(ctx, operatorAddr, detail.ChainID); err != nil {
panic(err)
}
// #nosec G703 // already validated
key, _ := types.HexStringToPubKey(detail.ConsensusKey)
// then set pub key
if err := k.setOperatorConsKeyForChainID(
ctx, operatorAddr, detail.ChainID, key, true,
); err != nil {
panic(err)
}
bootstrapping = bootstrapping || ctx.ChainID() == detail.ChainID
}
if !bootstrapping {
// TODO: consider removing this check
panic("registered an operator but they aren't bootstrapping the current chain")
}
}
return []abci.ValidatorUpdate{}
}

func (Keeper) ExportGenesis(sdk.Context) *types.GenesisState {
// TODO
return types.DefaultGenesis()
}
2 changes: 1 addition & 1 deletion x/operator/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ func (k *Keeper) QueryOperatorConsKeyForChainID(
return nil, errors.New("no key assigned")
}
return &operatortypes.QueryOperatorConsKeyResponse{
PublicKey: key,
PublicKey: *key,
}, nil
}
Loading
Loading