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] implement the operator interfaces expected by dogfood #21

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ proto-download-deps:
git remote add origin "https://github.com/cosmos/cosmos-sdk.git" && \
git config core.sparseCheckout true && \
printf "proto\nthird_party\n" > .git/info/sparse-checkout && \
git pull origin main && \
git pull origin release/v0.47.x && \
rm -f ./proto/buf.* && \
mv ./proto/* ..
rm -rf "$(THIRD_PARTY_DIR)/cosmos_tmp"
Expand All @@ -441,7 +441,7 @@ proto-download-deps:
git remote add origin "https://github.com/cosmos/ibc-go.git" && \
git config core.sparseCheckout true && \
printf "proto\n" > .git/info/sparse-checkout && \
git pull origin main && \
git pull origin release/v7.2.x && \
rm -f ./proto/buf.* && \
mv ./proto/* ..
rm -rf "$(THIRD_PARTY_DIR)/ibc_tmp"
Expand All @@ -452,7 +452,8 @@ proto-download-deps:
git remote add origin "https://github.com/cosmos/cosmos-proto.git" && \
git config core.sparseCheckout true && \
printf "proto\n" > .git/info/sparse-checkout && \
git pull origin main && \
git fetch origin tags/v1.0.0-beta.3 && \
git checkout -b my_branch FETCH_HEAD && \
rm -f ./proto/buf.* && \
mv ./proto/* ..
rm -rf "$(THIRD_PARTY_DIR)/cosmos_proto_tmp"
Expand Down
5 changes: 5 additions & 0 deletions proto/exocore/operator/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package exocore.operator.v1;

import "amino/amino.proto";
import "cosmos/msg/v1/msg.proto";
import "cosmos/staking/v1beta1/staking.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";

Expand Down Expand Up @@ -45,6 +46,8 @@ message OperatorInfo {
string operator_meta_info = 3;
// client_chain_earning_addr_list is the client chain earning address list.
ClientChainEarningAddrList client_chain_earnings_addr = 4;
// commission defines the commission parameters.
cosmos.staking.v1beta1.Commission commission = 5 [(gogoproto.nullable) = false, (amino.dont_omitempty) = true];
}

// OptedInfo is the opted information about operator
Expand All @@ -55,6 +58,8 @@ message OptedInfo {
uint64 opted_in_height = 2;
// opted_out_height is the exocore block height at which the operator opted out
uint64 opted_out_height = 3;
// jailed defined whether the operator has been jailed from bonded status or not.
bool jailed = 4;
Copy link
Contributor

Choose a reason for hiding this comment

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

Design question: Should jailing be done on a per-AVS basis or globally? We consider two potential reasons for slashing: first being the downtime and second being malicious activity like double-signing.

If an operator is slashed for downtime, we should only jail them for that specific AVS.
If an operator is slashed for malicious activity, we should ban them on all AVS.

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.

Both the malicious activities and slash proportions are configured by the specific Avs. If we ban them on all AVS when an operator acts maliciously on one Avs, Is it a problem that low-level misconduct will cause a maximum punishment?
I noticed a tombstone mechanism to mitigate the impact of initially likely categories of non-malicious protocol faults in cosmos-sdk. So, I'm also unsure about the appropriate level for the slash mechanism in Exocore.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we ban them on all AVS when an operator acts maliciously on one Avs, Is it a problem that low-level misconduct will cause a maximum punishment?

Yes, as discussed, we should only jail temporarily for low level misconduct.

I noticed a tombstone mechanism to mitigate the impact of initially likely categories of non-malicious protocol faults in cosmos-sdk. So, I'm also unsure about the appropriate level for the slash mechanism in Exocore.

I might be misinterpreting this but I think they have two things:

  • Jailing, which is temporary or permanent. An operator is jailed for downtime and slashed a small amount. Unjailing is first on the protocol level (upon expiry of the jailing time), and then the operator must send a transaction indicating that they have fixed the misconfiguration that led to the slashing time.
  • Tombstoning, which is permanent (equivalent to freezing). An operator is tombstoned for double signing, slashed a larger amount and permanently jailed. The only "mitigation" offered by this mechanism is that a validator cannot be slashed twice if they double sign across multiple blocks, even if evidence is received for each of those blocks.

I observed that permanent jailing is equivalent to tombstoning (and the SDK even performs them together). So we can remove that feature. Either an operator is jailed temporarily on a specific AVS, or they are permanently blocked from them all. For ease of use, we can replace the word "tombstone" with "freeze" like in our proposed design.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this needs to be considered in the design and implementation of the slash module.

}

// OptedInAssetState is the state of opted-in asset
Expand Down
4 changes: 2 additions & 2 deletions x/dogfood/keeper/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ func (k Keeper) EndBlock(ctx sdk.Context) []abci.ValidatorUpdate {
prev := k.getKeyPowerMapping(ctx).List
res := make([]abci.ValidatorUpdate, 0, len(prev))
operators, keys := k.operatorKeeper.GetActiveOperatorsForChainID(ctx, ctx.ChainID())
powers, err := k.restakingKeeper.GetAvgDelegatedValue(
ctx, operators, k.GetAssetIDs(ctx), k.GetEpochIdentifier(ctx),
powers, err := k.operatorKeeper.GetAvgDelegatedValue(
ctx, operators, ctx.ChainID(), k.GetEpochIdentifier(ctx),
)
if err != nil {
return []abci.ValidatorUpdate{}
Expand Down
24 changes: 5 additions & 19 deletions x/dogfood/keeper/impl_sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,8 @@ func (k Keeper) ValidatorByConsAddr(
ctx sdk.Context,
addr sdk.ConsAddress,
) stakingtypes.ValidatorI {
found, accAddr := k.operatorKeeper.GetOperatorAddressForChainIDAndConsAddr(
ctx, ctx.ChainID(), addr,
)
if !found {
// replicate the behavior of the SDK's staking module; do not panic.
return nil
}
return stakingtypes.Validator{
Jailed: k.operatorKeeper.IsOperatorJailedForChainID(ctx, accAddr, ctx.ChainID()),
Jailed: k.operatorKeeper.IsOperatorJailedForChainID(ctx, addr, ctx.ChainID()),
}
}

Expand Down Expand Up @@ -104,7 +97,7 @@ func (k Keeper) SlashWithInfractionReason(
}
// TODO(mm): add list of assets to be slashed (and not just all of them).
// based on yet to be finalized slashing design.
return k.slashingKeeper.SlashWithInfractionReason(
return k.operatorKeeper.SlashWithInfractionReason(
ctx, accAddress, infractionHeight,
power, slashFactor, infraction,
bwhour marked this conversation as resolved.
Show resolved Hide resolved
)
Expand All @@ -124,8 +117,8 @@ func (k Keeper) Jail(ctx sdk.Context, addr sdk.ConsAddress) {
// The function is called by the slashing module only when it receives a request from the
// operator to do so. TODO(mm): We need to use the SDK's slashing module to allow for downtime
// slashing but somehow we need to prevent its Unjail function from being called by anyone.
func (k Keeper) Unjail(sdk.Context, sdk.ConsAddress) {
panic("unimplemented on this keeper")
func (k Keeper) Unjail(ctx sdk.Context, addr sdk.ConsAddress) {
k.operatorKeeper.Unjail(ctx, addr, ctx.ChainID())
}

// Delegation is an implementation of the staking interface expected by the SDK's slashing
Expand Down Expand Up @@ -155,14 +148,7 @@ func (k Keeper) GetAllValidators(sdk.Context) (validators []stakingtypes.Validat
// slashing module. It is called by the slashing module to record validator signatures
// for downtime tracking. We delegate the call to the operator keeper.
func (k Keeper) IsValidatorJailed(ctx sdk.Context, addr sdk.ConsAddress) bool {
found, accAddr := k.operatorKeeper.GetOperatorAddressForChainIDAndConsAddr(
ctx, ctx.ChainID(), addr,
)
if !found {
// replicate the behavior of the SDK's staking module
return false
}
return k.operatorKeeper.IsOperatorJailedForChainID(ctx, accAddr, ctx.ChainID())
return k.operatorKeeper.IsOperatorJailedForChainID(ctx, addr, ctx.ChainID())
}

// ApplyAndReturnValidatorSetUpdates is an implementation of the staking interface expected
Expand Down
23 changes: 11 additions & 12 deletions x/dogfood/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,22 @@ type OperatorKeeper interface {
GetOperatorAddressForChainIDAndConsAddr(
sdk.Context, string, sdk.ConsAddress,
) (bool, sdk.AccAddress)
IsOperatorJailedForChainID(sdk.Context, sdk.AccAddress, string) bool
IsOperatorJailedForChainID(sdk.Context, sdk.ConsAddress, string) bool
Jail(sdk.Context, sdk.ConsAddress, string)
Unjail(sdk.Context, sdk.ConsAddress, string)
// GetActiveOperatorsForChainID should return a list of operators and their public keys.
// These operators should not be in the process of opting our, and should not be jailed
// These operators should not be in the process of opting out, and should not be jailed
// whether permanently or temporarily.
GetActiveOperatorsForChainID(
sdk.Context, string,
) ([]sdk.AccAddress, []tmprotocrypto.PublicKey)
GetAvgDelegatedValue(
sdk.Context, []sdk.AccAddress, string, string,
) ([]int64, error)
SlashWithInfractionReason(
sdk.Context, sdk.AccAddress, int64,
int64, sdk.Dec, stakingtypes.Infraction,
) math.Int
}

// DelegationKeeper represents the expected keeper interface for the delegation module.
Expand All @@ -77,17 +85,8 @@ type EpochsHooks interface {

// AssetsKeeper represents the expected keeper interface for the assets module.
type AssetsKeeper interface {
GetOperatorAssetValue(sdk.Context, sdk.AccAddress) (int64, error)
IsStakingAsset(sdk.Context, string) bool
GetAvgDelegatedValue(
sdk.Context, []sdk.AccAddress, []string, string,
) ([]int64, error)
}

// SlashingKeeper represents the expected keeper interface for the (exo-)slashing module.
type SlashingKeeper interface {
SlashWithInfractionReason(
sdk.Context, sdk.AccAddress, int64,
int64, sdk.Dec, stakingtypes.Infraction,
) math.Int
}
type SlashingKeeper interface{}
32 changes: 32 additions & 0 deletions x/operator/keeper/avs_operator_shares.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,35 @@ func (k *Keeper) GetStakerShare(ctx sdk.Context, avsAddr, stakerID, operatorAddr

return ret.Amount, nil
}

func (k *Keeper) GetAvgDelegatedValue(
ctx sdk.Context, operators []sdk.AccAddress, chainID, _ string,
) ([]int64, error) {
avsAddr, err := k.avsKeeper.GetAvsAddrByChainID(ctx, chainID)
if err != nil {
return nil, err
}
ret := make([]int64, 0)
for _, operator := range operators {
share, err := k.GetOperatorShare(ctx, operator.String(), avsAddr)
if err != nil {
return nil, err
}
// truncate the USD value to int64
ret = append(ret, share.TruncateInt64())
}
return ret, nil
}

func (k *Keeper) GetOperatorAssetValue(ctx sdk.Context, operator sdk.AccAddress, chainID string) (int64, error) {
avsAddr, err := k.avsKeeper.GetAvsAddrByChainID(ctx, chainID)
bwhour marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return 0, err
}
share, err := k.GetOperatorShare(ctx, operator.String(), avsAddr)
if err != nil {
return 0, err
}
// truncate the USD value to int64
return share.TruncateInt64(), nil
}
6 changes: 0 additions & 6 deletions x/operator/keeper/consensus_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,12 +435,6 @@ func (k *Keeper) CompleteOperatorOptOutFromChainID(
store.Delete(types.KeyForOperatorOptOutFromChainID(opAccAddr, chainID))
}

// IsOperatorJailedForChainID add for dogfood
func (k *Keeper) IsOperatorJailedForChainID(sdk.Context, sdk.AccAddress, string) bool {
return false
}
func (k *Keeper) Jail(sdk.Context, sdk.ConsAddress, string) {}

func (k *Keeper) GetActiveOperatorsForChainID(
sdk.Context, string,
) ([]sdk.AccAddress, []*tmprotocrypto.PublicKey) {
Expand Down
9 changes: 9 additions & 0 deletions x/operator/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ func (k *Keeper) OptInToCosmosChain(
if err != nil {
return nil, err
}
// call the basic OptIn
avsAddr, err := k.avsKeeper.GetAvsAddrByChainID(ctx, req.ChainId)
if err != nil {
return nil, err
}
err = k.OptIn(ctx, addr, avsAddr)
if err != nil {
return nil, err
}
return &types.OptInToCosmosChainResponse{}, nil
}

Expand Down
54 changes: 52 additions & 2 deletions x/operator/keeper/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ func (k *Keeper) SetOperatorInfo(ctx sdk.Context, addr string, info *operatortyp
if err != nil {
return errorsmod.Wrap(err, "SetOperatorInfo: error occurred when parse acc address from Bech32")
}
// todo: to check the validation of input info
store := prefix.NewStore(ctx.KVStore(k.storeKey), operatortypes.KeyPrefixOperatorInfo)
// todo: think about the difference between init and update in future

Expand Down Expand Up @@ -49,12 +48,49 @@ func (k *Keeper) OperatorInfo(ctx sdk.Context, addr string) (info *operatortypes
return &ret, nil
}

// AllOperators return the address list of all operators
func (k *Keeper) AllOperators(ctx sdk.Context) []string {
store := prefix.NewStore(ctx.KVStore(k.storeKey), operatortypes.KeyPrefixOperatorInfo)
iterator := sdk.KVStorePrefixIterator(store, nil)
defer iterator.Close()

ret := make([]string, 0)
for ; iterator.Valid(); iterator.Next() {
accAddr := sdk.AccAddress(iterator.Key())
ret = append(ret, accAddr.String())
}
return ret
}

func (k *Keeper) IsOperator(ctx sdk.Context, addr sdk.AccAddress) bool {
store := prefix.NewStore(ctx.KVStore(k.storeKey), operatortypes.KeyPrefixOperatorInfo)
return store.Has(addr)
}

func (k *Keeper) UpdateOptedInfo(ctx sdk.Context, operatorAddr, avsAddr string, info *operatortypes.OptedInfo) error {
func (k *Keeper) HandleOptedInfo(ctx sdk.Context, operatorAddr, avsAddr string, handleFunc func(info *operatortypes.OptedInfo)) error {
opAccAddr, err := sdk.AccAddressFromBech32(operatorAddr)
if err != nil {
return errorsmod.Wrap(err, "HandleOptedInfo: error occurred when parse acc address from Bech32")
}
store := prefix.NewStore(ctx.KVStore(k.storeKey), operatortypes.KeyPrefixOperatorOptedAVSInfo)
infoKey := assetstype.GetJoinedStoreKey(operatorAddr, avsAddr)
ifExist := store.Has(infoKey)
if !ifExist {
return errorsmod.Wrap(operatortypes.ErrNoKeyInTheStore, fmt.Sprintf("HandleOptedInfo: key is %suite", opAccAddr))
}
// get info from the store
value := store.Get(infoKey)
info := &operatortypes.OptedInfo{}
k.cdc.MustUnmarshal(value, info)
// call the handleFunc
handleFunc(info)
// restore the info after handling
bz := k.cdc.MustMarshal(info)
store.Set(infoKey, bz)
return nil
}

func (k *Keeper) SetOptedInfo(ctx sdk.Context, operatorAddr, avsAddr string, info *operatortypes.OptedInfo) error {
bwhour marked this conversation as resolved.
Show resolved Hide resolved
store := prefix.NewStore(ctx.KVStore(k.storeKey), operatortypes.KeyPrefixOperatorOptedAVSInfo)

// check operator address validation
Expand Down Expand Up @@ -99,6 +135,20 @@ func (k *Keeper) IsOptedIn(ctx sdk.Context, operatorAddr, avsAddr string) bool {
return true
}

func (k *Keeper) IsActive(ctx sdk.Context, operatorAddr, avsAddr string) bool {
optedInfo, err := k.GetOptedInfo(ctx, operatorAddr, avsAddr)
if err != nil {
return false
}
if optedInfo.OptedOutHeight != operatortypes.DefaultOptedOutHeight {
return false
}
if optedInfo.Jailed {
return false
}
return true
}

func (k *Keeper) GetOptedInAVSForOperator(ctx sdk.Context, operatorAddr string) ([]string, error) {
// get all opted-in info
store := prefix.NewStore(ctx.KVStore(k.storeKey), operatortypes.KeyPrefixOperatorOptedAVSInfo)
Expand Down
14 changes: 14 additions & 0 deletions x/operator/keeper/operator_info_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package keeper_test

import (
"cosmossdk.io/math"
"github.com/ExocoreNetwork/exocore/x/assets/types"
operatortype "github.com/ExocoreNetwork/exocore/x/operator/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

func (suite *OperatorTestSuite) TestOperatorInfo() {
Expand All @@ -15,6 +17,7 @@ func (suite *OperatorTestSuite) TestOperatorInfo() {
{101, "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"},
},
},
Commission: stakingtypes.NewCommission(math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()),
}
err := suite.App.OperatorKeeper.SetOperatorInfo(suite.Ctx, suite.AccAddress.String(), info)
suite.NoError(err)
Expand All @@ -24,6 +27,17 @@ func (suite *OperatorTestSuite) TestOperatorInfo() {
suite.Equal(*info, *getOperatorInfo)
}

func (suite *OperatorTestSuite) TestAllOperators() {
suite.prepare()
operators := []string{suite.operatorAddr.String(), suite.AccAddress.String()}
info := &operatortype.OperatorInfo{}
err := suite.App.OperatorKeeper.SetOperatorInfo(suite.Ctx, suite.AccAddress.String(), info)
suite.NoError(err)

getOperators := suite.App.OperatorKeeper.AllOperators(suite.Ctx)
suite.Equal(operators, getOperators)
}

func (suite *OperatorTestSuite) TestHistoricalOperatorInfo() {
height := suite.Ctx.BlockHeight()
info := &operatortype.OperatorInfo{
Expand Down
Loading
Loading