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(x/feegrant): Add limits to grant pruning and enable message to aid manually (backport #18047) #18128

Merged
merged 17 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 9 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
958 changes: 920 additions & 38 deletions api/cosmos/feegrant/v1beta1/tx.pulsar.go

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions api/cosmos/feegrant/v1beta1/tx_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions proto/cosmos/feegrant/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ service Msg {
// RevokeAllowance revokes any fee allowance of granter's account that
// has been granted to the grantee.
rpc RevokeAllowance(MsgRevokeAllowance) returns (MsgRevokeAllowanceResponse);

// PruneAllowances prunes expired fee allowances, currently up to 75 at a time.
//
// Since cosmos-sdk 0.50
rpc PruneAllowances(MsgPruneAllowances) returns (MsgPruneAllowancesResponse);
}

// MsgGrantAllowance adds permission for Grantee to spend up to Allowance
Expand Down Expand Up @@ -55,3 +60,18 @@ message MsgRevokeAllowance {

// MsgRevokeAllowanceResponse defines the Msg/RevokeAllowanceResponse response type.
message MsgRevokeAllowanceResponse {}

// MsgPruneAllowances prunes expired fee allowances.
//
// Since cosmos-sdk 0.50
message MsgPruneAllowances {
option (cosmos.msg.v1.signer) = "pruner";

// pruner is the address of the user pruning expired allowances.
string pruner = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}

// MsgPruneAllowancesResponse defines the Msg/PruneAllowancesResponse response type.
//
// Since cosmos-sdk 0.50
message MsgPruneAllowancesResponse {}
2 changes: 1 addition & 1 deletion x/bank/migrations/v4/gen_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,6 @@ func TestMigrateGenState(t *testing.T) {
},
}
_ = v4.MigrateGenState(&origState)
assert.Len(t, origState.Params.SendEnabled, 2) //nolint:staticcheck // SA1019: keep for test
assert.Len(t, origState.Params.SendEnabled, 2)
})
}
1 change: 1 addition & 0 deletions x/feegrant/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

* [#18047](https://github.com/cosmos/cosmos-sdk/pull/18047) Added a limit of 200 grants pruned per EndBlock and the method PruneAllowances that prunes 75 expired grants on every run.
* [#14649](https://github.com/cosmos/cosmos-sdk/pull/14649) The `x/feegrant` module is extracted to have a separate go.mod file which allows it to be a standalone module.

### API Breaking Changes
Expand Down
8 changes: 8 additions & 0 deletions x/feegrant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,14 @@ The feegrant module emits the following events:
| message | granter | {granterAddress} |
| message | grantee | {granteeAddress} |

### Prune fee allowances

| Type | Attribute Key | Attribute Value |
| ------- | ------------- | ---------------- |
| message | action | prune_feegrant |
| message | pruner | {prunerAddress} |


## Client

### CLI
Expand Down
2 changes: 2 additions & 0 deletions x/feegrant/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ const (
EventTypeRevokeFeeGrant = "revoke_feegrant"
EventTypeSetFeeGrant = "set_feegrant"
EventTypeUpdateFeeGrant = "update_feegrant"
EventTypePruneFeeGrant = "prune_feegrant"

AttributeKeyGranter = "granter"
AttributeKeyGrantee = "grantee"
AttributeKeyPruner = "pruner"
)
16 changes: 9 additions & 7 deletions x/feegrant/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,7 @@ func (k Keeper) IterateAllFeeAllowances(ctx context.Context, cb func(grant feegr

// UseGrantedFees will try to pay the given fee from the granter's account as requested by the grantee
func (k Keeper) UseGrantedFees(ctx context.Context, granter, grantee sdk.AccAddress, fee sdk.Coins, msgs []sdk.Msg) error {
f, err := k.getGrant(ctx, granter, grantee)
if err != nil {
return err
}

grant, err := f.GetGrant()
grant, err := k.GetAllowance(ctx, granter, grantee)
if err != nil {
return err
}
Expand Down Expand Up @@ -322,16 +317,23 @@ func (k Keeper) addToFeeAllowanceQueue(ctx context.Context, grantKey []byte, exp
}

// RemoveExpiredAllowances iterates grantsByExpiryQueue and deletes the expired grants.
func (k Keeper) RemoveExpiredAllowances(ctx context.Context) error {
func (k Keeper) RemoveExpiredAllowances(ctx context.Context, limit int32) error {
exp := sdk.UnwrapSDKContext(ctx).BlockTime()
store := k.storeService.OpenKVStore(ctx)
iterator, err := store.Iterator(feegrant.FeeAllowanceQueueKeyPrefix, storetypes.InclusiveEndBytes(feegrant.AllowanceByExpTimeKey(&exp)))
var count int32
if err != nil {
return err
}
defer iterator.Close()

for ; iterator.Valid(); iterator.Next() {
// limit the amount of iterations to avoid taking too much time
count++
if count == limit {
return nil
}

err = store.Delete(iterator.Key())
if err != nil {
return err
Expand Down
13 changes: 7 additions & 6 deletions x/feegrant/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,17 @@ func TestKeeperTestSuite(t *testing.T) {
}

func (suite *KeeperTestSuite) SetupTest() {
suite.addrs = simtestutil.CreateIncrementalAccounts(4)
suite.addrs = simtestutil.CreateIncrementalAccounts(20)
key := storetypes.NewKVStoreKey(feegrant.StoreKey)
testCtx := testutil.DefaultContextWithDB(suite.T(), key, storetypes.NewTransientStoreKey("transient_test"))
encCfg := moduletestutil.MakeTestEncodingConfig(module.AppModuleBasic{})

// setup gomock and initialize some globally expected executions
ctrl := gomock.NewController(suite.T())
suite.accountKeeper = feegranttestutil.NewMockAccountKeeper(ctrl)
suite.accountKeeper.EXPECT().GetAccount(gomock.Any(), suite.addrs[0]).Return(authtypes.NewBaseAccountWithAddress(suite.addrs[0])).AnyTimes()
suite.accountKeeper.EXPECT().GetAccount(gomock.Any(), suite.addrs[1]).Return(authtypes.NewBaseAccountWithAddress(suite.addrs[1])).AnyTimes()
suite.accountKeeper.EXPECT().GetAccount(gomock.Any(), suite.addrs[2]).Return(authtypes.NewBaseAccountWithAddress(suite.addrs[2])).AnyTimes()
suite.accountKeeper.EXPECT().GetAccount(gomock.Any(), suite.addrs[3]).Return(authtypes.NewBaseAccountWithAddress(suite.addrs[3])).AnyTimes()
for i := 0; i < len(suite.addrs); i++ {
suite.accountKeeper.EXPECT().GetAccount(gomock.Any(), suite.addrs[i]).Return(authtypes.NewBaseAccountWithAddress(suite.addrs[i])).AnyTimes()
}

suite.accountKeeper.EXPECT().AddressCodec().Return(codecaddress.NewBech32Codec("cosmos")).AnyTimes()

Expand Down Expand Up @@ -395,7 +394,9 @@ func (suite *KeeperTestSuite) TestPruneGrants() {
}
err := suite.feegrantKeeper.GrantAllowance(suite.ctx, tc.granter, tc.grantee, tc.allowance)
suite.NoError(err)
suite.feegrantKeeper.RemoveExpiredAllowances(tc.ctx)
err = suite.feegrantKeeper.RemoveExpiredAllowances(tc.ctx, 5)
suite.NoError(err)

grant, err := suite.feegrantKeeper.GetAllowance(tc.ctx, tc.granter, tc.grantee)
if tc.expErrMsg != "" {
suite.Error(err)
Expand Down
19 changes: 19 additions & 0 deletions x/feegrant/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,22 @@ func (k msgServer) RevokeAllowance(goCtx context.Context, msg *feegrant.MsgRevok

return &feegrant.MsgRevokeAllowanceResponse{}, nil
}

// PruneAllowances removes expired allowances from the store.
func (k msgServer) PruneAllowances(ctx context.Context, req *feegrant.MsgPruneAllowances) (*feegrant.MsgPruneAllowancesResponse, error) {
// 75 is an arbitrary value, we can change it later if needed
err := k.RemoveExpiredAllowances(ctx, 75)
if err != nil {
return nil, err
}

sdkCtx := sdk.UnwrapSDKContext(ctx)
sdkCtx.EventManager().EmitEvent(
sdk.NewEvent(
feegrant.EventTypePruneFeeGrant,
sdk.NewAttribute(feegrant.AttributeKeyPruner, req.Pruner),
),
)

return &feegrant.MsgPruneAllowancesResponse{}, nil
}
64 changes: 64 additions & 0 deletions x/feegrant/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/golang/mock/gomock"

"cosmossdk.io/core/header"
"cosmossdk.io/x/feegrant"

codecaddress "github.com/cosmos/cosmos-sdk/codec/address"
Expand Down Expand Up @@ -284,3 +285,66 @@ func (suite *KeeperTestSuite) TestRevokeAllowance() {
})
}
}

func (suite *KeeperTestSuite) TestPruneAllowances() {
ctx := suite.ctx.WithHeaderInfo(header.Info{Time: time.Now()})
oneYear := ctx.HeaderInfo().Time.AddDate(1, 0, 0)

// We create 76 allowances, all expiring in one year
count := 0
for i := 0; i < len(suite.addrs); i++ {
for j := 0; j < len(suite.addrs); j++ {
if count == 76 {
break
}
if suite.addrs[i].String() == suite.addrs[j].String() {
continue
}

any, err := codectypes.NewAnyWithValue(&feegrant.BasicAllowance{
SpendLimit: suite.atom,
Expiration: &oneYear,
})
suite.Require().NoError(err)
req := &feegrant.MsgGrantAllowance{
Granter: suite.addrs[i].String(),
Grantee: suite.addrs[j].String(),
Allowance: any,
}

_, err = suite.msgSrvr.GrantAllowance(ctx, req)
if err != nil {
// do not fail, just try with another pair
continue
}

count++
}
}

// we have 76 allowances
count = 0
err := suite.feegrantKeeper.IterateAllFeeAllowances(ctx, func(grant feegrant.Grant) bool {
count++
return false
})
suite.Require().NoError(err)
suite.Require().Equal(76, count)

// after a year and one day passes, they are all expired
oneYearAndADay := ctx.HeaderInfo().Time.AddDate(1, 0, 1)
ctx = suite.ctx.WithHeaderInfo(header.Info{Time: oneYearAndADay})

// we prune them, but currently only 75 will be pruned
_, err = suite.msgSrvr.PruneAllowances(ctx, &feegrant.MsgPruneAllowances{})
suite.Require().NoError(err)

// we have 1 allowance left
count = 0
err = suite.feegrantKeeper.IterateAllFeeAllowances(ctx, func(grant feegrant.Grant) bool {
count++
return false
})
suite.Require().NoError(err)
suite.Require().Equal(1, count)
}
12 changes: 5 additions & 7 deletions x/feegrant/module/abci.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package module

import (
"cosmossdk.io/x/feegrant/keeper"
"context"

sdk "github.com/cosmos/cosmos-sdk/types"
"cosmossdk.io/x/feegrant/keeper"
)

func EndBlocker(ctx sdk.Context, k keeper.Keeper) {
err := k.RemoveExpiredAllowances(ctx)
if err != nil {
panic(err)
}
func EndBlocker(ctx context.Context, k keeper.Keeper) error {
// 200 is an arbitrary value, we can change it later if needed
return k.RemoveExpiredAllowances(ctx, 200)
}
2 changes: 1 addition & 1 deletion x/feegrant/module/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestFeegrantPruning(t *testing.T) {
feegrant.RegisterQueryServer(queryHelper, feegrantKeeper)
queryClient := feegrant.NewQueryClient(queryHelper)

module.EndBlocker(testCtx.Ctx, feegrantKeeper)
require.NoError(t, module.EndBlocker(testCtx.Ctx, feegrantKeeper))

res, err := queryClient.Allowances(testCtx.Ctx.Context(), &feegrant.QueryAllowancesRequest{
Grantee: grantee.String(),
Expand Down
8 changes: 7 additions & 1 deletion x/feegrant/module/autocli.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,14 @@ You can find the fee-grant of a granter and grantee.`),
{ProtoField: "grantee"},
},
},
{
RpcMethod: "PruneAllowances",
Use: "prune",
Short: "Prune expired allowances",
Long: "Prune up to 75 expired allowances in order to reduce the size of the store when the number of expired allowances is large.",
Example: fmt.Sprintf(`$ %s tx feegrant prune --from [mykey]`, version.AppName),
},
},
EnhanceCustomCommand: true,
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved
},
}
}
4 changes: 1 addition & 3 deletions x/feegrant/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,7 @@ func (AppModule) ConsensusVersion() uint64 { return 2 }
// EndBlock returns the end blocker for the feegrant module. It returns no validator
// updates.
func (am AppModule) EndBlock(ctx context.Context) error {
c := sdk.UnwrapSDKContext(ctx)
EndBlocker(c, am.keeper)
return nil
return EndBlocker(ctx, am.keeper)
}

func init() {
Expand Down
Loading
Loading