cosmos-sdk/x/auth/keeper/keeper.go
Tyler a158c24b38
refactor: unordered transactions ante handling (#24573)
Co-authored-by: Aaron Craelius <aaronc@users.noreply.github.com>
2025-04-28 20:30:01 +00:00

380 lines
13 KiB
Go

package keeper
import (
"context"
"errors"
"fmt"
"time"
gogotypes "github.com/cosmos/gogoproto/types"
"cosmossdk.io/collections"
"cosmossdk.io/collections/indexes"
"cosmossdk.io/core/address"
"cosmossdk.io/core/store"
errorsmod "cosmossdk.io/errors"
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/codec"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth/types"
)
// AccountKeeperI is the interface contract that x/auth's keeper implements.
type AccountKeeperI interface {
// Return a new account with the next account number and the specified address. Does not save the new account to the store.
NewAccountWithAddress(context.Context, sdk.AccAddress) sdk.AccountI
// Return a new account with the next account number. Does not save the new account to the store.
NewAccount(context.Context, sdk.AccountI) sdk.AccountI
// Check if an account exists in the store.
HasAccount(context.Context, sdk.AccAddress) bool
// Retrieve an account from the store.
GetAccount(context.Context, sdk.AccAddress) sdk.AccountI
// Set an account in the store.
SetAccount(context.Context, sdk.AccountI)
// Remove an account from the store.
RemoveAccount(context.Context, sdk.AccountI)
// Iterate over all accounts, calling the provided function. Stop iteration when it returns true.
IterateAccounts(context.Context, func(sdk.AccountI) bool)
// Fetch the public key of an account at a specified address
GetPubKey(context.Context, sdk.AccAddress) (cryptotypes.PubKey, error)
// Fetch the sequence of an account at a specified address.
GetSequence(context.Context, sdk.AccAddress) (uint64, error)
// Fetch the next account number, and increment the internal counter.
NextAccountNumber(context.Context) uint64
// GetModulePermissions fetches per-module account permissions
GetModulePermissions() map[string]types.PermissionsForAddress
// AddressCodec returns the account address codec.
AddressCodec() address.Codec
}
func NewAccountIndexes(sb *collections.SchemaBuilder) AccountsIndexes {
return AccountsIndexes{
Number: indexes.NewUnique(
sb, types.AccountNumberStoreKeyPrefix, "account_by_number", collections.Uint64Key, sdk.AccAddressKey,
func(_ sdk.AccAddress, v sdk.AccountI) (uint64, error) {
return v.GetAccountNumber(), nil
},
),
}
}
type AccountsIndexes struct {
// Number is a unique index that indexes accounts by their account number.
Number *indexes.Unique[uint64, sdk.AccAddress, sdk.AccountI]
}
func (a AccountsIndexes) IndexesList() []collections.Index[sdk.AccAddress, sdk.AccountI] {
return []collections.Index[sdk.AccAddress, sdk.AccountI]{
a.Number,
}
}
// AccountKeeper encodes/decodes accounts using the go-amino (binary)
// encoding/decoding library.
type AccountKeeper struct {
addressCodec address.Codec
storeService store.KVStoreService
cdc codec.BinaryCodec
permAddrs map[string]types.PermissionsForAddress
bech32Prefix string
// enableUnorderedTxs enables unordered transaction support.
// This boolean helps sigverify ante handlers to determine if they should process unordered transactions.
enableUnorderedTxs bool
// The prototypical AccountI constructor.
proto func() sdk.AccountI
// the address capable of executing a MsgUpdateParams message. Typically, this
// should be the x/gov module account.
authority string
// State
Schema collections.Schema
Params collections.Item[types.Params]
AccountNumber collections.Sequence
Accounts *collections.IndexedMap[sdk.AccAddress, sdk.AccountI, AccountsIndexes]
UnorderedNonces collections.KeySet[collections.Pair[int64, []byte]]
}
type InitOption func(*AccountKeeper)
// WithUnorderedTransactions enables unordered transaction support.
// When true, sigverify ante handlers will validate and process unordered transactions.
// When false, sigverify ante handlers will reject unordered transactions.
func WithUnorderedTransactions(enable bool) InitOption {
return func(ak *AccountKeeper) {
ak.enableUnorderedTxs = enable
}
}
var _ AccountKeeperI = &AccountKeeper{}
// NewAccountKeeper returns a new AccountKeeperI that uses go-amino to
// (binary) encode and decode concrete sdk.Accounts.
// `maccPerms` is a map that takes accounts' addresses as keys, and their respective permissions as values. This map is used to construct
// types.PermissionsForAddress and is used in keeper.ValidatePermissions. Permissions are plain strings,
// and don't have to fit into any predefined structure. This auth module does not use account permissions internally, though other modules
// may use auth.Keeper to access the accounts permissions map.
func NewAccountKeeper(
cdc codec.BinaryCodec, storeService store.KVStoreService, proto func() sdk.AccountI,
maccPerms map[string][]string, ac address.Codec, bech32Prefix, authority string, opts ...InitOption,
) AccountKeeper {
permAddrs := make(map[string]types.PermissionsForAddress)
for name, perms := range maccPerms {
permAddrs[name] = types.NewPermissionsForAddress(name, perms)
}
sb := collections.NewSchemaBuilder(storeService)
ak := AccountKeeper{
addressCodec: ac,
bech32Prefix: bech32Prefix,
storeService: storeService,
proto: proto,
cdc: cdc,
permAddrs: permAddrs,
authority: authority,
Params: collections.NewItem(sb, types.ParamsKey, "params", codec.CollValue[types.Params](cdc)),
AccountNumber: collections.NewSequence(sb, types.GlobalAccountNumberKey, "account_number"),
Accounts: collections.NewIndexedMap(sb, types.AddressStoreKeyPrefix, "accounts", sdk.AccAddressKey, codec.CollInterfaceValue[sdk.AccountI](cdc), NewAccountIndexes(sb)),
UnorderedNonces: collections.NewKeySet(sb, types.UnorderedNoncesKey, "unordered_nonces", collections.PairKeyCodec(collections.Int64Key, collections.BytesKey)),
}
schema, err := sb.Build()
if err != nil {
panic(err)
}
ak.Schema = schema
for _, opt := range opts {
opt(&ak)
}
return ak
}
func (ak AccountKeeper) UnorderedTransactionsEnabled() bool {
return ak.enableUnorderedTxs
}
// GetAuthority returns the x/auth module's authority.
func (ak AccountKeeper) GetAuthority() string {
return ak.authority
}
// AddressCodec returns the x/auth account address codec.
// x/auth is tied to bech32 encoded user accounts
func (ak AccountKeeper) AddressCodec() address.Codec {
return ak.addressCodec
}
// Logger returns a module-specific logger.
func (ak AccountKeeper) Logger(ctx context.Context) log.Logger {
return sdk.UnwrapSDKContext(ctx).Logger().With("module", "x/"+types.ModuleName)
}
// GetPubKey Returns the PubKey of the account at address
func (ak AccountKeeper) GetPubKey(ctx context.Context, addr sdk.AccAddress) (cryptotypes.PubKey, error) {
acc := ak.GetAccount(ctx, addr)
if acc == nil {
return nil, errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "account %s does not exist", addr)
}
return acc.GetPubKey(), nil
}
// GetSequence Returns the Sequence of the account at address
func (ak AccountKeeper) GetSequence(ctx context.Context, addr sdk.AccAddress) (uint64, error) {
acc := ak.GetAccount(ctx, addr)
if acc == nil {
return 0, errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "account %s does not exist", addr)
}
return acc.GetSequence(), nil
}
func (ak AccountKeeper) getAccountNumberLegacy(ctx context.Context) (uint64, error) {
store := ak.storeService.OpenKVStore(ctx)
b, err := store.Get(types.LegacyGlobalAccountNumberKey)
if err != nil {
return 0, fmt.Errorf("failed to get legacy account number: %w", err)
}
v := new(gogotypes.UInt64Value)
if err := v.Unmarshal(b); err != nil {
return 0, fmt.Errorf("failed to unmarshal legacy account number: %w", err)
}
return v.Value, nil
}
// NextAccountNumber returns and increments the global account number counter.
// If the global account number is not set, it initializes it with value 0.
func (ak AccountKeeper) NextAccountNumber(ctx context.Context) uint64 {
n, err := collections.Item[uint64](ak.AccountNumber).Get(ctx)
if err != nil && errors.Is(err, collections.ErrNotFound) {
// this won't happen in the tip of production network,
// but can happen when query historical states,
// fallback to old key for backward-compatibility.
// for more info, see https://github.com/cosmos/cosmos-sdk/issues/23741
n, err = ak.getAccountNumberLegacy(ctx)
}
if err != nil {
panic(err)
}
if err := ak.AccountNumber.Set(ctx, n+1); err != nil {
panic(err)
}
return n
}
// GetModulePermissions fetches per-module account permissions.
func (ak AccountKeeper) GetModulePermissions() map[string]types.PermissionsForAddress {
return ak.permAddrs
}
// ValidatePermissions validates that the module account has been granted
// permissions within its set of allowed permissions.
func (ak AccountKeeper) ValidatePermissions(macc sdk.ModuleAccountI) error {
permAddr := ak.permAddrs[macc.GetName()]
for _, perm := range macc.GetPermissions() {
if !permAddr.HasPermission(perm) {
return fmt.Errorf("invalid module permission %s", perm)
}
}
return nil
}
// GetModuleAddress returns an address based on the module name
func (ak AccountKeeper) GetModuleAddress(moduleName string) sdk.AccAddress {
permAddr, ok := ak.permAddrs[moduleName]
if !ok {
return nil
}
return permAddr.GetAddress()
}
// GetModuleAddressAndPermissions returns an address and permissions based on the module name
func (ak AccountKeeper) GetModuleAddressAndPermissions(moduleName string) (addr sdk.AccAddress, permissions []string) {
permAddr, ok := ak.permAddrs[moduleName]
if !ok {
return addr, permissions
}
return permAddr.GetAddress(), permAddr.GetPermissions()
}
// GetModuleAccountAndPermissions gets the module account from the auth account store and its
// registered permissions
func (ak AccountKeeper) GetModuleAccountAndPermissions(ctx context.Context, moduleName string) (sdk.ModuleAccountI, []string) {
addr, perms := ak.GetModuleAddressAndPermissions(moduleName)
if addr == nil {
return nil, []string{}
}
acc := ak.GetAccount(ctx, addr)
if acc != nil {
macc, ok := acc.(sdk.ModuleAccountI)
if !ok {
panic("account is not a module account")
}
return macc, perms
}
// create a new module account
macc := types.NewEmptyModuleAccount(moduleName, perms...)
maccI := (ak.NewAccount(ctx, macc)).(sdk.ModuleAccountI) // set the account number
ak.SetModuleAccount(ctx, maccI)
return maccI, perms
}
// GetModuleAccount gets the module account from the auth account store, if the account does not
// exist in the AccountKeeper, then it is created.
func (ak AccountKeeper) GetModuleAccount(ctx context.Context, moduleName string) sdk.ModuleAccountI {
acc, _ := ak.GetModuleAccountAndPermissions(ctx, moduleName)
return acc
}
// SetModuleAccount sets the module account to the auth account store
func (ak AccountKeeper) SetModuleAccount(ctx context.Context, macc sdk.ModuleAccountI) {
ak.SetAccount(ctx, macc)
}
// add getter for bech32Prefix
func (ak AccountKeeper) getBech32Prefix() (string, error) {
return ak.bech32Prefix, nil
}
// GetParams gets the auth module's parameters.
func (ak AccountKeeper) GetParams(ctx context.Context) (params types.Params) {
params, err := ak.Params.Get(ctx)
if err != nil && !errors.Is(err, collections.ErrNotFound) {
panic(err)
}
return params
}
// -------------------------------------
// Unordered Nonce management methods
// -------------------------------------
// ContainsUnorderedNonce reports whether the sender has used this timeout already.
func (ak AccountKeeper) ContainsUnorderedNonce(ctx sdk.Context, sender []byte, timeout time.Time) (bool, error) {
return ak.UnorderedNonces.Has(ctx, collections.Join(timeout.UnixNano(), sender))
}
// TryAddUnorderedNonce tries to add a new unordered nonce for the sender.
// If the sender already has an entry with the provided timeout, an error is returned.
func (ak AccountKeeper) TryAddUnorderedNonce(ctx sdk.Context, sender []byte, timeout time.Time) error {
alreadyHas, err := ak.ContainsUnorderedNonce(ctx, sender, timeout)
if err != nil {
return fmt.Errorf("failed to check unordered nonce in storage: %w", err)
}
if alreadyHas {
return fmt.Errorf("sender %s has already used timeout %d", sdk.AccAddress(sender).String(), timeout.UnixNano())
}
return ak.UnorderedNonces.Set(ctx, collections.Join(timeout.UnixNano(), sender))
}
// RemoveExpiredUnorderedNonces removes all unordered nonces that have a timeout value before
// the current block time.
func (ak AccountKeeper) RemoveExpiredUnorderedNonces(ctx sdk.Context) error {
blkTime := ctx.BlockTime().UnixNano()
it, err := ak.UnorderedNonces.Iterate(ctx, collections.NewPrefixUntilPairRange[int64, []byte](blkTime))
if err != nil {
return err
}
defer it.Close()
keys, err := it.Keys()
if err != nil {
return err
}
for _, key := range keys {
if err := ak.UnorderedNonces.Remove(ctx, key); err != nil {
return err
}
}
return nil
}