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

Enable Fee Delegation #5768

Closed
wants to merge 85 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
abce938
Add docs
ethanfrey Oct 16, 2019
ea6fe7d
Add BasicFeeAllowance implementation
ethanfrey Oct 16, 2019
c46153c
Add expiration structs and complete basic fee
ethanfrey Oct 16, 2019
42c17f5
Add delegation messages, add validation logic
ethanfrey Oct 16, 2019
0c137cb
Add keeper and helper structs
ethanfrey Oct 16, 2019
67cb830
Add alias and handler to top level
ethanfrey Oct 16, 2019
f6ba9bc
Add delegation module
ethanfrey Oct 16, 2019
a002366
Add basic querier
ethanfrey Oct 16, 2019
15c6361
Add types tests
ethanfrey Oct 16, 2019
410d1f1
Add types tests
ethanfrey Oct 16, 2019
f65db09
More internal test coverage
ethanfrey Oct 16, 2019
e0f6327
Solid internal test coverage
ethanfrey Oct 16, 2019
93c9b7e
Expose Querier to top level module
ethanfrey Oct 16, 2019
7968441
Add FeeAccount to auth/types, like StdTx, SignDoc
ethanfrey Oct 16, 2019
7e82fb8
Fix all tests in x/auth
ethanfrey Oct 16, 2019
64d5159
All tests pass
ethanfrey Oct 16, 2019
8b2b64b
Appease the Golang Linter
ethanfrey Oct 16, 2019
4e22edd
Merge remote-tracking branch 'origin/master' into cg-key-management/f…
ethanfrey Oct 16, 2019
be12b44
Add fee-account command line flag
ethanfrey Oct 16, 2019
c703e59
Start on DelegatedDeductFeeDecorator
ethanfrey Oct 17, 2019
907776f
Cleanup the Decorator
ethanfrey Oct 17, 2019
005afd9
Wire up delegation module in simapp
ethanfrey Oct 17, 2019
45c64d2
add basic test for decorator (no delegation)
ethanfrey Oct 17, 2019
87fd0ce
Table tests for deduct fees
ethanfrey Oct 17, 2019
d03c650
Table tests over all conditions of delegated fee decorator
ethanfrey Oct 17, 2019
e476c03
Build full ante handler stack and test it
ethanfrey Oct 17, 2019
182eb34
Start genesis
ethanfrey Oct 17, 2019
071e111
Implement Genesis
ethanfrey Oct 17, 2019
0a362b8
Merge branch 'master' into cg-key-management/fee-delegation-new
alexanderbez Oct 17, 2019
98b88a3
Rename package delegation to subkeys
ethanfrey Oct 18, 2019
376e9cd
Clarify antes test cases, handle empty account w/o fees
ethanfrey Oct 18, 2019
529764f
Allow paying delegated fees with no account
ethanfrey Oct 18, 2019
d293aae
Pull mempool into delegated ante, for control on StdFee
ethanfrey Oct 18, 2019
a8b3e31
Use custom DelegatedTx, DelegatedFee for subkeys
ethanfrey Oct 18, 2019
e939e98
Revert all changes to x/auth.StdTx
ethanfrey Oct 18, 2019
6aef5c8
Appease scopelint
ethanfrey Oct 18, 2019
1212772
Register DelegatedTx with codec
ethanfrey Oct 18, 2019
f688116
Merge branch 'master' into cg-key-management/fee-delegation-new
alexanderbez Oct 18, 2019
61df0c6
Address PR comments
ethanfrey Oct 21, 2019
d684f92
Remove unnecessary DelegatedMempoolFeeDecorator
ethanfrey Oct 21, 2019
aacf6fb
Cleaned up errors in querier
ethanfrey Oct 21, 2019
92f64cc
Clean up message sign bytes
ethanfrey Oct 21, 2019
4daf4af
Minor PR comments
ethanfrey Oct 24, 2019
a497575
Replace GetAllFees... with Iterator variants
ethanfrey Oct 24, 2019
73eab92
PrepareForExport adjusts grant expiration height
ethanfrey Oct 24, 2019
546fa8f
Panic on de/serialization error in keeper
ethanfrey Oct 24, 2019
5f9e391
Move custom ante handler chain to tests, update docs
ethanfrey Oct 24, 2019
c45dc71
More cleanup
ethanfrey Oct 24, 2019
2be48ef
More doc cleanup
ethanfrey Oct 24, 2019
e8a624f
Renamed subkeys module to fee_grant
ethanfrey Oct 24, 2019
6756099
Rename subkeys/delegation to fee grant in all strings
ethanfrey Oct 24, 2019
320bad4
Modify Msg and Keeper methods to use Grant not Delegate
ethanfrey Oct 24, 2019
a625fd0
Merge remote-tracking branch 'origin/master' into cg-key-management/f…
ethanfrey Oct 24, 2019
4b4391b
Add PeriodicFeeAllowance
ethanfrey Oct 29, 2019
9ec284b
Update aliases
ethanfrey Oct 29, 2019
4b34cf6
Cover all accept cases for PeriodicFeeAllowance
ethanfrey Oct 29, 2019
78f1f7a
Et tu scopelint?
ethanfrey Oct 29, 2019
a1951b4
Update docs as requested
ethanfrey Nov 7, 2019
17c39ca
Remove error return from GetFeeGrant
ethanfrey Nov 7, 2019
9c399bb
Code cleanup as requested by PR
ethanfrey Nov 7, 2019
6b9e06e
Merge remote-tracking branch 'origin/master' into cg-key-management/f…
ethanfrey Nov 7, 2019
21b0ac5
Updated all errors to use new sdk/errors package
ethanfrey Nov 7, 2019
2222d0e
Use test suite for keeper tests
ethanfrey Nov 7, 2019
eda42d4
Clean up alias.go file
ethanfrey Nov 7, 2019
a56b472
Define expected interfaces in exported, rather than importing from ac…
ethanfrey Nov 7, 2019
be29328
Remove dependency on auth/ante
ethanfrey Nov 7, 2019
786bf01
Improve godoc, Logger
ethanfrey Nov 7, 2019
5f2bcb5
Cleaned up ExpiresAt
ethanfrey Nov 7, 2019
314b099
Improve error reporting with UseGrantedFee
ethanfrey Nov 7, 2019
29958e0
Enforce period limit subset of basic limit
ethanfrey Nov 7, 2019
fb5b97a
Add events
ethanfrey Nov 7, 2019
eedb8d8
Rename fee_grant to feegrant
ethanfrey Nov 7, 2019
88df255
Ensure KeeperTestSuite actually runs
ethanfrey Nov 7, 2019
dbf3578
Move types/tx to types
ethanfrey Nov 7, 2019
375f114
Update alias file, include ante
ethanfrey Nov 7, 2019
9af9f43
I do need nolint in alias.go
ethanfrey Nov 7, 2019
3c390e5
Properly emit events in the handler. Use cosmos-sdk in amino types
ethanfrey Nov 7, 2019
de694c5
Merge branch 'master' into bez/enable-fee-auth
alexanderbez Mar 9, 2020
720e9f4
Update godoc
alexanderbez Mar 9, 2020
eb46dd0
Linting...
alexanderbez Mar 9, 2020
63cca04
Update errors
alexanderbez Mar 9, 2020
8eddbb7
Update pkg doc and fix ante-handler order
alexanderbez Mar 9, 2020
f32db1d
Merge PR #5782: Migrate x/feegrant to proto
sahith-narahari Apr 7, 2020
8dc477a
Merge branch 'master' of github.com:cosmos/cosmos-sdk into bez/enable…
sahith-narahari May 27, 2020
b8038ee
Merge branch 'master' of github.com:cosmos/cosmos-sdk into bez/enable…
sahith-narahari Jun 8, 2020
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
2 changes: 2 additions & 0 deletions client/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const (
FlagRPCMaxBodyBytes = "max-body-bytes"
FlagOutputDocument = "output-document" // inspired by wget -O
FlagSkipConfirmation = "yes"
FlagFeeAccount = "fee-account"
FlagProve = "prove"
FlagKeyringBackend = "keyring-backend"
FlagPage = "page"
Expand Down Expand Up @@ -122,6 +123,7 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command {
c.Flags().Bool(FlagOffline, false, "Offline mode (does not allow any online functionality")
c.Flags().BoolP(FlagSkipConfirmation, "y", false, "Skip tx broadcasting prompt confirmation")
c.Flags().String(FlagKeyringBackend, DefaultKeyringBackend, "Select keyring's backend (os|file|kwallet|pass|test)")
c.Flags().String(FlagFeeAccount, "", "Set a fee account to pay fess with if they have been authorized by this account")

// --gas can accept integers and "simulate"
c.Flags().Var(&GasFlagVar, "gas", fmt.Sprintf(
Expand Down
35 changes: 35 additions & 0 deletions simapp/ante.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package simapp

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
"github.com/cosmos/cosmos-sdk/x/feegrant"
feegrantante "github.com/cosmos/cosmos-sdk/x/feegrant/ante"
)

// NewAnteHandler returns an AnteHandler that checks and increments sequence
// numbers, checks signatures & account numbers, and deducts fees from the first
// signer.
func NewAnteHandler(
ak auth.AccountKeeper, supplyKeeper feegrant.SupplyKeeper, feeGrantKeeper feegrant.Keeper,
sigGasConsumer auth.SignatureVerificationGasConsumer,
) sdk.AnteHandler {

return sdk.ChainAnteDecorators(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aaronc @ethanfrey please verify the ante-handler and order here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has been a while, but this looks good.

NewSetPubKeyDecorator position is key, as otherwise an empty account could never use a grant until it had some tokens (which is not the behavior we wanted)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting to take a look at this work again as I'm trying to figure out how to deal with account numbers and sequences starting from 1 (which was a part of ADR 020).

So one thing that I'm aware of here is that if we create the account in the DeduceGrantedFeeDecorator that account number will differ from the one use for tx signing. So it seems like the correct approach to doing this is to allow accounts that don't yet exist in state to use some zero-value for account number to do signing. Since we want to avoid 0 as a value for account number, it sounds like 1 would become the zero value and 2 would be the first number of any real account in state.

What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another alternative is that accounts get created (if they don't exist) whenever fee grants are created. That might be a bit cleaner in some ways.

Although, conceptually the idea that there is some way for accounts that aren't in state to sign transactions sounds desirable to me.

authante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
authante.NewMempoolFeeDecorator(),
authante.NewValidateBasicDecorator(),
authante.NewValidateMemoDecorator(ak),
authante.NewConsumeGasForTxSizeDecorator(ak),
// DeductGrantedFeeDecorator will create an empty account if we sign with no
// tokens but valid validation. This must be before SetPubKey, ValidateSigCount,
// SigVerification, which error if account doesn't exist yet.
feegrantante.NewDeductGrantedFeeDecorator(ak, supplyKeeper, feeGrantKeeper),
authante.NewSetPubKeyDecorator(ak), // SetPubKeyDecorator must be called before all signature verification decorators
authante.NewValidateSigCountDecorator(ak),
authante.NewSigGasConsumeDecorator(ak, sigGasConsumer),
authante.NewSigVerificationDecorator(ak),
authante.NewIncrementSequenceDecorator(ak), // innermost AnteDecorator
)
}
12 changes: 8 additions & 4 deletions simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import (
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/ante"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/capability"
"github.com/cosmos/cosmos-sdk/x/crisis"
distr "github.com/cosmos/cosmos-sdk/x/distribution"
"github.com/cosmos/cosmos-sdk/x/evidence"
"github.com/cosmos/cosmos-sdk/x/feegrant"
"github.com/cosmos/cosmos-sdk/x/genutil"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/ibc"
Expand Down Expand Up @@ -67,6 +67,7 @@ var (
crisis.AppModuleBasic{},
slashing.AppModuleBasic{},
ibc.AppModuleBasic{},
feegrant.AppModuleBasic{},
upgrade.AppModuleBasic{},
evidence.AppModuleBasic{},
transfer.AppModuleBasic{},
Expand Down Expand Up @@ -117,6 +118,7 @@ type SimApp struct {
SlashingKeeper slashing.Keeper
MintKeeper mint.Keeper
DistrKeeper distr.Keeper
FeeGrantKeeper feegrant.Keeper
GovKeeper gov.Keeper
CrisisKeeper crisis.Keeper
UpgradeKeeper upgrade.Keeper
Expand Down Expand Up @@ -152,7 +154,7 @@ func NewSimApp(
keys := sdk.NewKVStoreKeys(
auth.StoreKey, bank.StoreKey, staking.StoreKey,
mint.StoreKey, distr.StoreKey, slashing.StoreKey,
gov.StoreKey, params.StoreKey, ibc.StoreKey, upgrade.StoreKey,
gov.StoreKey, params.StoreKey, ibc.StoreKey, upgrade.StoreKey, feegrant.StoreKey,
evidence.StoreKey, transfer.StoreKey, capability.StoreKey,
)
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
Expand Down Expand Up @@ -212,6 +214,7 @@ func NewSimApp(
app.CrisisKeeper = crisis.NewKeeper(
app.subspaces[crisis.ModuleName], invCheckPeriod, app.BankKeeper, auth.FeeCollectorName,
)
app.FeeGrantKeeper = feegrant.NewKeeper(appCodec, keys[feegrant.StoreKey])
app.UpgradeKeeper = upgrade.NewKeeper(skipUpgradeHeights, keys[upgrade.StoreKey], appCodec, homePath)

// register the proposal types
Expand Down Expand Up @@ -269,6 +272,7 @@ func NewSimApp(
bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper),
capability.NewAppModule(appCodec, *app.CapabilityKeeper),
crisis.NewAppModule(&app.CrisisKeeper),
feegrant.NewAppModule(app.FeeGrantKeeper),
gov.NewAppModule(appCodec, app.GovKeeper, app.AccountKeeper, app.BankKeeper),
mint.NewAppModule(appCodec, app.MintKeeper, app.AccountKeeper),
slashing.NewAppModule(appCodec, app.SlashingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper),
Expand Down Expand Up @@ -299,7 +303,7 @@ func NewSimApp(
app.mm.SetOrderInitGenesis(
capability.ModuleName, auth.ModuleName, distr.ModuleName, staking.ModuleName, bank.ModuleName,
slashing.ModuleName, gov.ModuleName, mint.ModuleName, crisis.ModuleName,
ibc.ModuleName, genutil.ModuleName, evidence.ModuleName, transfer.ModuleName,
ibc.ModuleName, genutil.ModuleName, evidence.ModuleName, transfer.ModuleName, feegrant.ModuleName,
)

app.mm.RegisterInvariants(&app.CrisisKeeper)
Expand Down Expand Up @@ -340,7 +344,7 @@ func NewSimApp(
app.SetBeginBlocker(app.BeginBlocker)
app.SetAnteHandler(
ante.NewAnteHandler(
app.AccountKeeper, app.BankKeeper, *app.IBCKeeper, ante.DefaultSigVerificationGasConsumer,
app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, *app.IBCKeeper, ante.DefaultSigVerificationGasConsumer,
),
)
app.SetEndBlocker(app.EndBlocker)
Expand Down
66 changes: 66 additions & 0 deletions x/feegrant/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package feegrant

import (
"github.com/cosmos/cosmos-sdk/x/feegrant/ante"
"github.com/cosmos/cosmos-sdk/x/feegrant/keeper"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)

// nolint

const (
DefaultCodespace = types.DefaultCodespace
EventTypeUseFeeGrant = types.EventTypeUseFeeGrant
EventTypeRevokeFeeGrant = types.EventTypeRevokeFeeGrant
EventTypeSetFeeGrant = types.EventTypeSetFeeGrant
AttributeKeyGranter = types.AttributeKeyGranter
AttributeKeyGrantee = types.AttributeKeyGrantee
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute
QueryGetFeeAllowances = keeper.QueryGetFeeAllowances
)

var (
NewDeductGrantedFeeDecorator = ante.NewDeductGrantedFeeDecorator
RegisterCodec = types.RegisterCodec
ExpiresAtTime = types.ExpiresAtTime
ExpiresAtHeight = types.ExpiresAtHeight
ClockDuration = types.ClockDuration
BlockDuration = types.BlockDuration
FeeAllowanceKey = types.FeeAllowanceKey
FeeAllowancePrefixByGrantee = types.FeeAllowancePrefixByGrantee
NewMsgRevokeFeeAllowance = types.NewMsgRevokeFeeAllowance
NewFeeGrantTx = types.NewFeeGrantTx
CountSubKeys = types.CountSubKeys
NewGrantedFee = types.NewGrantedFee
StdSignBytes = types.StdSignBytes
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier

ModuleCdc = types.ModuleCdc
ErrFeeLimitExceeded = types.ErrFeeLimitExceeded
ErrFeeLimitExpired = types.ErrFeeLimitExpired
ErrInvalidDuration = types.ErrInvalidDuration
ErrNoAllowance = types.ErrNoAllowance
FeeAllowanceKeyPrefix = types.FeeAllowanceKeyPrefix
)

type (
GrantedFeeTx = ante.GrantedFeeTx
DeductGrantedFeeDecorator = ante.DeductGrantedFeeDecorator
BasicFeeAllowance = types.BasicFeeAllowance
ExpiresAt = types.ExpiresAt
Duration = types.Duration
FeeAllowance = types.FeeAllowance
FeeAllowanceGrant = types.FeeAllowanceGrant
MsgGrantFeeAllowance = types.MsgGrantFeeAllowance
MsgRevokeFeeAllowance = types.MsgRevokeFeeAllowance
PeriodicFeeAllowance = types.PeriodicFeeAllowance
FeeGrantTx = types.FeeGrantTx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type name will be used as feegrant.FeeGrantTx by other packages, and that stutters; consider calling this Tx (from golint)

GrantedFee = types.GrantedFee
DelegatedSignDoc = types.DelegatedSignDoc
SupplyKeeper = types.SupplyKeeper
Keeper = keeper.Keeper
)
98 changes: 98 additions & 0 deletions x/feegrant/ante/fee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package ante

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/feegrant/keeper"
"github.com/cosmos/cosmos-sdk/x/feegrant/types"
)

var (
_ GrantedFeeTx = (*types.FeeGrantTx)(nil) // assert FeeGrantTx implements GrantedFeeTx
)

// GrantedFeeTx defines the interface to be implemented by Tx to use the GrantedFeeDecorator
type GrantedFeeTx interface {
sdk.Tx

GetGas() uint64
GetFee() sdk.Coins
FeePayer() sdk.AccAddress
MainSigner() sdk.AccAddress
}

// DeductGrantedFeeDecorator deducts fees from the first signer of the tx
// If the first signer does not have the funds to pay for the fees, return with InsufficientFunds error
// Call next AnteHandler if fees successfully deducted
// CONTRACT: Tx must implement GrantedFeeTx interface to use DeductGrantedFeeDecorator
type DeductGrantedFeeDecorator struct {
ak types.AccountKeeper
k keeper.Keeper
sk types.SupplyKeeper
}

func NewDeductGrantedFeeDecorator(ak types.AccountKeeper, sk types.SupplyKeeper, k keeper.Keeper) DeductGrantedFeeDecorator {
return DeductGrantedFeeDecorator{
ak: ak,
k: k,
sk: sk,
}
}

// AnteHandle performs a decorated ante-handler responsible for deducting transaction
// fees. Fees will be deducted from the account designated by the FeePayer on a
// transaction by default. However, if the fee payer differs from the transaction
// signer, the handler will check if a fee grant has been authorized. If the
// transaction's signer does not exist, it will be created.
func (d DeductGrantedFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) {
feeTx, ok := tx.(GrantedFeeTx)
if !ok {
return ctx, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a GrantedFeeTx")
}

// sanity check from DeductFeeDecorator
if addr := d.sk.GetModuleAddress(auth.FeeCollectorName); addr == nil {
panic(fmt.Sprintf("%s module account has not been set", auth.FeeCollectorName))
}

fee := feeTx.GetFee()
feePayer := feeTx.FeePayer()
txSigner := feeTx.MainSigner()

// ensure the grant is allowed, if we request a different fee payer
if !txSigner.Equals(feePayer) {
err := d.k.UseGrantedFees(ctx, feePayer, txSigner, fee)
if err != nil {
return ctx, sdkerrors.Wrapf(err, "%s not allowed to pay fees from %s", txSigner, feePayer)
}

// if there was a valid grant, ensure that the txSigner account exists (we create it if needed)
signerAcc := d.ak.GetAccount(ctx, txSigner)
if signerAcc == nil {
signerAcc = d.ak.NewAccountWithAddress(ctx, txSigner)
d.ak.SetAccount(ctx, signerAcc)
}
}

// now, either way, we know that we are authorized to deduct the fees from the feePayer account
feePayerAcc := d.ak.GetAccount(ctx, feePayer)
if feePayerAcc == nil {
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownAddress, "fee payer address: %s does not exist", feePayer)
}

// move on if there is no fee to deduct
if fee.IsZero() {
return next(ctx, tx, simulate)
}

// deduct fee if non-zero
err = auth.DeductFees(d.sk, ctx, feePayerAcc, fee)
if err != nil {
return ctx, err
}

return next(ctx, tx, simulate)
}
Loading