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:
parent
ebddeee360
commit
786afb37f8
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ const (
|
||||
EventTypeUseFeeGrant = "use_feegrant"
|
||||
EventTypeRevokeFeeGrant = "revoke_feegrant"
|
||||
EventTypeSetFeeGrant = "set_feegrant"
|
||||
EventTypeUpdateFeeGrant = "update_feegrant"
|
||||
|
||||
AttributeKeyGranter = "granter"
|
||||
AttributeKeyGrantee = "grantee"
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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:]...))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
21
x/feegrant/keeper/migrations.go
Normal file
21
x/feegrant/keeper/migrations.go
Normal 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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
47
x/feegrant/migrations/v046/keys.go
Normal file
47
x/feegrant/migrations/v046/keys.go
Normal 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())...)
|
||||
}
|
||||
51
x/feegrant/migrations/v046/store.go
Normal file
51
x/feegrant/migrations/v046/store.go
Normal 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)
|
||||
}
|
||||
83
x/feegrant/migrations/v046/store_test.go
Normal file
83
x/feegrant/migrations/v046/store_test.go
Normal 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
10
x/feegrant/module/abci.go
Normal 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)
|
||||
}
|
||||
79
x/feegrant/module/abci_test.go
Normal file
79
x/feegrant/module/abci_test.go
Normal 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)
|
||||
}
|
||||
@ -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{}
|
||||
}
|
||||
|
||||
|
||||
@ -105,3 +105,7 @@ func (a PeriodicAllowance) ValidateBasic() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a PeriodicAllowance) ExpiresAt() (*time.Time, error) {
|
||||
return a.Basic.ExpiresAt()
|
||||
}
|
||||
|
||||
@ -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.
|
||||
@ -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`
|
||||
Loading…
Reference in New Issue
Block a user