package keeper import ( "context" "strconv" "time" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/armon/go-metrics" tmstrings "github.com/tendermint/tendermint/libs/strings" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/staking/types" ) type msgServer struct { Keeper } // NewMsgServerImpl returns an implementation of the bank MsgServer interface // for the provided Keeper. func NewMsgServerImpl(keeper Keeper) types.MsgServer { return &msgServer{Keeper: keeper} } var _ types.MsgServer = msgServer{} // CreateValidator defines a method for creating a new validator func (k msgServer) CreateValidator(goCtx context.Context, msg *types.MsgCreateValidator) (*types.MsgCreateValidatorResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) valAddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddress) if err != nil { return nil, err } if msg.Commission.Rate.LT(k.MinCommissionRate(ctx)) { return nil, sdkerrors.Wrapf(types.ErrCommissionLTMinRate, "cannot set validator commission to less than minimum rate of %s", k.MinCommissionRate(ctx)) } // check to see if the pubkey or sender has been registered before if _, found := k.GetValidator(ctx, valAddr); found { return nil, types.ErrValidatorOwnerExists } pk, ok := msg.Pubkey.GetCachedValue().(cryptotypes.PubKey) if !ok { return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "Expecting cryptotypes.PubKey, got %T", pk) } if _, found := k.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(pk)); found { return nil, types.ErrValidatorPubKeyExists } bondDenom := k.BondDenom(ctx) if msg.Value.Denom != bondDenom { return nil, sdkerrors.Wrapf( sdkerrors.ErrInvalidRequest, "invalid coin denomination: got %s, expected %s", msg.Value.Denom, bondDenom, ) } if _, err := msg.Description.EnsureLength(); err != nil { return nil, err } cp := ctx.ConsensusParams() if cp != nil && cp.Validator != nil { if !tmstrings.StringInSlice(pk.Type(), cp.Validator.PubKeyTypes) { return nil, sdkerrors.Wrapf( types.ErrValidatorPubKeyTypeNotSupported, "got: %s, expected: %s", pk.Type(), cp.Validator.PubKeyTypes, ) } } validator, err := types.NewValidator(valAddr, pk, msg.Description) if err != nil { return nil, err } commission := types.NewCommissionWithTime( msg.Commission.Rate, msg.Commission.MaxRate, msg.Commission.MaxChangeRate, ctx.BlockHeader().Time, ) validator, err = validator.SetInitialCommission(commission) if err != nil { return nil, err } delegatorAddress, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) if err != nil { return nil, err } validator.MinSelfDelegation = msg.MinSelfDelegation k.SetValidator(ctx, validator) k.SetValidatorByConsAddr(ctx, validator) k.SetNewValidatorByPowerIndex(ctx, validator) // call the after-creation hook if err := k.AfterValidatorCreated(ctx, validator.GetOperator()); err != nil { return nil, err } // move coins from the msg.Address account to a (self-delegation) delegator account // the validator account and global shares are updated within here // NOTE source will always be from a wallet which are unbonded _, err = k.Keeper.Delegate(ctx, delegatorAddress, msg.Value.Amount, types.Unbonded, validator, true) if err != nil { return nil, err } ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeCreateValidator, sdk.NewAttribute(types.AttributeKeyValidator, msg.ValidatorAddress), sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Value.String()), ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(sdk.AttributeKeySender, msg.DelegatorAddress), ), }) return &types.MsgCreateValidatorResponse{}, nil } // EditValidator defines a method for editing an existing validator func (k msgServer) EditValidator(goCtx context.Context, msg *types.MsgEditValidator) (*types.MsgEditValidatorResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) valAddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddress) if err != nil { return nil, err } // validator must already be registered validator, found := k.GetValidator(ctx, valAddr) if !found { return nil, types.ErrNoValidatorFound } // replace all editable fields (clients should autofill existing values) description, err := validator.Description.UpdateDescription(msg.Description) if err != nil { return nil, err } validator.Description = description if msg.CommissionRate != nil { commission, err := k.UpdateValidatorCommission(ctx, validator, *msg.CommissionRate) if err != nil { return nil, err } // call the before-modification hook since we're about to update the commission if err := k.BeforeValidatorModified(ctx, valAddr); err != nil { return nil, err } validator.Commission = commission } if msg.MinSelfDelegation != nil { if !msg.MinSelfDelegation.GT(validator.MinSelfDelegation) { return nil, types.ErrMinSelfDelegationDecreased } if msg.MinSelfDelegation.GT(validator.Tokens) { return nil, types.ErrSelfDelegationBelowMinimum } validator.MinSelfDelegation = *msg.MinSelfDelegation } k.SetValidator(ctx, validator) ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeEditValidator, sdk.NewAttribute(types.AttributeKeyCommissionRate, validator.Commission.String()), sdk.NewAttribute(types.AttributeKeyMinSelfDelegation, validator.MinSelfDelegation.String()), ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(sdk.AttributeKeySender, msg.ValidatorAddress), ), }) return &types.MsgEditValidatorResponse{}, nil } // Delegate defines a method for performing a delegation of coins from a delegator to a validator func (k msgServer) Delegate(goCtx context.Context, msg *types.MsgDelegate) (*types.MsgDelegateResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) valAddr, valErr := sdk.ValAddressFromBech32(msg.ValidatorAddress) if valErr != nil { return nil, valErr } validator, found := k.GetValidator(ctx, valAddr) if !found { return nil, types.ErrNoValidatorFound } delegatorAddress, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) if err != nil { return nil, err } bondDenom := k.BondDenom(ctx) if msg.Amount.Denom != bondDenom { return nil, sdkerrors.Wrapf( sdkerrors.ErrInvalidRequest, "invalid coin denomination: got %s, expected %s", msg.Amount.Denom, bondDenom, ) } // NOTE: source funds are always unbonded newShares, err := k.Keeper.Delegate(ctx, delegatorAddress, msg.Amount.Amount, types.Unbonded, validator, true) if err != nil { return nil, err } if msg.Amount.Amount.IsInt64() { defer func() { telemetry.IncrCounter(1, types.ModuleName, "delegate") telemetry.SetGaugeWithLabels( []string{"tx", "msg", msg.Type()}, float32(msg.Amount.Amount.Int64()), []metrics.Label{telemetry.NewLabel("denom", msg.Amount.Denom)}, ) }() } ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeDelegate, sdk.NewAttribute(types.AttributeKeyValidator, msg.ValidatorAddress), sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()), sdk.NewAttribute(types.AttributeKeyNewShares, newShares.String()), ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(sdk.AttributeKeySender, msg.DelegatorAddress), ), }) return &types.MsgDelegateResponse{}, nil } // BeginRedelegate defines a method for performing a redelegation of coins from a delegator and source validator to a destination validator func (k msgServer) BeginRedelegate(goCtx context.Context, msg *types.MsgBeginRedelegate) (*types.MsgBeginRedelegateResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) valSrcAddr, err := sdk.ValAddressFromBech32(msg.ValidatorSrcAddress) if err != nil { return nil, err } delegatorAddress, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) if err != nil { return nil, err } shares, err := k.ValidateUnbondAmount( ctx, delegatorAddress, valSrcAddr, msg.Amount.Amount, ) if err != nil { return nil, err } bondDenom := k.BondDenom(ctx) if msg.Amount.Denom != bondDenom { return nil, sdkerrors.Wrapf( sdkerrors.ErrInvalidRequest, "invalid coin denomination: got %s, expected %s", msg.Amount.Denom, bondDenom, ) } valDstAddr, err := sdk.ValAddressFromBech32(msg.ValidatorDstAddress) if err != nil { return nil, err } completionTime, err := k.BeginRedelegation( ctx, delegatorAddress, valSrcAddr, valDstAddr, shares, ) if err != nil { return nil, err } if msg.Amount.Amount.IsInt64() { defer func() { telemetry.IncrCounter(1, types.ModuleName, "redelegate") telemetry.SetGaugeWithLabels( []string{"tx", "msg", msg.Type()}, float32(msg.Amount.Amount.Int64()), []metrics.Label{telemetry.NewLabel("denom", msg.Amount.Denom)}, ) }() } ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeRedelegate, sdk.NewAttribute(types.AttributeKeySrcValidator, msg.ValidatorSrcAddress), sdk.NewAttribute(types.AttributeKeyDstValidator, msg.ValidatorDstAddress), sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()), sdk.NewAttribute(types.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(sdk.AttributeKeySender, msg.DelegatorAddress), ), }) return &types.MsgBeginRedelegateResponse{ CompletionTime: completionTime, }, nil } // Undelegate defines a method for performing an undelegation from a delegate and a validator func (k msgServer) Undelegate(goCtx context.Context, msg *types.MsgUndelegate) (*types.MsgUndelegateResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) addr, err := sdk.ValAddressFromBech32(msg.ValidatorAddress) if err != nil { return nil, err } delegatorAddress, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) if err != nil { return nil, err } shares, err := k.ValidateUnbondAmount( ctx, delegatorAddress, addr, msg.Amount.Amount, ) if err != nil { return nil, err } bondDenom := k.BondDenom(ctx) if msg.Amount.Denom != bondDenom { return nil, sdkerrors.Wrapf( sdkerrors.ErrInvalidRequest, "invalid coin denomination: got %s, expected %s", msg.Amount.Denom, bondDenom, ) } completionTime, err := k.Keeper.Undelegate(ctx, delegatorAddress, addr, shares) if err != nil { return nil, err } if msg.Amount.Amount.IsInt64() { defer func() { telemetry.IncrCounter(1, types.ModuleName, "undelegate") telemetry.SetGaugeWithLabels( []string{"tx", "msg", msg.Type()}, float32(msg.Amount.Amount.Int64()), []metrics.Label{telemetry.NewLabel("denom", msg.Amount.Denom)}, ) }() } ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeUnbond, sdk.NewAttribute(types.AttributeKeyValidator, msg.ValidatorAddress), sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()), sdk.NewAttribute(types.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)), ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(sdk.AttributeKeySender, msg.DelegatorAddress), ), }) return &types.MsgUndelegateResponse{ CompletionTime: completionTime, }, nil } // CancelUnbondingDelegation defines a method for canceling the unbonding delegation // and delegate back to the validator. func (k msgServer) CancelUnbondingDelegation(goCtx context.Context, msg *types.MsgCancelUnbondingDelegation) (*types.MsgCancelUnbondingDelegationResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) valAddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddress) if err != nil { return nil, err } delegatorAddress, err := sdk.AccAddressFromBech32(msg.DelegatorAddress) if err != nil { return nil, err } bondDenom := k.BondDenom(ctx) if msg.Amount.Denom != bondDenom { return nil, sdkerrors.Wrapf( sdkerrors.ErrInvalidRequest, "invalid coin denomination: got %s, expected %s", msg.Amount.Denom, bondDenom, ) } validator, found := k.GetValidator(ctx, valAddr) if !found { return nil, types.ErrNoValidatorFound } // In some situations, the exchange rate becomes invalid, e.g. if // Validator loses all tokens due to slashing. In this case, // make all future delegations invalid. if validator.InvalidExRate() { return nil, types.ErrDelegatorShareExRateInvalid } if validator.IsJailed() { return nil, types.ErrValidatorJailed } ubd, found := k.GetUnbondingDelegation(ctx, delegatorAddress, valAddr) if !found { return nil, status.Errorf( codes.NotFound, "unbonding delegation with delegator %s not found for validator %s", msg.DelegatorAddress, msg.ValidatorAddress, ) } var ( unbondEntry types.UnbondingDelegationEntry unbondEntryIndex int64 = -1 ) for i, entry := range ubd.Entries { if entry.CreationHeight == msg.CreationHeight { unbondEntry = entry unbondEntryIndex = int64(i) break } } if unbondEntryIndex == -1 { return nil, sdkerrors.ErrNotFound.Wrapf("unbonding delegation entry is not found at block height %d", msg.CreationHeight) } if unbondEntry.Balance.LT(msg.Amount.Amount) { return nil, sdkerrors.ErrInvalidRequest.Wrap("amount is greater than the unbonding delegation entry balance") } if unbondEntry.CompletionTime.Before(ctx.BlockTime()) { return nil, sdkerrors.ErrInvalidRequest.Wrap("unbonding delegation is already processed") } // delegate back the unbonding delegation amount to the validator _, err = k.Keeper.Delegate(ctx, delegatorAddress, msg.Amount.Amount, types.Unbonding, validator, false) if err != nil { return nil, err } amount := unbondEntry.Balance.Sub(msg.Amount.Amount) if amount.IsZero() { ubd.RemoveEntry(unbondEntryIndex) } else { // update the unbondingDelegationEntryBalance and InitialBalance for ubd entry unbondEntry.Balance = amount unbondEntry.InitialBalance = unbondEntry.InitialBalance.Sub(msg.Amount.Amount) ubd.Entries[unbondEntryIndex] = unbondEntry } // set the unbonding delegation or remove it if there are no more entries if len(ubd.Entries) == 0 { k.RemoveUnbondingDelegation(ctx, ubd) } else { k.SetUnbondingDelegation(ctx, ubd) } ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeCancelUnbondingDelegation, sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()), sdk.NewAttribute(types.AttributeKeyValidator, msg.ValidatorAddress), sdk.NewAttribute(types.AttributeKeyDelegator, msg.DelegatorAddress), sdk.NewAttribute(types.AttributeKeyCreationHeight, strconv.FormatInt(msg.CreationHeight, 10)), ), ) return &types.MsgCancelUnbondingDelegationResponse{}, nil }