Skip to content

Commit

Permalink
x/authz charge gas for expensive authorizations (cosmos#8995)
Browse files Browse the repository at this point in the history
* WIP

* Add consume gas

* update authz.go

* add build flag

* Update x/staking/types/authz.go

* Update x/staking/types/authz.go

* add tests docs

* review changes

* fix operations

* Update x/authz/types/msgs.go

Co-authored-by: Amaury <[email protected]>

* review changes

* update gas cost

* review changes

Co-authored-by: SaReN <[email protected]>
Co-authored-by: Amaury <[email protected]>
  • Loading branch information
3 people authored Apr 9, 2021
1 parent 338e9a4 commit 8a376a4
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 37 deletions.
7 changes: 7 additions & 0 deletions types/msgservice/msg_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package msgservice
import (
"context"
"fmt"
"strings"

"github.com/gogo/protobuf/proto"
"google.golang.org/grpc"
Expand Down Expand Up @@ -42,3 +43,9 @@ func RegisterMsgServiceDesc(registry codectypes.InterfaceRegistry, sd *grpc.Serv
func noopInterceptor(_ context.Context, _ interface{}, _ *grpc.UnaryServerInfo, _ grpc.UnaryHandler) (interface{}, error) {
return nil, nil
}

// IsServiceMsg checks if a type URL corresponds to a service method name,
// i.e. /cosmos.bank.Msg/Send vs /cosmos.bank.MsgSend
func IsServiceMsg(typeURL string) bool {
return strings.Count(typeURL, "/") >= 2
}
12 changes: 3 additions & 9 deletions types/tx/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package tx

import (
"fmt"
"strings"

codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/msgservice"
)

// MaxGasWanted defines the max gas allowed.
Expand All @@ -27,7 +27,7 @@ func (t *Tx) GetMsgs() []sdk.Msg {
res := make([]sdk.Msg, len(anys))
for i, any := range anys {
var msg sdk.Msg
if isServiceMsg(any.TypeUrl) {
if msgservice.IsServiceMsg(any.TypeUrl) {
req := any.GetCachedValue()
if req == nil {
panic("Any cached value is nil. Transaction messages must be correctly packed Any values.")
Expand Down Expand Up @@ -183,7 +183,7 @@ func (m *TxBody) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {
for _, any := range m.Messages {
// If the any's typeUrl contains 2 slashes, then we unpack the any into
// a ServiceMsg struct as per ADR-031.
if isServiceMsg(any.TypeUrl) {
if msgservice.IsServiceMsg(any.TypeUrl) {
var req sdk.MsgRequest
err := unpacker.UnpackAny(any, &req)
if err != nil {
Expand Down Expand Up @@ -222,9 +222,3 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) {
registry.RegisterInterface("cosmos.tx.v1beta1.Tx", (*sdk.Tx)(nil))
registry.RegisterImplementations((*sdk.Tx)(nil), &Tx{})
}

// isServiceMsg checks if a type URL corresponds to a service method name,
// i.e. /cosmos.bank.Msg/Send vs /cosmos.bank.MsgSend
func isServiceMsg(typeURL string) bool {
return strings.Count(typeURL, "/") >= 2
}
4 changes: 2 additions & 2 deletions x/authz/client/cli/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ func (s *IntegrationTestSuite) TestCLITxGrantAuthorization() {
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
fmt.Sprintf("--%s=%d", cli.FlagExpiration, twoHours),
},
&sdk.TxResponse{}, 29,
false,
nil, 0,
true,
},
{
"failed with error both validators not allowed",
Expand Down
8 changes: 5 additions & 3 deletions x/authz/exported/authorizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package exported
import (
"github.com/gogo/protobuf/proto"

tmproto "github.com/tendermint/tendermint/proto/tendermint/types"

sdk "github.com/cosmos/cosmos-sdk/types"
)

Expand All @@ -17,5 +15,9 @@ type Authorization interface {

// Accept determines whether this grant permits the provided sdk.ServiceMsg to be performed, and if
// so provides an upgraded authorization instance.
Accept(msg sdk.ServiceMsg, block tmproto.Header) (updated Authorization, delete bool, err error)
Accept(ctx sdk.Context, msg sdk.ServiceMsg) (updated Authorization, delete bool, err error)

// ValidateBasic does a simple validation check that
// doesn't require access to any other information.
ValidateBasic() error
}
2 changes: 1 addition & 1 deletion x/authz/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (k Keeper) DispatchActions(ctx sdk.Context, grantee sdk.AccAddress, service
if authorization == nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "authorization not found")
}
updated, del, err := authorization.Accept(serviceMsg, ctx.BlockHeader())
updated, del, err := authorization.Accept(ctx, serviceMsg)
if err != nil {
return nil, err
}
Expand Down
8 changes: 6 additions & 2 deletions x/authz/simulation/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,12 @@ func SimulateMsgGrantAuthorization(ak types.AccountKeeper, bk types.BankKeeper,
}

blockTime := ctx.BlockTime()
spendLimit := spendableCoins.Sub(fees)
if spendLimit == nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgGrantAuthorization, "spend limit is nil"), nil, nil
}
msg, err := types.NewMsgGrantAuthorization(granter.Address, grantee.Address,
banktype.NewSendAuthorization(spendableCoins.Sub(fees)), blockTime.AddDate(1, 0, 0))
banktype.NewSendAuthorization(spendLimit), blockTime.AddDate(1, 0, 0))

if err != nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgGrantAuthorization, err.Error()), nil, err
Expand Down Expand Up @@ -249,7 +253,7 @@ func SimulateMsgExecuteAuthorized(ak types.AccountKeeper, bk types.BankKeeper, k

msg := types.NewMsgExecAuthorized(grantee.Address, []sdk.ServiceMsg{execMsg})
sendGrant := targetGrant.Authorization.GetCachedValue().(*banktype.SendAuthorization)
_, _, err = sendGrant.Accept(execMsg, ctx.BlockHeader())
_, _, err = sendGrant.Accept(ctx, execMsg)
if err != nil {
return simtypes.NoOpMsg(types.ModuleName, TypeMsgExecDelegated, err.Error()), nil, nil
}
Expand Down
20 changes: 14 additions & 6 deletions x/authz/types/generic_authorization.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package types

import (
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/msgservice"
"github.com/cosmos/cosmos-sdk/x/authz/exported"
)

Expand All @@ -19,11 +19,19 @@ func NewGenericAuthorization(methodName string) *GenericAuthorization {
}

// MethodName implements Authorization.MethodName.
func (cap GenericAuthorization) MethodName() string {
return cap.MessageName
func (authorization GenericAuthorization) MethodName() string {
return authorization.MessageName
}

// Accept implements Authorization.Accept.
func (cap GenericAuthorization) Accept(msg sdk.ServiceMsg, block tmproto.Header) (updated exported.Authorization, delete bool, err error) {
return &cap, false, nil
func (authorization GenericAuthorization) Accept(ctx sdk.Context, msg sdk.ServiceMsg) (updated exported.Authorization, delete bool, err error) {
return &authorization, false, nil
}

// ValidateBasic implements Authorization.ValidateBasic.
func (authorization GenericAuthorization) ValidateBasic() error {
if !msgservice.IsServiceMsg(authorization.MessageName) {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidType, " %s is not a valid service msg", authorization.MessageName)
}
return nil
}
20 changes: 20 additions & 0 deletions x/authz/types/generic_authorization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package types_test

import (
"testing"

"github.com/cosmos/cosmos-sdk/x/authz/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/stretchr/testify/require"
)

func TestGenericAuthorization(t *testing.T) {
t.Log("verify ValidateBasic returns error for non-service msg")
authorization := types.NewGenericAuthorization(banktypes.TypeMsgSend)
require.Error(t, authorization.ValidateBasic())

t.Log("verify ValidateBasic returns nil for service msg")
authorization = types.NewGenericAuthorization(banktypes.SendAuthorization{}.MethodName())
require.NoError(t, authorization.ValidateBasic())
require.Equal(t, banktypes.SendAuthorization{}.MethodName(), authorization.MessageName)
}
6 changes: 5 additions & 1 deletion x/authz/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ func (msg MsgGrantAuthorizationRequest) ValidateBasic() error {
return sdkerrors.Wrap(ErrInvalidExpirationTime, "Time can't be in the past")
}

return nil
authorization, ok := msg.Authorization.GetCachedValue().(exported.Authorization)
if !ok {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "expected %T, got %T", (exported.Authorization)(nil), msg.Authorization.GetCachedValue())
}
return authorization.ValidateBasic()
}

// GetGrantAuthorization returns the cache value from the MsgGrantAuthorization.Authorization if present.
Expand Down
15 changes: 12 additions & 3 deletions x/bank/types/send_authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package types
import (
"reflect"

tmproto "github.com/tendermint/tendermint/proto/tendermint/types"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authz "github.com/cosmos/cosmos-sdk/x/authz/exported"
Expand All @@ -27,7 +25,7 @@ func (authorization SendAuthorization) MethodName() string {
}

// Accept implements Authorization.Accept.
func (authorization SendAuthorization) Accept(msg sdk.ServiceMsg, block tmproto.Header) (updated authz.Authorization, delete bool, err error) {
func (authorization SendAuthorization) Accept(ctx sdk.Context, msg sdk.ServiceMsg) (updated authz.Authorization, delete bool, err error) {
if reflect.TypeOf(msg.Request) == reflect.TypeOf(&MsgSend{}) {
msg, ok := msg.Request.(*MsgSend)
if ok {
Expand All @@ -44,3 +42,14 @@ func (authorization SendAuthorization) Accept(msg sdk.ServiceMsg, block tmproto.
}
return nil, false, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "type mismatch")
}

// ValidateBasic implements Authorization.ValidateBasic.
func (authorization SendAuthorization) ValidateBasic() error {
if authorization.SpendLimit == nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "spend limit cannot be nil")
}
if !authorization.SpendLimit.IsAllPositive() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "spend limit cannot be negitive")
}
return nil
}
64 changes: 64 additions & 0 deletions x/bank/types/send_authorization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package types_test

import (
"testing"

"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/stretchr/testify/require"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)

var (
coins1000 = sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000)))
coins500 = sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(500)))
fromAddr = sdk.AccAddress("_____from _____")
toAddr = sdk.AccAddress("_______to________")
)

func TestSendAuthorization(t *testing.T) {
app := simapp.Setup(false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{})
authorization := types.NewSendAuthorization(coins1000)

t.Log("verify authorization returns valid method name")
require.Equal(t, authorization.MethodName(), "/cosmos.bank.v1beta1.Msg/Send")
require.NoError(t, authorization.ValidateBasic())
send := types.NewMsgSend(fromAddr, toAddr, coins1000)
srvMsg := sdk.ServiceMsg{
MethodName: "/cosmos.bank.v1beta1.Msg/Send",
Request: send,
}
require.NoError(t, authorization.ValidateBasic())

t.Log("verify updated authorization returns nil")
updated, del, err := authorization.Accept(ctx, srvMsg)
require.NoError(t, err)
require.True(t, del)
require.Nil(t, updated)

authorization = types.NewSendAuthorization(coins1000)
require.Equal(t, authorization.MethodName(), "/cosmos.bank.v1beta1.Msg/Send")
require.NoError(t, authorization.ValidateBasic())
send = types.NewMsgSend(fromAddr, toAddr, coins500)
srvMsg = sdk.ServiceMsg{
MethodName: "/cosmos.bank.v1beta1.Msg/Send",
Request: send,
}
require.NoError(t, authorization.ValidateBasic())
updated, del, err = authorization.Accept(ctx, srvMsg)

t.Log("verify updated authorization returns remaining spent limit")
require.NoError(t, err)
require.False(t, del)
require.NotNil(t, updated)
sendAuth := types.NewSendAuthorization(coins500)
require.Equal(t, sendAuth.String(), updated.String())

t.Log("expect updated authorization nil after spending remaining amount")
updated, del, err = updated.Accept(ctx, srvMsg)
require.NoError(t, err)
require.True(t, del)
require.Nil(t, updated)
}
31 changes: 24 additions & 7 deletions x/staking/types/authz.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package types

import (
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authz "github.com/cosmos/cosmos-sdk/x/authz/exported"
)

// TODO: Revisit this once we have propoer gas fee framework.
// Tracking issues https://github.com/cosmos/cosmos-sdk/issues/9054, https://github.com/cosmos/cosmos-sdk/discussions/9072
const gasCostPerIteration = uint64(10)

var (
_ authz.Authorization = &StakeAuthorization{}
TypeDelegate = "/cosmos.staking.v1beta1.Msg/Delegate"
TypeUndelegate = "/cosmos.staking.v1beta1.Msg/Undelegate"
TypeBeginRedelegate = "/cosmos.staking.v1beta1.Msg/BeginRedelegate"
_ authz.Authorization = &StakeAuthorization{}

TypeDelegate = "/cosmos.staking.v1beta1.Msg/Delegate"
TypeUndelegate = "/cosmos.staking.v1beta1.Msg/Undelegate"
TypeBeginRedelegate = "/cosmos.staking.v1beta1.Msg/BeginRedelegate"
)

// NewStakeAuthorization creates a new StakeAuthorization object.
Expand Down Expand Up @@ -46,8 +49,19 @@ func (authorization StakeAuthorization) MethodName() string {
return authzType
}

func (authorization StakeAuthorization) ValidateBasic() error {
if authorization.MaxTokens != nil && authorization.MaxTokens.IsNegative() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "negative coin amount: %v", authorization.MaxTokens)
}
if authorization.AuthorizationType == AuthorizationType_AUTHORIZATION_TYPE_UNSPECIFIED {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "unknown authorization type")
}

return nil
}

// Accept implements Authorization.Accept.
func (authorization StakeAuthorization) Accept(msg sdk.ServiceMsg, block tmproto.Header) (updated authz.Authorization, delete bool, err error) {
func (authorization StakeAuthorization) Accept(ctx sdk.Context, msg sdk.ServiceMsg) (updated authz.Authorization, delete bool, err error) {
var validatorAddress string
var amount sdk.Coin

Expand All @@ -68,13 +82,16 @@ func (authorization StakeAuthorization) Accept(msg sdk.ServiceMsg, block tmproto
isValidatorExists := false
allowedList := authorization.GetAllowList().GetAddress()
for _, validator := range allowedList {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "stake authorization")
if validator == validatorAddress {
isValidatorExists = true
break
}
}

denyList := authorization.GetDenyList().GetAddress()
for _, validator := range denyList {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "stake authorization")
if validator == validatorAddress {
return nil, false, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, " cannot delegate/undelegate to %s validator", validator)
}
Expand Down
15 changes: 12 additions & 3 deletions x/staking/types/authz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/stretchr/testify/require"

"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)
Expand All @@ -21,13 +22,21 @@ var (
)

func TestAuthzAuthorizations(t *testing.T) {
app := simapp.Setup(false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{})

// verify ValidateBasic returns error for the AUTHORIZATION_TYPE_UNSPECIFIED authorization type
delAuth, err := stakingtypes.NewStakeAuthorization([]sdk.ValAddress{val1, val2}, []sdk.ValAddress{}, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_UNSPECIFIED, &coin100)
require.NoError(t, err)
require.Error(t, delAuth.ValidateBasic())

// verify MethodName
delAuth, _ := stakingtypes.NewStakeAuthorization([]sdk.ValAddress{val1, val2}, []sdk.ValAddress{}, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, &coin100)
delAuth, err = stakingtypes.NewStakeAuthorization([]sdk.ValAddress{val1, val2}, []sdk.ValAddress{}, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, &coin100)
require.NoError(t, err)
require.Equal(t, delAuth.MethodName(), stakingtypes.TypeDelegate)

// error both allow & deny list
_, err := stakingtypes.NewStakeAuthorization([]sdk.ValAddress{val1, val2}, []sdk.ValAddress{val1}, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, &coin100)
_, err = stakingtypes.NewStakeAuthorization([]sdk.ValAddress{val1, val2}, []sdk.ValAddress{val1}, stakingtypes.AuthorizationType_AUTHORIZATION_TYPE_DELEGATE, &coin100)
require.Error(t, err)

// verify MethodName
Expand Down Expand Up @@ -243,7 +252,7 @@ func TestAuthzAuthorizations(t *testing.T) {
t.Run(tc.msg, func(t *testing.T) {
delAuth, err := stakingtypes.NewStakeAuthorization(tc.allowed, tc.denied, tc.msgType, tc.limit)
require.NoError(t, err)
updated, del, err := delAuth.Accept(tc.srvMsg, tmproto.Header{})
updated, del, err := delAuth.Accept(ctx, tc.srvMsg)
if tc.expectErr {
require.Error(t, err)
require.Equal(t, tc.isDelete, del)
Expand Down

0 comments on commit 8a376a4

Please sign in to comment.