diff --git a/CHANGELOG.md b/CHANGELOG.md index 955e47976ac1..d585b0654cdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -202,6 +202,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [\#10868](https://github.com/cosmos/cosmos-sdk/pull/10868) Bump gov to v1beta2. Both v1beta1 and v1beta2 queries and Msgs are accepted. * [\#11011](https://github.com/cosmos/cosmos-sdk/pull/11011) Remove burning of deposits when qourum is not reached on a governance proposal and when the deposit is not fully met. * [\#11019](https://github.com/cosmos/cosmos-sdk/pull/11019) Add `MsgCreatePermanentLockedAccount` and CLI method for creating permanent locked account +* (x/feegrant) [\#10830](https://github.com/cosmos/cosmos-sdk/pull/10830) Expired allowances will be pruned from state. ### Deprecated diff --git a/x/feegrant/basic_fee.go b/x/feegrant/basic_fee.go index 85ba8ab2564a..703b509b3a68 100644 --- a/x/feegrant/basic_fee.go +++ b/x/feegrant/basic_fee.go @@ -1,6 +1,8 @@ package feegrant import ( + time "time" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -52,3 +54,7 @@ func (a BasicAllowance) ValidateBasic() error { return nil } + +func (a BasicAllowance) ExpiresAt() (*time.Time, error) { + return a.Expiration, nil +} diff --git a/x/feegrant/events.go b/x/feegrant/events.go index a4470b82704e..67aafafb4a35 100644 --- a/x/feegrant/events.go +++ b/x/feegrant/events.go @@ -5,6 +5,7 @@ const ( EventTypeUseFeeGrant = "use_feegrant" EventTypeRevokeFeeGrant = "revoke_feegrant" EventTypeSetFeeGrant = "set_feegrant" + EventTypeUpdateFeeGrant = "update_feegrant" AttributeKeyGranter = "granter" AttributeKeyGrantee = "grantee" diff --git a/x/feegrant/fees.go b/x/feegrant/fees.go index bd8c682608fd..9b2c03338685 100644 --- a/x/feegrant/fees.go +++ b/x/feegrant/fees.go @@ -1,6 +1,8 @@ package feegrant import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -22,4 +24,7 @@ type FeeAllowanceI interface { // ValidateBasic should evaluate this FeeAllowance for internal consistency. // Don't allow negative amounts, or negative periods for example. ValidateBasic() error + + // ExpiresAt returns the expiry time of the allowance. + ExpiresAt() (*time.Time, error) } diff --git a/x/feegrant/filtered_fee.go b/x/feegrant/filtered_fee.go index d1b5b0a81ee6..aeef392feb46 100644 --- a/x/feegrant/filtered_fee.go +++ b/x/feegrant/filtered_fee.go @@ -1,6 +1,8 @@ package feegrant import ( + "time" + "github.com/gogo/protobuf/proto" "github.com/cosmos/cosmos-sdk/codec/types" @@ -120,3 +122,11 @@ func (a *AllowedMsgAllowance) ValidateBasic() error { return allowance.ValidateBasic() } + +func (a *AllowedMsgAllowance) ExpiresAt() (*time.Time, error) { + allowance, err := a.GetAllowance() + if err != nil { + return nil, err + } + return allowance.ExpiresAt() +} diff --git a/x/feegrant/keeper/grpc_query.go b/x/feegrant/keeper/grpc_query.go index c439d0b73523..027eba648aa0 100644 --- a/x/feegrant/keeper/grpc_query.go +++ b/x/feegrant/keeper/grpc_query.go @@ -110,10 +110,12 @@ func (q Keeper) AllowancesByGranter(c context.Context, req *feegrant.QueryAllowa var grants []*feegrant.Grant store := ctx.KVStore(q.storeKey) - pageRes, err := query.Paginate(store, req.Pagination, func(key []byte, value []byte) error { + prefixStore := prefix.NewStore(store, feegrant.FeeAllowanceKeyPrefix) + pageRes, err := query.Paginate(prefixStore, req.Pagination, func(key []byte, value []byte) error { var grant feegrant.Grant - granter, _ := feegrant.ParseAddressesFromFeeAllowanceKey(key) + // ParseAddressesFromFeeAllowanceKey expects the full key including the prefix. + granter, _ := feegrant.ParseAddressesFromFeeAllowanceKey(append(feegrant.FeeAllowanceKeyPrefix, key...)) if !granter.Equals(granterAddr) { return nil } diff --git a/x/feegrant/keeper/keeper.go b/x/feegrant/keeper/keeper.go index 5d4d79fabc40..408273f91af7 100644 --- a/x/feegrant/keeper/keeper.go +++ b/x/feegrant/keeper/keeper.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + "time" storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/tendermint/tendermint/libs/log" @@ -49,6 +50,45 @@ func (k Keeper) GrantAllowance(ctx sdk.Context, granter, grantee sdk.AccAddress, store := ctx.KVStore(k.storeKey) key := feegrant.FeeAllowanceKey(granter, grantee) + + var oldExp *time.Time + existingGrant, err := k.getGrant(ctx, grantee, granter) + if err != nil && existingGrant != nil && existingGrant.GetAllowance() != nil { + grantInfo, err := existingGrant.GetGrant() + if err != nil { + return err + } + + oldExp, err = grantInfo.ExpiresAt() + if err != nil { + return err + } + } + + newExp, err := feeAllowance.ExpiresAt() + if err != nil { + return err + } else if newExp != nil && newExp.Before(ctx.BlockTime()) { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "expiration is before current block time") + } else if oldExp == nil && newExp != nil { + // when old oldExp is nil there won't be any key added before to queue. + // add the new key to queue directly. + k.addToFeeAllowanceQueue(ctx, key[1:], newExp) + } else if oldExp != nil && newExp == nil { + // when newExp is nil no need of adding the key to the pruning queue + // remove the old key from queue. + k.removeFromGrantQueue(ctx, oldExp, key[1:]) + } else if oldExp != nil && newExp != nil && !oldExp.Equal(*newExp) { + // `key` formed here with the prefix of `FeeAllowanceKeyPrefix` (which is `0x00`) + // remove the 1st byte and reuse the remaining key as it is. + + // remove the old key from queue. + k.removeFromGrantQueue(ctx, oldExp, key[1:]) + + // add the new key to queue. + k.addToFeeAllowanceQueue(ctx, key[1:], newExp) + } + grant, err := feegrant.NewGrant(granter, grantee, feeAllowance) if err != nil { return err @@ -72,6 +112,39 @@ func (k Keeper) GrantAllowance(ctx sdk.Context, granter, grantee sdk.AccAddress, return nil } +// UpdateAllowance updates the existing grant. +func (k Keeper) UpdateAllowance(ctx sdk.Context, granter, grantee sdk.AccAddress, feeAllowance feegrant.FeeAllowanceI) error { + store := ctx.KVStore(k.storeKey) + key := feegrant.FeeAllowanceKey(granter, grantee) + + _, err := k.getGrant(ctx, granter, grantee) + if err != nil { + return err + } + + grant, err := feegrant.NewGrant(granter, grantee, feeAllowance) + if err != nil { + return err + } + + bz, err := k.cdc.Marshal(&grant) + if err != nil { + return err + } + + store.Set(key, bz) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + feegrant.EventTypeUpdateFeeGrant, + sdk.NewAttribute(feegrant.AttributeKeyGranter, grant.Granter), + sdk.NewAttribute(feegrant.AttributeKeyGrantee, grant.Grantee), + ), + ) + + return nil +} + // revokeAllowance removes an existing grant func (k Keeper) revokeAllowance(ctx sdk.Context, granter, grantee sdk.AccAddress) error { _, err := k.getGrant(ctx, granter, grantee) @@ -177,7 +250,7 @@ func (k Keeper) UseGrantedFees(ctx sdk.Context, granter, grantee sdk.AccAddress, emitUseGrantEvent(ctx, granter.String(), grantee.String()) // if fee allowance is accepted, store the updated state of the allowance - return k.GrantAllowance(ctx, granter, grantee, grant) + return k.UpdateAllowance(ctx, granter, grantee, grant) } func emitUseGrantEvent(ctx sdk.Context, granter, grantee string) { @@ -228,3 +301,30 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) (*feegrant.GenesisState, error) { Allowances: grants, }, err } + +func (k Keeper) removeFromGrantQueue(ctx sdk.Context, exp *time.Time, allowanceKey []byte) { + key := feegrant.FeeAllowancePrefixQueue(exp, allowanceKey) + store := ctx.KVStore(k.storeKey) + store.Delete(key) +} + +func (k Keeper) addToFeeAllowanceQueue(ctx sdk.Context, grantKey []byte, exp *time.Time) { + store := ctx.KVStore(k.storeKey) + store.Set(feegrant.FeeAllowancePrefixQueue(exp, grantKey), []byte{}) +} + +// RemoveExpiredAllowances iterates grantsByExpiryQueue and deletes the expired grants. +func (k Keeper) RemoveExpiredAllowances(ctx sdk.Context) { + exp := ctx.BlockTime() + store := ctx.KVStore(k.storeKey) + iterator := store.Iterator(feegrant.FeeAllowanceQueueKeyPrefix, sdk.InclusiveEndBytes(feegrant.AllowanceByExpTimeKey(&exp))) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + store.Delete(iterator.Key()) + expLen := len(sdk.FormatTimeBytes(ctx.BlockTime())) + + // extract the fee allowance key by removing the allowance queue prefix length, expiration length from key. + store.Delete(append(feegrant.FeeAllowanceKeyPrefix, iterator.Key()[1+expLen:]...)) + } +} diff --git a/x/feegrant/keeper/keeper_test.go b/x/feegrant/keeper/keeper_test.go index 8397f264fb95..933d85c7cefe 100644 --- a/x/feegrant/keeper/keeper_test.go +++ b/x/feegrant/keeper/keeper_test.go @@ -214,15 +214,18 @@ func (suite *KeeperTestSuite) TestUseGrantedFee() { }) } - expired := &feegrant.BasicAllowance{ + basicAllowance := &feegrant.BasicAllowance{ SpendLimit: eth, Expiration: &blockTime, } - // creating expired feegrant - ctx := suite.sdkCtx.WithBlockTime(oneYear) - err := suite.keeper.GrantAllowance(ctx, suite.addrs[0], suite.addrs[2], expired) + + // create basic fee allowance + err := suite.keeper.GrantAllowance(suite.sdkCtx, suite.addrs[0], suite.addrs[2], basicAllowance) suite.Require().NoError(err) + // waiting for future blocks, allowance to be pruned. + ctx := suite.sdkCtx.WithBlockTime(oneYear) + // expect error: feegrant expired err = suite.keeper.UseGrantedFees(ctx, suite.addrs[0], suite.addrs[2], eth, []sdk.Msg{}) suite.Error(err) @@ -232,7 +235,6 @@ func (suite *KeeperTestSuite) TestUseGrantedFee() { _, err = suite.keeper.GetAllowance(ctx, suite.addrs[0], suite.addrs[2]) suite.Error(err) suite.Contains(err.Error(), "fee-grant not found") - } func (suite *KeeperTestSuite) TestIterateGrants() { @@ -257,5 +259,87 @@ func (suite *KeeperTestSuite) TestIterateGrants() { suite.Require().Contains([]string{suite.addrs[0].String(), suite.addrs[2].String()}, grant.Granter) return true }) +} + +func (suite *KeeperTestSuite) TestPruneGrants() { + eth := sdk.NewCoins(sdk.NewInt64Coin("eth", 123)) + now := suite.sdkCtx.BlockTime() + oneYearExpiry := now.AddDate(1, 0, 0) + testCases := []struct { + name string + ctx sdk.Context + granter sdk.AccAddress + grantee sdk.AccAddress + allowance feegrant.FeeAllowanceI + expErrMsg string + }{ + { + name: "grant not pruned from state", + ctx: suite.sdkCtx, + granter: suite.addrs[0], + grantee: suite.addrs[1], + allowance: &feegrant.BasicAllowance{ + SpendLimit: suite.atom, + Expiration: &now, + }, + }, + { + name: "grant pruned from state after a block: error", + ctx: suite.sdkCtx.WithBlockTime(now.AddDate(0, 0, 1)), + granter: suite.addrs[2], + grantee: suite.addrs[1], + expErrMsg: "not found", + allowance: &feegrant.BasicAllowance{ + SpendLimit: eth, + Expiration: &now, + }, + }, + { + name: "grant not pruned from state after a day: no error", + ctx: suite.sdkCtx.WithBlockTime(now.AddDate(0, 0, 1)), + granter: suite.addrs[1], + grantee: suite.addrs[0], + allowance: &feegrant.BasicAllowance{ + SpendLimit: eth, + Expiration: &oneYearExpiry, + }, + }, + { + name: "grant pruned from state after a year: error", + ctx: suite.sdkCtx.WithBlockTime(now.AddDate(1, 0, 1)), + granter: suite.addrs[1], + grantee: suite.addrs[2], + expErrMsg: "not found", + allowance: &feegrant.BasicAllowance{ + SpendLimit: eth, + Expiration: &oneYearExpiry, + }, + }, + { + name: "no expiry: no error", + ctx: suite.sdkCtx.WithBlockTime(now.AddDate(1, 0, 0)), + granter: suite.addrs[1], + grantee: suite.addrs[2], + allowance: &feegrant.BasicAllowance{ + SpendLimit: eth, + Expiration: &oneYearExpiry, + }, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.keeper.GrantAllowance(suite.sdkCtx, tc.granter, tc.grantee, tc.allowance) + suite.app.FeeGrantKeeper.RemoveExpiredAllowances(tc.ctx) + grant, err := suite.keeper.GetAllowance(tc.ctx, tc.granter, tc.grantee) + if tc.expErrMsg != "" { + suite.Error(err) + suite.Contains(err.Error(), tc.expErrMsg) + } else { + suite.NotNil(grant) + } + }) + } } diff --git a/x/feegrant/keeper/migrations.go b/x/feegrant/keeper/migrations.go new file mode 100644 index 000000000000..67c3e489cf05 --- /dev/null +++ b/x/feegrant/keeper/migrations.go @@ -0,0 +1,21 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + v046 "github.com/cosmos/cosmos-sdk/x/feegrant/migrations/v046" +) + +// Migrator is a struct for handling in-place store migrations. +type Migrator struct { + keeper Keeper +} + +// NewMigrator returns a new Migrator. +func NewMigrator(keeper Keeper) Migrator { + return Migrator{keeper: keeper} +} + +// Migrate1to2 migrates from version 1 to 2. +func (m Migrator) Migrate1to2(ctx sdk.Context) error { + return v046.MigrateStore(ctx, m.keeper.storeKey, m.keeper.cdc) +} diff --git a/x/feegrant/key.go b/x/feegrant/key.go index 70f6eaa44003..3d2fdb56336b 100644 --- a/x/feegrant/key.go +++ b/x/feegrant/key.go @@ -1,6 +1,8 @@ package feegrant import ( + time "time" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/address" "github.com/cosmos/cosmos-sdk/types/kv" @@ -22,30 +24,60 @@ const ( var ( // FeeAllowanceKeyPrefix is the set of the kvstore for fee allowance data + // - 0x00: allowance FeeAllowanceKeyPrefix = []byte{0x00} + + // FeeAllowanceQueueKeyPrefix is the set of the kvstore for fee allowance keys data + // - 0x01: + FeeAllowanceQueueKeyPrefix = []byte{0x01} ) // FeeAllowanceKey is the canonical key to store a grant from granter to grantee // We store by grantee first to allow searching by everyone who granted to you +// +// Key format: +// - <0x00> func FeeAllowanceKey(granter sdk.AccAddress, grantee sdk.AccAddress) []byte { return append(FeeAllowancePrefixByGrantee(grantee), address.MustLengthPrefix(granter.Bytes())...) } // FeeAllowancePrefixByGrantee returns a prefix to scan for all grants to this given address. +// +// Key format: +// - <0x00> func FeeAllowancePrefixByGrantee(grantee sdk.AccAddress) []byte { return append(FeeAllowanceKeyPrefix, address.MustLengthPrefix(grantee.Bytes())...) } +// FeeAllowancePrefixQueue is the canonical key to store grant key. +// +// Key format: +// - <0x01> +func FeeAllowancePrefixQueue(exp *time.Time, key []byte) []byte { + allowanceByExpTimeKey := AllowanceByExpTimeKey(exp) + return append(allowanceByExpTimeKey, key...) +} + +// AllowanceByExpTimeKey returns a key with `FeeAllowanceQueueKeyPrefix`, expiry +// +// Key format: +// - <0x01> +func AllowanceByExpTimeKey(exp *time.Time) []byte { + // no need of appending len(exp_bytes) here, `FormatTimeBytes` gives const length everytime. + return append(FeeAllowanceQueueKeyPrefix, sdk.FormatTimeBytes(*exp)...) +} + +// ParseAddressesFromFeeAllowanceKey exrtacts and returns the granter, grantee from the given key. func ParseAddressesFromFeeAllowanceKey(key []byte) (granter, grantee sdk.AccAddress) { // key is of format: - // 0x00 + // 0x00 kv.AssertKeyAtLeastLength(key, 2) granteeAddrLen := key[1] // remove prefix key - kv.AssertKeyAtLeastLength(key, int(2+granteeAddrLen)) - grantee = sdk.AccAddress(key[2 : 2+granteeAddrLen]) + kv.AssertKeyAtLeastLength(key, 2+int(granteeAddrLen)) + grantee = sdk.AccAddress(key[2 : 2+int(granteeAddrLen)]) granterAddrLen := int(key[2+granteeAddrLen]) - kv.AssertKeyAtLeastLength(key, 3+int(granteeAddrLen+byte(granterAddrLen))) - granter = sdk.AccAddress(key[3+granterAddrLen : 3+granteeAddrLen+byte(granterAddrLen)]) + kv.AssertKeyAtLeastLength(key, 3+int(granteeAddrLen)+int(granterAddrLen)) + granter = sdk.AccAddress(key[3+granterAddrLen : 3+int(granteeAddrLen)+int(granterAddrLen)]) return granter, grantee } diff --git a/x/feegrant/migrations/v046/keys.go b/x/feegrant/migrations/v046/keys.go new file mode 100644 index 000000000000..818472e6ca68 --- /dev/null +++ b/x/feegrant/migrations/v046/keys.go @@ -0,0 +1,47 @@ +package v046 + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" +) + +var ( + ModuleName = "feegrant" + + // FeeAllowanceKeyPrefix is the set of the kvstore for fee allowance data + // - 0x00: allowance + FeeAllowanceKeyPrefix = []byte{0x00} + + // FeeAllowanceQueueKeyPrefix is the set of the kvstore for fee allowance keys data + // - 0x01: + FeeAllowanceQueueKeyPrefix = []byte{0x01} +) + +// FeeAllowancePrefixQueue is the canonical key to store grant key. +// +// Key format: +// - <0x01> +func FeeAllowancePrefixQueue(exp *time.Time, key []byte) []byte { + // no need of appending len(exp_bytes) here, `FormatTimeBytes` gives const length everytime. + allowanceByExpTimeKey := append(FeeAllowanceQueueKeyPrefix, sdk.FormatTimeBytes(*exp)...) + return append(allowanceByExpTimeKey, key...) +} + +// FeeAllowanceKey is the canonical key to store a grant from granter to grantee +// We store by grantee first to allow searching by everyone who granted to you +// +// Key format: +// - <0x00> +func FeeAllowanceKey(granter sdk.AccAddress, grantee sdk.AccAddress) []byte { + return append(FeeAllowancePrefixByGrantee(grantee), address.MustLengthPrefix(granter.Bytes())...) +} + +// FeeAllowancePrefixByGrantee returns a prefix to scan for all grants to this given address. +// +// Key format: +// - <0x00> +func FeeAllowancePrefixByGrantee(grantee sdk.AccAddress) []byte { + return append(FeeAllowanceKeyPrefix, address.MustLengthPrefix(grantee.Bytes())...) +} diff --git a/x/feegrant/migrations/v046/store.go b/x/feegrant/migrations/v046/store.go new file mode 100644 index 000000000000..f3d49312caa4 --- /dev/null +++ b/x/feegrant/migrations/v046/store.go @@ -0,0 +1,51 @@ +package v046 + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/feegrant" +) + +func addAllowancesByExpTimeQueue(ctx types.Context, store storetypes.KVStore, cdc codec.BinaryCodec) error { + prefixStore := prefix.NewStore(store, FeeAllowanceKeyPrefix) + iterator := prefixStore.Iterator(nil, nil) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var grant feegrant.Grant + bz := iterator.Value() + if err := cdc.Unmarshal(bz, &grant); err != nil { + return err + } + + grantInfo, err := grant.GetGrant() + if err != nil { + return err + } + + exp, err := grantInfo.ExpiresAt() + if err != nil { + return err + } + + if exp != nil { + // store key is not changed in 0.46 + key := iterator.Key() + if exp.Before(ctx.BlockTime()) { + prefixStore.Delete(key) + } else { + grantByExpTimeQueueKey := FeeAllowancePrefixQueue(exp, key) + store.Set(grantByExpTimeQueueKey, []byte{}) + } + } + } + + return nil +} + +func MigrateStore(ctx types.Context, storeKey storetypes.StoreKey, cdc codec.BinaryCodec) error { + store := ctx.KVStore(storeKey) + return addAllowancesByExpTimeQueue(ctx, store, cdc) +} diff --git a/x/feegrant/migrations/v046/store_test.go b/x/feegrant/migrations/v046/store_test.go new file mode 100644 index 000000000000..5d7cb5d9d073 --- /dev/null +++ b/x/feegrant/migrations/v046/store_test.go @@ -0,0 +1,83 @@ +package v046_test + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/feegrant" + v046 "github.com/cosmos/cosmos-sdk/x/feegrant/migrations/v046" + "github.com/stretchr/testify/require" +) + +func TestMigration(t *testing.T) { + encCfg := simapp.MakeTestEncodingConfig() + cdc := encCfg.Codec + feegrantKey := sdk.NewKVStoreKey(v046.ModuleName) + ctx := testutil.DefaultContext(feegrantKey, sdk.NewTransientStoreKey("transient_test")) + granter1 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + grantee1 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + granter2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + grantee2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + + spendLimit := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000))) + now := ctx.BlockTime() + oneDay := now.AddDate(0, 0, 1) + twoDays := now.AddDate(0, 0, 2) + + grants := []struct { + granter sdk.AccAddress + grantee sdk.AccAddress + spendLimit sdk.Coins + expiration *time.Time + }{ + { + granter: granter1, + grantee: grantee1, + spendLimit: spendLimit, + expiration: &twoDays, + }, + { + granter: granter2, + grantee: grantee2, + spendLimit: spendLimit, + expiration: &oneDay, + }, + { + granter: granter1, + grantee: grantee2, + spendLimit: spendLimit, + }, + { + granter: granter2, + grantee: grantee1, + expiration: &oneDay, + }, + } + + store := ctx.KVStore(feegrantKey) + for _, grant := range grants { + newGrant, err := feegrant.NewGrant(grant.granter, grant.grantee, &feegrant.BasicAllowance{ + SpendLimit: grant.spendLimit, + Expiration: grant.expiration, + }) + require.NoError(t, err) + + bz, err := cdc.Marshal(&newGrant) + require.NoError(t, err) + + store.Set(v046.FeeAllowanceKey(grant.granter, grant.grantee), bz) + } + + ctx = ctx.WithBlockTime(now.Add(30 * time.Hour)) + require.NoError(t, v046.MigrateStore(ctx, feegrantKey, cdc)) + store = ctx.KVStore(feegrantKey) + + require.NotNil(t, store.Get(v046.FeeAllowanceKey(granter1, grantee1))) + require.Nil(t, store.Get(v046.FeeAllowanceKey(granter2, grantee2))) + require.NotNil(t, store.Get(v046.FeeAllowanceKey(granter1, grantee2))) + require.Nil(t, store.Get(v046.FeeAllowanceKey(granter2, grantee1))) +} diff --git a/x/feegrant/module/abci.go b/x/feegrant/module/abci.go new file mode 100644 index 000000000000..34c19c528d6a --- /dev/null +++ b/x/feegrant/module/abci.go @@ -0,0 +1,10 @@ +package module + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/feegrant/keeper" +) + +func EndBlocker(ctx sdk.Context, k keeper.Keeper) { + k.RemoveExpiredAllowances(ctx) +} diff --git a/x/feegrant/module/abci_test.go b/x/feegrant/module/abci_test.go new file mode 100644 index 000000000000..4c8aa03d410e --- /dev/null +++ b/x/feegrant/module/abci_test.go @@ -0,0 +1,79 @@ +package module_test + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/feegrant" + "github.com/cosmos/cosmos-sdk/x/feegrant/module" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" +) + +func TestFeegrantPruning(t *testing.T) { + app := simapp.Setup(t, false) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := simapp.AddTestAddrs(app, ctx, 4, sdk.NewInt(1000)) + granter1 := addrs[0] + granter2 := addrs[1] + granter3 := addrs[2] + grantee := addrs[3] + spendLimit := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000))) + now := ctx.BlockTime() + oneDay := now.AddDate(0, 0, 1) + + header := tmproto.Header{Height: app.LastBlockHeight() + 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + + app.FeeGrantKeeper.GrantAllowance( + ctx, + granter1, + grantee, + &feegrant.BasicAllowance{ + Expiration: &now, + }, + ) + app.FeeGrantKeeper.GrantAllowance( + ctx, + granter2, + grantee, + &feegrant.BasicAllowance{ + SpendLimit: spendLimit, + }, + ) + app.FeeGrantKeeper.GrantAllowance( + ctx, + granter3, + grantee, + &feegrant.BasicAllowance{ + Expiration: &oneDay, + }, + ) + + queryHelper := baseapp.NewQueryServerTestHelper(ctx, app.InterfaceRegistry()) + feegrant.RegisterQueryServer(queryHelper, app.FeeGrantKeeper) + queryClient := feegrant.NewQueryClient(queryHelper) + + module.EndBlocker(ctx, app.FeeGrantKeeper) + + res, err := queryClient.Allowances(ctx.Context(), &feegrant.QueryAllowancesRequest{ + Grantee: grantee.String(), + }) + require.NoError(t, err) + require.NotNil(t, res) + require.Len(t, res.Allowances, 3) + + ctx = ctx.WithBlockTime(now.AddDate(0, 0, 2)) + module.EndBlocker(ctx, app.FeeGrantKeeper) + + res, err = queryClient.Allowances(ctx.Context(), &feegrant.QueryAllowancesRequest{ + Grantee: grantee.String(), + }) + require.NoError(t, err) + require.NotNil(t, res) + require.Len(t, res.Allowances, 1) +} diff --git a/x/feegrant/module/module.go b/x/feegrant/module/module.go index 44107bc733da..b44c6c295948 100644 --- a/x/feegrant/module/module.go +++ b/x/feegrant/module/module.go @@ -48,6 +48,11 @@ func (AppModuleBasic) Name() string { func (am AppModule) RegisterServices(cfg module.Configurator) { feegrant.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) feegrant.RegisterQueryServer(cfg.QueryServer(), am.keeper) + m := keeper.NewMigrator(am.keeper) + err := cfg.RegisterMigration(feegrant.ModuleName, 1, m.Migrate1to2) + if err != nil { + panic(err) + } } // RegisterLegacyAminoCodec registers the feegrant module's types for the given codec. @@ -173,14 +178,15 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw } // ConsensusVersion implements AppModule/ConsensusVersion. -func (AppModule) ConsensusVersion() uint64 { return 1 } +func (AppModule) ConsensusVersion() uint64 { return 2 } // BeginBlock returns the begin blocker for the feegrant module. func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} // EndBlock returns the end blocker for the feegrant module. It returns no validator // updates. -func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + EndBlocker(ctx, am.keeper) return []abci.ValidatorUpdate{} } diff --git a/x/feegrant/periodic_fee.go b/x/feegrant/periodic_fee.go index c6fbc94b6ea9..43229247ff0c 100644 --- a/x/feegrant/periodic_fee.go +++ b/x/feegrant/periodic_fee.go @@ -105,3 +105,7 @@ func (a PeriodicAllowance) ValidateBasic() error { return nil } + +func (a PeriodicAllowance) ExpiresAt() (*time.Time, error) { + return a.Basic.ExpiresAt() +} diff --git a/x/feegrant/spec/01_concepts.md b/x/feegrant/spec/01_concepts.md index 6b22d46b68c4..16af82792020 100644 --- a/x/feegrant/spec/01_concepts.md +++ b/x/feegrant/spec/01_concepts.md @@ -76,3 +76,7 @@ Fees are deducted from grants in the `x/auth` ante handler. To learn more about In order to prevent DoS attacks, using a filtered `x/feegrant` incurs gas. The SDK must assure that the `grantee`'s transactions all conform to the filter set by the `granter`. The SDK does this by iterating over the allowed messages in the filter and charging 10 gas per filtered message. The SDK will then iterate over the messages being sent by the `grantee` to ensure the messages adhere to the filter, also charging 10 gas per message. The SDK will stop iterating and fail the transaction if it finds a message that does not conform to the filter. **WARNING**: The gas is charged against the granted allowance. Ensure your messages conform to the filter, if any, before sending transactions using your allowance. + +## Pruning + +A queue in the state maintained with the prefix of expiration of the grants and checks them on EndBlock with the current block time for every block to prune. \ No newline at end of file diff --git a/x/feegrant/spec/02_state.md b/x/feegrant/spec/02_state.md index 5b12cd2d0cee..47106e006818 100644 --- a/x/feegrant/spec/02_state.md +++ b/x/feegrant/spec/02_state.md @@ -13,3 +13,11 @@ Fee allowance grants are stored in the state as follows: - Grant: `0x00 | grantee_addr_len (1 byte) | grantee_addr_bytes | granter_addr_len (1 byte) | granter_addr_bytes -> ProtocolBuffer(Grant)` +++ https://github.com/cosmos/cosmos-sdk/blob/691032b8be0f7539ec99f8882caecefc51f33d1f/x/feegrant/feegrant.pb.go#L221-L229 + +## FeeAllowanceQueue + +Fee Allowances queue items are identified by combining the `FeeAllowancePrefixQueue` (i.e., 0x01), `expiration`, `grantee` (the account address of fee allowance grantee), `granter` (the account address of fee allowance granter). Endblocker checks `FeeAllowanceQueue` state for the expired grants and prunes them from `FeeAllowance` if there are any found. + +Fee allowance queue keys are stored in the state as follows: + +- Grant: `0x01 | expiration_bytes | grantee_addr_len (1 byte) | grantee_addr_bytes | granter_addr_len (1 byte) | granter_addr_bytes -> EmptyBytes` \ No newline at end of file