Skip to content

Commit

Permalink
feat(dogfood): asset ids, oracle usage (#11)
Browse files Browse the repository at this point in the history
* feat(dogfood): add asset_ids param

* feat(dogfood): move to average pricing

* fix(dogfood): relint, add epoch id for avg

* chore(dogfood): golangci-lint

* chore(dogfood): lint gosec again

* fix(dogfood): use correct string for pubkey

* fix(dogfood): ++ the undelegation hold count

The line was accidentally deleted.
  • Loading branch information
MaxMustermann2 authored Mar 5, 2024
1 parent 59fce84 commit f3df5ef
Show file tree
Hide file tree
Showing 19 changed files with 728 additions and 1,020 deletions.
54 changes: 7 additions & 47 deletions proto/exocore/dogfood/v1/dogfood.proto
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import "google/protobuf/timestamp.proto";

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 @@ message ExocoreValidator {
];
}

// 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 @@ message HeaderSubset {
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"];
}
124 changes: 94 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.
undelegations := k.GetPendingUndelegations(ctx)
for _, undelegation := range undelegations.GetList() {
Expand All @@ -30,40 +39,95 @@ func (k Keeper) EndBlock(ctx sdk.Context) []abci.ValidatorUpdate {
}
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 {
// #nosec G701 // ok if 64-bit.
if i >= int(maxVals) {
// we have reached the maximum number of validators.
break
}
power := powers[i]
if power < 1 {
// we have reached the bottom of the rung.
break
}
// find the previous power.
key := keys[i]
keyString := string(k.cdc.MustMarshal(&key))
prevPower, found := prev[keyString]
if found && prevPower == power {
delete(prev, keyString)
continue
}
// either the key was not in the previous set,
// or the power has changed.
res = append(res, abci.ValidatorUpdate{
PubKey: key,
// note that this is the final power and not the change in power.
Power: power,
})
}
// the remaining keys in prev have lost their power.
// gosec does not like `for key := range prev` while others do not like
// `for key, _ := range prev`
// #nosec G705
for key := range prev {
bz := []byte(key) // undo string operation
var keyObj tmprotocrypto.PublicKey
k.cdc.MustUnmarshal(bz, &keyObj) // undo marshal operation
res = append(res, abci.ValidatorUpdate{
PubKey: keyObj,
Power: 0,
})
}

Check warning

Code scanning / CodeQL

Iteration over map Warning

Iteration over map may be a possible source of non-determinism
// 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)
}

// sortByPower sorts operators, their pubkeys, and their powers by the powers.
// the sorting is descending, so the highest power is first.
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
}
22 changes: 20 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,29 @@ func (k Keeper) InitGenesis(
// 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.
// #nosec G701 // ok if 64-bit.
if len(genState.ValSet) > int(k.GetMaxValidators(ctx)) {
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
}
104 changes: 19 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,34 @@ 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)
wrapper.keeper.delegationKeeper.IncrementUndelegationHoldCount(ctx, recordKey)
}

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

0 comments on commit f3df5ef

Please sign in to comment.