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(dogfood): asset ids, oracle usage #11

Merged
merged 7 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
54 changes: 7 additions & 47 deletions proto/exocore/dogfood/v1/dogfood.proto
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
syntax = "proto3";

Check failure on line 1 in proto/exocore/dogfood/v1/dogfood.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present enum "OperationType" was deleted from file.

Check failure on line 1 in proto/exocore/dogfood/v1/dogfood.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present enum "QueueResultType" was deleted from file.

Check failure on line 1 in proto/exocore/dogfood/v1/dogfood.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present message "Operation" was deleted from file.

Check failure on line 1 in proto/exocore/dogfood/v1/dogfood.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present message "Operations" was deleted from file.

package exocore.dogfood.v1;

Expand All @@ -8,7 +8,6 @@

import "cosmos/staking/v1beta1/staking.proto";
import "cosmos_proto/cosmos.proto";
import "tendermint/crypto/keys.proto";

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

Expand All @@ -27,52 +26,6 @@
];
}

// OperationType is used to indicate the type of operation that is being
// cached by the module to create the updated validator set.
enum OperationType {
option (gogoproto.goproto_enum_prefix) = false;
// KeyOpUnspecified is used to indicate that the operation type is not specified.
// This should never be used.
OPERATION_TYPE_UNSPECIFIED = 0 [ (gogoproto.enumvalue_customname) = "KeyOpUnspecified" ];
// KeyAddition is used to indicate that the operation is a key addition.
OPERATION_TYPE_ADDITION_OR_UPDATE = 1 [ (gogoproto.enumvalue_customname) = "KeyAdditionOrUpdate" ];
// KeyRemoval is used to indicate that the operation is a key removal. Typically
// this is done due to key replacement mechanism and not directly.
OPERATION_TYPE_REMOVAL = 2 [ (gogoproto.enumvalue_customname) = "KeyRemoval" ];
}

// QueueResultType is used to indicate the result of the queue operation.
enum QueueResultType {
option (gogoproto.goproto_enum_prefix) = false;
// QueueResultUnspecified is used to indicate that the queue result type is not specified.
QUEUE_RESULT_TYPE_UNSPECIFIED = 0 [ (gogoproto.enumvalue_customname) = "QueueResultUnspecified" ];
// QueueResultSuccess is used to indicate that the queue operation was successful.
QUEUE_RESULT_TYPE_SUCCESS = 1 [ (gogoproto.enumvalue_customname) = "QueueResultSuccess" ];
// QueueResultExists is used to indicate that the queue operation failed because the
// operation already exists in the queue.
QUEUE_RESULT_TYPE_EXISTS = 2 [ (gogoproto.enumvalue_customname) = "QueueResultExists" ];
// QueueResultRemoved is used to indicate that the queue operation resulted in an existing
// operation being removed from the queue.
QUEUE_RESULT_TYPE_REMOVED = 3 [ (gogoproto.enumvalue_customname) = "QueueResultRemoved" ];
}

// Operation is used to indicate the operation that is being cached by the module
// to create the updated validator set.
message Operation {
// OperationType is the type of the operation (addition / removal).
OperationType operation_type = 1;
// OperatorAddress is the sdk.AccAddress of the operator.
bytes operator_address = 2;
// PubKey is the public key for which the operation is being applied.
tendermint.crypto.PublicKey pub_key = 3 [(gogoproto.nullable) = false];
}

// Operations is a collection of Operation.
message Operations {
// list is the list of operations.
repeated Operation list = 1 [(gogoproto.nullable) = false];
}

// AccountAddresses represents a list of account addresses. It is used to store the list of
// operator addresses whose operations are maturing at an epoch.
message AccountAddresses {
Expand Down Expand Up @@ -110,4 +63,11 @@
bytes next_validators_hash = 2;
// state after txs from the previous block
bytes app_hash = 3;
}

// KeyPowerMapping is a mapping of the consensus public key (as a string)
// to the power of the key.
message KeyPowerMapping {
// list is the actual mapping of the consensus public key to the power.
map<string, int64> list = 1;
}
5 changes: 5 additions & 0 deletions proto/exocore/dogfood/v1/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ message Params {
uint32 max_validators = 3;
// HistoricalEntries is the number of historical entries to persist.
uint32 historical_entries = 4;
// AssetIDs is the ids of the assets which will be accepted by the module.
// It must be within the list of assets supported by the restaking module.
// The typical format of these IDs is
// lower(assetAddress) + _ + hex(clientChainLzID)
repeated string asset_ids = 5 [(gogoproto.customname) = "AssetIDs"];
}
116 changes: 86 additions & 30 deletions x/dogfood/keeper/abci.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package keeper

import (
"github.com/ExocoreNetwork/exocore/x/dogfood/types"
"sort"

abci "github.com/cometbft/cometbft/abci/types"
tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
)

func (k Keeper) EndBlock(ctx sdk.Context) []abci.ValidatorUpdate {
id, _ := k.getValidatorSetID(ctx, ctx.BlockHeight())
if !k.IsEpochEnd(ctx) {
// save the same id for the next block height.
k.setValidatorSetID(ctx, ctx.BlockHeight()+1, id)
return []abci.ValidatorUpdate{}
}
defer k.ClearEpochEnd(ctx)
// start with clearing the hold on the undelegations.
Copy link
Contributor

Choose a reason for hiding this comment

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

I haven't seen the code to increase the undelegationHoldCount. Why does it need to be decreased here? Additionally, I have looked at the related code about OnHold in Cosmos-SDK and Evmos, It's a great feature. But I didn't find this feature being used in Cosmos-SDK and Evmos.

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 haven't seen the code to increase the undelegationHoldCount. Why does it need to be decreased here?

You are right; I made a mistake. The line that increases it was accidentally removed. I will add it back. Great catch!

Additionally, I have looked at the related code about OnHold in Cosmos-SDK and Evmos, It's a great feature. But I didn't find this feature being used in Cosmos-SDK and Evmos.

Evmos certainly does not use it, as far as I can tell. Cosmos-SDK introduced the feature to support interchain-security, which does use it. I do not think any standalone chains use it.

undelegations := k.GetPendingUndelegations(ctx)
for _, undelegation := range undelegations.GetList() {
Expand All @@ -30,40 +39,87 @@
}
k.ClearPendingConsensusAddrs(ctx)
// finally, perform the actual operations of vote power changes.
operations := k.GetPendingOperations(ctx)
id, _ := k.getValidatorSetID(ctx, ctx.BlockHeight())
if len(operations.GetList()) == 0 {
// there is no validator set change, so we just increment the block height
// and retain the same val set id mapping.
k.setValidatorSetID(ctx, ctx.BlockHeight()+1, id)
// 1. find all operator keys for the chain.
// 2. find last stored operator keys + their powers.
// 3. find newest vote power for the operator keys, and sort them.
// 4. loop through #1 and see if anything has changed.
// if it hasn't, do nothing for that operator key.
// if it has, queue an update.
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),
)
if err != nil {
return []abci.ValidatorUpdate{}
}
res := make([]abci.ValidatorUpdate, 0, len(operations.GetList()))
for _, operation := range operations.GetList() {
switch operation.OperationType {
case types.KeyAdditionOrUpdate:
power, err := k.restakingKeeper.GetOperatorAssetValue(
ctx, operation.OperatorAddress,
)
if err != nil {
// this should never happen, but if it does, we just skip the operation.
continue
}
res = append(res, abci.ValidatorUpdate{
PubKey: operation.PubKey,
Power: power,
})
case types.KeyRemoval:
res = append(res, abci.ValidatorUpdate{
PubKey: operation.PubKey,
Power: 0,
})
case types.KeyOpUnspecified:
// this should never happen, but if it does, we just skip the operation.
operators, keys, powers = sortByPower(operators, keys, powers)
maxVals := k.GetMaxValidators(ctx)
for i := range operators {
if i >= int(maxVals) { // #nosec G701 // #nosec G701 // ok if 64-bit.
Fixed Show fixed Hide fixed
// we have reached the maximum number of validators.
break
}
key := keys[i]
power := powers[i]
if power < 1 {
// we have reached the bottom of the rung.
break
}
// find the previous power.
prevPower, found := prev[key.String()]
if found && prevPower == power {
delete(prev, key.String())
continue
}
// either the key was not in the previous set,
// or the power has changed.
res = append(res, abci.ValidatorUpdate{
PubKey: key,
Power: power,
})
}
// the remaining keys in prev have lost their power.
for key := range prev {
bz := []byte(key)
var keyObj tmprotocrypto.PublicKey
k.cdc.MustUnmarshal(bz, &keyObj)
res = append(res, abci.ValidatorUpdate{
PubKey: keyObj,
Power: 0,
})
}

Check failure

Code scanning / gosec

the value in the range statement should be _ unless copying a map: want: for key := range m Error

expected exactly 1 statement (either append, delete, or copying to another map) in a range with a map, got 4
// call via wrapper function so that validator info is stored.
// the id is incremented by 1 for the next block.
return k.ApplyValidatorChanges(ctx, res, id+1, false)
return k.ApplyValidatorChanges(ctx, res, id+1)
}

func sortByPower(
operatorAddrs []sdk.AccAddress,
pubKeys []tmprotocrypto.PublicKey,
powers []int64,
) ([]sdk.AccAddress, []tmprotocrypto.PublicKey, []int64) {
// Create a slice of indices
indices := make([]int, len(powers))
for i := range indices {
indices[i] = i
}

// Sort the indices slice based on the powers slice
sort.SliceStable(indices, func(i, j int) bool {
return powers[indices[i]] > powers[indices[j]]
})

// Reorder all slices using the sorted indices
sortedOperatorAddrs := make([]sdk.AccAddress, len(operatorAddrs))
sortedPubKeys := make([]tmprotocrypto.PublicKey, len(pubKeys))
sortedPowers := make([]int64, len(powers))
for i, idx := range indices {
sortedOperatorAddrs[i] = operatorAddrs[idx]
sortedPubKeys[i] = pubKeys[idx]
sortedPowers[i] = powers[idx]
}

return sortedOperatorAddrs, sortedPubKeys, sortedPowers
}
21 changes: 19 additions & 2 deletions x/dogfood/keeper/genesis.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package keeper

import (
"fmt"

"github.com/ExocoreNetwork/exocore/x/dogfood/types"
abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -21,13 +23,28 @@
// is not running. it means that the genesis file is malformed.
panic("epoch info not found")
}
return k.ApplyValidatorChanges(ctx, genState.ValSet, types.InitialValidatorSetID, true)
// apply the same logic to the staking assets.
for _, assetID := range genState.Params.AssetIDs {
if !k.restakingKeeper.IsStakingAsset(ctx, assetID) {
panic(fmt.Errorf("staking asset %s not found", assetID))
}
}
// genState must not be malformed.
if len(genState.ValSet) > int(k.GetMaxValidators(ctx)) { // #nosec G701 // ok if 64-bit.
Fixed Show fixed Hide fixed
panic(fmt.Errorf(
"cannot have more than %d validators in the genesis state",
k.GetMaxValidators(ctx),
))
}
return k.ApplyValidatorChanges(
ctx, genState.ValSet, types.InitialValidatorSetID,
)
}

// ExportGenesis returns the module's exported genesis
func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState {
genesis := types.DefaultGenesis()
genesis.Params = k.GetDogfoodParams(ctx)

// TODO(mm)
return genesis
}
103 changes: 18 additions & 85 deletions x/dogfood/keeper/impl_delegation_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,100 +22,33 @@ func (k *Keeper) DelegationHooks() DelegationHooksWrapper {

// AfterDelegation is called after a delegation is made.
func (wrapper DelegationHooksWrapper) AfterDelegation(
ctx sdk.Context, operator sdk.AccAddress,
sdk.Context, sdk.AccAddress,
) {
found, pubKey, err := wrapper.keeper.operatorKeeper.GetOperatorConsKeyForChainId(
ctx, operator, ctx.ChainID(),
)
if err != nil {
// the operator keeper can offer two errors: not an operator and not a chain.
// both of these should not happen here because the dogfooding genesis will
// register the chain, and the operator must be known to the delegation module
// when it calls this hook.
panic(err)
}
if found {
if !wrapper.keeper.operatorKeeper.IsOperatorOptingOutFromChainId(
ctx, operator, ctx.ChainID(),
) {
// only queue the operation if operator is still opted into the chain.
res := wrapper.keeper.QueueOperation(
ctx, operator, pubKey, types.KeyAdditionOrUpdate,
)
switch res {
case types.QueueResultExists:
// nothing to do because the operation is in the queue already.
case types.QueueResultRemoved:
// a KeyRemoval was in the queue which has now been cleared from the queue.
// the KeyRemoval can only be in the queue if the operator is opting out from
// the chain, or has replaced their key. if it is the former, it means that
// there is some inconsistency. if it is the latter, it means that the operator
// module just reported the old key in `GetOperatorConsKeyForChainId`, which
// should not happen.
panic("unexpected removal of operation from queue")
case types.QueueResultSuccess:
// best case, nothing to do.
case types.QueueResultUnspecified:
panic("unspecified queue result")
}
}
}
// we do nothing here, since the vote power for all operators is calculated
// in the end separately. even if we knew the amount of the delegation, the
// average exchange rate for the epoch is unknown.
}

// AfterUndelegationStarted is called after an undelegation is started.
func (wrapper DelegationHooksWrapper) AfterUndelegationStarted(
ctx sdk.Context, operator sdk.AccAddress, recordKey []byte,
) {
found, pubKey, err := wrapper.keeper.operatorKeeper.GetOperatorConsKeyForChainId(
var unbondingCompletionEpoch int64
if wrapper.keeper.operatorKeeper.IsOperatorOptingOutFromChainId(
ctx, operator, ctx.ChainID(),
)
if err != nil {
panic(err)
}
if found {
// note that this is still key addition or update because undelegation does not remove
// the operator from the list. it only decreases their vote power.
if !wrapper.keeper.operatorKeeper.IsOperatorOptingOutFromChainId(
ctx, operator, ctx.ChainID(),
) {
// only queue the operation if operator is still opted into the chain.
res := wrapper.keeper.QueueOperation(
ctx, operator, pubKey, types.KeyAdditionOrUpdate,
)
switch res {
case types.QueueResultExists:
// nothing to do
case types.QueueResultRemoved:
// KeyRemoval + KeyAdditionOrUpdate => Removed
// KeyRemoval can happen
// 1. if the operator is opting out from the chain,which is inconsistent.
// 2. if the operator is replacing their old key, which should not be returned
// by `GetOperatorConsKeyForChainId`.
panic("unexpected removal of operation from queue")
case types.QueueResultSuccess:
// best case, nothing to do.
case types.QueueResultUnspecified:
panic("unspecified queue result")
}
}
// now handle the unbonding timeline.
wrapper.keeper.delegationKeeper.IncrementUndelegationHoldCount(ctx, recordKey)
// mark for unbonding release.
// note that we aren't supporting redelegation yet, so this undelegated amount will be
// held until the end of the unbonding period or the operator opt out period, whichever
// is first.
var unbondingCompletionEpoch int64
if wrapper.keeper.operatorKeeper.IsOperatorOptingOutFromChainId(
ctx, operator, ctx.ChainID(),
) {
unbondingCompletionEpoch = wrapper.keeper.GetOperatorOptOutFinishEpoch(
ctx, operator,
)
} else {
unbondingCompletionEpoch = wrapper.keeper.GetUnbondingCompletionEpoch(ctx)
}
wrapper.keeper.AppendUndelegationToMature(ctx, unbondingCompletionEpoch, recordKey)
) {
// if the operator is opting out, we need to use the finish epoch of the opt out.
unbondingCompletionEpoch = wrapper.keeper.GetOperatorOptOutFinishEpoch(ctx, operator)
// even if the operator opts back in, the undelegated vote power does not reappear
// in the picture. slashable events between undelegation and opt in cannot occur
// because the operator is not in the validator set.
} else {
// otherwise, we use the default unbonding completion epoch.
unbondingCompletionEpoch = wrapper.keeper.GetUnbondingCompletionEpoch(ctx)
// if the operator opts out after this, the undelegation will mature before the opt out.
// so this is not a concern.
}
wrapper.keeper.AppendUndelegationToMature(ctx, unbondingCompletionEpoch, recordKey)
}

// AfterUndelegationCompleted is called after an undelegation is completed.
Expand Down
Loading
Loading