feat: adds pruning for feegrant (#10830)

Closes: #10685 



---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
This commit is contained in:
atheeshp 2022-02-08 09:10:56 +05:30 committed by GitHub
parent ebddeee360
commit 786afb37f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 570 additions and 16 deletions

View File

@ -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

View File

@ -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
}

View File

@ -5,6 +5,7 @@ const (
EventTypeUseFeeGrant = "use_feegrant"
EventTypeRevokeFeeGrant = "revoke_feegrant"
EventTypeSetFeeGrant = "set_feegrant"
EventTypeUpdateFeeGrant = "update_feegrant"
AttributeKeyGranter = "granter"
AttributeKeyGrantee = "grantee"

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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:]...))
}
}

View File

@ -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)
}
})
}
}

View File

@ -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)
}

View File

@ -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_key_bytes>: allowance
FeeAllowanceKeyPrefix = []byte{0x00}
// FeeAllowanceQueueKeyPrefix is the set of the kvstore for fee allowance keys data
// - 0x01<allowance_prefix_queue_key_bytes>: <empty value>
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><len(grantee_address_bytes)><grantee_address_bytes><len(granter_address_bytes)><granter_address_bytes>
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><len(grantee_address_bytes)><grantee_address_bytes>
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><exp_bytes><len(grantee_address_bytes)><grantee_address_bytes><len(granter_address_bytes)><granter_address_bytes>
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><exp_bytes>
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<granteeAddressLen (1 Byte)><granteeAddress_Bytes><granterAddressLen (1 Byte)><granterAddress_Bytes><msgType_Bytes>
// 0x00<granteeAddressLen (1 Byte)><granteeAddress_Bytes><granterAddressLen (1 Byte)><granterAddress_Bytes>
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
}

View File

@ -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_key_bytes>: allowance
FeeAllowanceKeyPrefix = []byte{0x00}
// FeeAllowanceQueueKeyPrefix is the set of the kvstore for fee allowance keys data
// - 0x01<allowance_prefix_queue_key_bytes>: <empty value>
FeeAllowanceQueueKeyPrefix = []byte{0x01}
)
// FeeAllowancePrefixQueue is the canonical key to store grant key.
//
// Key format:
// - <0x01><exp_bytes><len(grantee_address_bytes)><grantee_address_bytes><len(granter_address_bytes)><granter_address_bytes>
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><len(grantee_address_bytes)><grantee_address_bytes><len(granter_address_bytes)><granter_address_bytes>
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><len(grantee_address_bytes)><grantee_address_bytes>
func FeeAllowancePrefixByGrantee(grantee sdk.AccAddress) []byte {
return append(FeeAllowanceKeyPrefix, address.MustLengthPrefix(grantee.Bytes())...)
}

View File

@ -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)
}

View File

@ -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)))
}

10
x/feegrant/module/abci.go Normal file
View File

@ -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)
}

View File

@ -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)
}

View File

@ -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{}
}

View File

@ -105,3 +105,7 @@ func (a PeriodicAllowance) ValidateBasic() error {
return nil
}
func (a PeriodicAllowance) ExpiresAt() (*time.Time, error) {
return a.Basic.ExpiresAt()
}

View File

@ -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.

View File

@ -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`