Skip to content

Commit

Permalink
ICQCallbacks File Org (#337)
Browse files Browse the repository at this point in the history
  • Loading branch information
sampocs authored Nov 16, 2022
1 parent 1ba0b50 commit 3ec6b8e
Show file tree
Hide file tree
Showing 14 changed files with 465 additions and 433 deletions.
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ func NewStrideApp(
)

// Register ICQ callbacks
err := app.InterchainqueryKeeper.SetCallbackHandler(stakeibcmoduletypes.ModuleName, app.StakeibcKeeper.CallbackHandler())
err := app.InterchainqueryKeeper.SetCallbackHandler(stakeibcmoduletypes.ModuleName, app.StakeibcKeeper.ICQCallbackHandler())
if err != nil {
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions x/interchainquery/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (k *Keeper) SetCallbackHandler(module string, handler types.QueryCallbacks)
if found {
return fmt.Errorf("callback handler already set for %s", module)
}
k.callbacks[module] = handler.RegisterCallbacks()
k.callbacks[module] = handler.RegisterICQCallbacks()
return nil
}

Expand Down Expand Up @@ -94,7 +94,7 @@ func (k *Keeper) MakeRequest(ctx sdk.Context, connection_id string, chain_id str
k.Logger(ctx).Error(err.Error())
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "no callback handler registered for module")
}
if exists := k.callbacks[module].Has(callback_id); !exists {
if exists := k.callbacks[module].HasICQCallback(callback_id); !exists {
err := fmt.Errorf("no callback %s registered for module %s", callback_id, module)
k.Logger(ctx).Error(err.Error())
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "no callback handler registered for module")
Expand Down
4 changes: 2 additions & 2 deletions x/interchainquery/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ func (k Keeper) InvokeCallback(ctx sdk.Context, msg *types.MsgSubmitQueryRespons
k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] executing callback for queryId (%s), module (%s)", q.Id, moduleName))
moduleCallbackHandler := k.callbacks[moduleName]

if moduleCallbackHandler.Has(q.CallbackId) {
if moduleCallbackHandler.HasICQCallback(q.CallbackId) {
k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] callback (%s) found for module (%s)", q.CallbackId, moduleName))
// call the correct callback function
err := moduleCallbackHandler.Call(ctx, q.CallbackId, msg.Result, q)
err := moduleCallbackHandler.CallICQCallback(ctx, q.CallbackId, msg.Result, q)
if err != nil {
k.Logger(ctx).Error(fmt.Sprintf("[ICQ Resp] error in ICQ callback, error: %s, msg: %s, result: %v, type: %s, params: %v", err.Error(), msg.QueryId, msg.Result, q.QueryType, q.Request))
return err
Expand Down
8 changes: 4 additions & 4 deletions x/interchainquery/types/callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
)

type QueryCallbacks interface {
AddCallback(id string, fn interface{}) QueryCallbacks
RegisterCallbacks() QueryCallbacks
Call(ctx sdk.Context, id string, args []byte, query Query) error
Has(id string) bool
AddICQCallback(id string, fn interface{}) QueryCallbacks
RegisterICQCallbacks() QueryCallbacks
CallICQCallback(ctx sdk.Context, id string, args []byte, query Query) error
HasICQCallback(id string) bool
}
404 changes: 0 additions & 404 deletions x/stakeibc/keeper/callbacks.go

This file was deleted.

24 changes: 12 additions & 12 deletions x/stakeibc/keeper/icacallbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
)

const (
DELEGATE = "delegate"
CLAIM = "claim"
UNDELEGATE = "undelegate"
REINVEST = "reinvest"
REDEMPTION = "redemption"
REBALANCE = "rebalance"
ICACallbackID_Delegate = "delegate"
ICACallbackID_Claim = "claim"
ICACallbackID_Undelegate = "undelegate"
ICACallbackID_Reinvest = "reinvest"
ICACallbackID_Redemption = "redemption"
ICACallbackID_Rebalance = "rebalance"
)

// ICACallbacks wrapper struct for stakeibc keeper
Expand Down Expand Up @@ -46,11 +46,11 @@ func (c ICACallbacks) AddICACallback(id string, fn interface{}) icacallbackstype

func (c ICACallbacks) RegisterICACallbacks() icacallbackstypes.ICACallbackHandler {
a := c.
AddICACallback(DELEGATE, ICACallback(DelegateCallback)).
AddICACallback(CLAIM, ICACallback(ClaimCallback)).
AddICACallback(UNDELEGATE, ICACallback(UndelegateCallback)).
AddICACallback(REINVEST, ICACallback(ReinvestCallback)).
AddICACallback(REDEMPTION, ICACallback(RedemptionCallback)).
AddICACallback(REBALANCE, ICACallback(RebalanceCallback))
AddICACallback(ICACallbackID_Delegate, ICACallback(DelegateCallback)).
AddICACallback(ICACallbackID_Claim, ICACallback(ClaimCallback)).
AddICACallback(ICACallbackID_Undelegate, ICACallback(UndelegateCallback)).
AddICACallback(ICACallbackID_Reinvest, ICACallback(ReinvestCallback)).
AddICACallback(ICACallbackID_Redemption, ICACallback(RedemptionCallback)).
AddICACallback(ICACallbackID_Rebalance, ICACallback(RebalanceCallback))
return a.(ICACallbacks)
}
48 changes: 48 additions & 0 deletions x/stakeibc/keeper/icqcallbacks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package keeper

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

icqtypes "github.com/Stride-Labs/stride/x/interchainquery/types"
)

const (
ICQCallbackID_WithdrawalBalance = "withdrawalbalance"
ICQCallbackID_Delegation = "delegation"
ICQCallbackID_Validator = "validator"
)

// ICQCallbacks wrapper struct for stakeibc keeper
type ICQCallback func(Keeper, sdk.Context, []byte, icqtypes.Query) error

type ICQCallbacks struct {
k Keeper
callbacks map[string]ICQCallback
}

var _ icqtypes.QueryCallbacks = ICQCallbacks{}

func (k Keeper) ICQCallbackHandler() ICQCallbacks {
return ICQCallbacks{k, make(map[string]ICQCallback)}
}

func (c ICQCallbacks) CallICQCallback(ctx sdk.Context, id string, args []byte, query icqtypes.Query) error {
return c.callbacks[id](c.k, ctx, args, query)
}

func (c ICQCallbacks) HasICQCallback(id string) bool {
_, found := c.callbacks[id]
return found
}

func (c ICQCallbacks) AddICQCallback(id string, fn interface{}) icqtypes.QueryCallbacks {
c.callbacks[id] = fn.(ICQCallback)
return c
}

func (c ICQCallbacks) RegisterICQCallbacks() icqtypes.QueryCallbacks {
return c.
AddICQCallback(ICQCallbackID_WithdrawalBalance, ICQCallback(WithdrawalBalanceCallback)).
AddICQCallback(ICQCallbackID_Delegation, ICQCallback(DelegatorSharesCallback)).
AddICQCallback(ICQCallbackID_Validator, ICQCallback(ValidatorExchangeRateCallback))
}
144 changes: 144 additions & 0 deletions x/stakeibc/keeper/icqcallbacks_delegator_shares.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package keeper

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/spf13/cast"

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

epochtypes "github.com/Stride-Labs/stride/x/epochs/types"
icqtypes "github.com/Stride-Labs/stride/x/interchainquery/types"
"github.com/Stride-Labs/stride/x/stakeibc/types"
)

// DelegatorSharesCallback is a callback handler for UpdateValidatorSharesExchRate queries.
//
// In an attempt to get the ICA's delegation amount on a given validator, we have to query:
// 1. the validator's internal exchange rate
// 2. the Delegation ICA's delegated shares
// And apply the following equation:
// num_tokens = exchange_rate * num_shares
//
// This is the callback from query #2
//
// Note: for now, to get proofs in your ICQs, you need to query the entire store on the host zone! e.g. "store/bank/key"
func DelegatorSharesCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error {
hostZone, found := k.GetHostZone(ctx, query.GetChainId())
if !found {
errMsg := fmt.Sprintf("no registered zone for queried chain ID (%s)", query.GetChainId())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrHostZoneNotFound, errMsg)
}

// Unmarshal the query response which returns a delegation object for the delegator/validator pair
queriedDelgation := stakingtypes.Delegation{}
err := k.cdc.Unmarshal(args, &queriedDelgation)
if err != nil {
errMsg := fmt.Sprintf("unable to unmarshal queried delegation info for zone %s, err: %s", hostZone.ChainId, err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrMarshalFailure, errMsg)
}
k.Logger(ctx).Info(fmt.Sprintf("DelegationCallback: HostZone: %s, Delegator: %s, Validator: %s, Shares: %v",
hostZone.ChainId, queriedDelgation.DelegatorAddress, queriedDelgation.ValidatorAddress, queriedDelgation.Shares))

// ensure ICQ can be issued now! else fail the callback
isWithinWindow, err := k.IsWithinBufferWindow(ctx)
if err != nil {
errMsg := fmt.Sprintf("unable to determine if ICQ callback is inside buffer window, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrOutsideIcqWindow, errMsg)
} else if !isWithinWindow {
k.Logger(ctx).Error("delegator shares callback is outside ICQ window")
return nil
}

// Grab the validator object form the hostZone using the address returned from the query
validator, valIndex, found := GetValidatorFromAddress(hostZone.Validators, queriedDelgation.ValidatorAddress)
if !found {
errMsg := fmt.Sprintf("no registered validator for address (%s)", queriedDelgation.ValidatorAddress)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrValidatorNotFound, errMsg)
}

// get the validator's internal exchange rate, aborting if it hasn't been updated this epoch
strideEpochTracker, found := k.GetEpochTracker(ctx, epochtypes.STRIDE_EPOCH)
if !found {
k.Logger(ctx).Error("failed to find stride epoch")
return sdkerrors.Wrapf(sdkerrors.ErrNotFound, "no epoch number for epoch (%s)", epochtypes.STRIDE_EPOCH)
}
if validator.InternalExchangeRate.EpochNumber != strideEpochTracker.GetEpochNumber() {
errMsg := fmt.Sprintf("DelegationCallback: validator (%s) internal exchange rate has not been updated this epoch (epoch #%d)",
validator.Address, strideEpochTracker.GetEpochNumber())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, errMsg)
}

// TODO: make sure conversion math precision matches the sdk's staking module's version (we did it slightly differently)
// note: truncateInt per https://github.com/cosmos/cosmos-sdk/blob/cb31043d35bad90c4daa923bb109f38fd092feda/x/staking/types/validator.go#L431
validatorTokens := queriedDelgation.Shares.Mul(validator.InternalExchangeRate.InternalTokensToSharesRate).TruncateInt()
k.Logger(ctx).Info(fmt.Sprintf("DelegationCallback: HostZone: %s, Validator: %s, Previous NumTokens: %d, Current NumTokens: %v",
hostZone.ChainId, validator.Address, validator.DelegationAmt, validatorTokens))

// Confirm the validator has actually been slashed
if validatorTokens.Uint64() == validator.DelegationAmt {
k.Logger(ctx).Info(fmt.Sprintf("DelegationCallback: Validator (%s) was not slashed", validator.Address))
return nil
} else if validatorTokens.Uint64() > validator.DelegationAmt {
errMsg := fmt.Sprintf("DelegationCallback: Validator (%s) tokens returned from query is greater than the DelegationAmt", validator.Address)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, errMsg)
}

// TODO(TESTS-171) add some safety checks here (e.g. we could query the slashing module to confirm the decr in tokens was due to slash)
// update our records of the total stakedbal and of the validator's delegation amt
// NOTE: we assume any decrease in delegation amt that's not tracked via records is a slash

// Get slash percentage
delegationAmount, err := cast.ToInt64E(validator.DelegationAmt)
if err != nil {
errMsg := fmt.Sprintf("unable to convert validator delegation amount to int64, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrIntCast, errMsg)
}
slashAmountUInt := validator.DelegationAmt - validatorTokens.Uint64()
slashAmount, err := cast.ToInt64E(slashAmountUInt)
if err != nil {
errMsg := fmt.Sprintf("unable to convert validator slash amount to int64, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrIntCast, errMsg)
}
weight, err := cast.ToInt64E(validator.Weight)
if err != nil {
errMsg := fmt.Sprintf("unable to convert validator weight to int64, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrIntCast, errMsg)
}

slashPct := sdk.NewDec(slashAmount).Quo(sdk.NewDec(delegationAmount))
k.Logger(ctx).Info(fmt.Sprintf("ICQ'd Delegation Amoount Mismatch, HostZone: %s, Validator: %s, Delegator: %s, Records Tokens: %d, Tokens from ICQ %v, Slash Amount: %d, Slash Pct: %v!",
hostZone.ChainId, validator.Address, queriedDelgation.DelegatorAddress, validator.DelegationAmt, validatorTokens, slashAmount, slashPct))

// Abort if the slash was greater than 10%
tenPercent := sdk.NewDec(10).Quo(sdk.NewDec(100))
if slashPct.GT(tenPercent) {
errMsg := fmt.Sprintf("DelegationCallback: Validator (%s) slashed but ABORTING update, slash is greater than 0.10 (%d)", validator.Address, slashPct)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrSlashGtTenPct, errMsg)
}

// Update the host zone and validator to reflect the weight and delegation change
weightAdjustment := sdk.NewDec(validatorTokens.Int64()).Quo(sdk.NewDec(delegationAmount))
validator.Weight = sdk.NewDec(weight).Mul(weightAdjustment).TruncateInt().Uint64()
validator.DelegationAmt -= slashAmountUInt

hostZone.StakedBal -= slashAmountUInt
hostZone.Validators[valIndex] = &validator
k.SetHostZone(ctx, hostZone)

k.Logger(ctx).Info(fmt.Sprintf("Validator (%s) slashed! Delegation updated to: %v", validator.Address, validator.DelegationAmt))

return nil
}
98 changes: 98 additions & 0 deletions x/stakeibc/keeper/icqcallbacks_validator_exchange_rate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package keeper

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

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

epochtypes "github.com/Stride-Labs/stride/x/epochs/types"
icqtypes "github.com/Stride-Labs/stride/x/interchainquery/types"
"github.com/Stride-Labs/stride/x/stakeibc/types"
)

// ValidatorCallback is a callback handler for validator queries.
//
// In an attempt to get the ICA's delegation amount on a given validator, we have to query:
// 1. the validator's internal exchange rate
// 2. the Delegation ICA's delegated shares
// And apply the following equation:
// num_tokens = exchange_rate * num_shares
//
// This is the callback from query #1
func ValidatorExchangeRateCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error {
hostZone, found := k.GetHostZone(ctx, query.GetChainId())
if !found {
errMsg := fmt.Sprintf("no registered zone for queried chain ID (%s)", query.GetChainId())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrHostZoneNotFound, errMsg)
}
queriedValidator := stakingtypes.Validator{}
err := k.cdc.Unmarshal(args, &queriedValidator)
if err != nil {
errMsg := fmt.Sprintf("unable to unmarshal queriedValidator info for zone %s, err: %s", hostZone.ChainId, err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrMarshalFailure, errMsg)
}
k.Logger(ctx).Info(fmt.Sprintf("ValidatorCallback: HostZone %s, Queried Validator %v, Jailed: %v, Tokens: %v, Shares: %v",
hostZone.ChainId, queriedValidator.OperatorAddress, queriedValidator.Jailed, queriedValidator.Tokens, queriedValidator.DelegatorShares))

// ensure ICQ can be issued now! else fail the callback
withinBufferWindow, err := k.IsWithinBufferWindow(ctx)
if err != nil {
errMsg := fmt.Sprintf("unable to determine if ICQ callback is inside buffer window, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrOutsideIcqWindow, errMsg)
} else if !withinBufferWindow {
k.Logger(ctx).Error("validator exchange rate callback is outside ICQ window")
return nil
}

// get the validator from the host zone
validator, valIndex, found := GetValidatorFromAddress(hostZone.Validators, queriedValidator.OperatorAddress)
if !found {
errMsg := fmt.Sprintf("no registered validator for address (%s)", queriedValidator.OperatorAddress)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrValidatorNotFound, errMsg)
}
// get the stride epoch number
strideEpochTracker, found := k.GetEpochTracker(ctx, epochtypes.STRIDE_EPOCH)
if !found {
k.Logger(ctx).Error("failed to find stride epoch")
return sdkerrors.Wrapf(sdkerrors.ErrNotFound, "no epoch number for epoch (%s)", epochtypes.STRIDE_EPOCH)
}

// If the validator's delegation shares is 0, we'll get a division by zero error when trying to get the exchange rate
// because `validator.TokensFromShares` uses delegation shares in the denominator
if queriedValidator.DelegatorShares.IsZero() {
errMsg := fmt.Sprintf("can't calculate validator internal exchange rate because delegation amount is 0 (validator: %s)", validator.Address)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrDivisionByZero, errMsg)
}

// We want the validator's internal exchange rate which is held internally behind `validator.TokensFromShares`
// Since,
// exchange_rate = num_tokens / num_shares
// We can use `validator.TokensFromShares`, plug in 1.0 for the number of shares,
// and the returned number of tokens will be equal to the internal exchange rate
validator.InternalExchangeRate = &types.ValidatorExchangeRate{
InternalTokensToSharesRate: queriedValidator.TokensFromShares(sdk.NewDec(1.0)),
EpochNumber: strideEpochTracker.GetEpochNumber(),
}
hostZone.Validators[valIndex] = &validator
k.SetHostZone(ctx, hostZone)

k.Logger(ctx).Info(fmt.Sprintf("ValidatorCallback: HostZone %s, Validator %v, tokensFromShares %v",
hostZone.ChainId, validator.Address, validator.InternalExchangeRate.InternalTokensToSharesRate))

// armed with the exch rate, we can now query the (val,del) delegation
err = k.QueryDelegationsIcq(ctx, hostZone, queriedValidator.OperatorAddress)
if err != nil {
errMsg := fmt.Sprintf("ValidatorCallback: failed to query delegation, zone %s, err: %s", hostZone.ChainId, err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrICQFailed, errMsg)
}
return nil
}
Loading

0 comments on commit 3ec6b8e

Please sign in to comment.