Try   HackMD

Objective

There are two main objectives in implementing account abstraction

  • Add flexibility in what represents an account and how they are managed
  • Add flexibility to account authentication when executing transactions/messages

Accounts

Right now an account is mainly represented by a key pair and and account's address can be derived from its public key. An account gets an account id when it first interacts with the chain. This has particular drawbacks for key rotation, but also the benefit of account addresses being deterministic across chains.

Authentication

Authentication is currently handled in the anteHandler by extracting the signers of each message and comparing them to the list of signatures in the tx.

This could be expanded to an interface, so that more complex authentication mechanisms can be implemented.


Of the two problems, I think authentication is the highest impact one and the one we should focus on.

Summary

This document outlines an alternative approach to the sdk's x/accounts proposal with the following goals

  • Allow account abstraction to happen by enhancing existing accounts, without having to migrate them to a new module or require account creation as a separate step
  • Incremental updates. We can get a lot of the bennefits of account abstraction by making a few small changes to the runTx logic to provide granular authentication without having to fully redefine accounts
  • An opinionated definition of authentication composability (top-level anyOf and an interface for authenticate accounts and modify authenticator's state)

Design

General

Accounts should be able to specify how their txs are authenticated. To do this, authentications should have their own processing section in baseapp.runTx() that calls a piece of code that implements the Authenticator interface.

Each account can then specify which code to use as their authenticator. Some examples would be:

  • ClassicAuthentication (the current implementation – uses SDK signature verification)
  • CosmwasmAuthenticator (calls a contract which returns a bool)
  • ZKAAuthenticator (prove something in zk to get authenticated)

Authenticators may be stateful.

Backwards compatibility

Switching existing accounts to using authenticators should be transparent. No need to manually migrate an account.

This can be achieved by setting the default authenticator to a ClassicAuthenticator on upgrade, and allowing users to change it later.

Integrations

The switch should be transparent for existing wallets (kepler, leap, trust, etc). If some authenticator require extra data in the tx, this should be optional

There's people already working on versions of this via smart-contract wallets (obi, vectis, and surely others). We should make it so that they can easily register their contracts as an authenticator by providing an interface addapter (i.e.: implementing the right message interface that maps to their existing implementation)

Existing applications

The modifications that existing applications need to make to support abstracted accounts should be minimal.

For the most common cases (different keys managing the same account but potentially having different permissions), this should be doable by just modifying the application to use one address as the account to display data for while allowing different keys to sign the messages.

This is an easy decoupling that can be added to the existing libraries (cosmjs,osmojs,etc)

Multiple Authenticators

An account can register multiple authenticators. For example, a single account can have a list of:

[classic(secp256k1), classic(multsig), cosmwasm(vectis), cosmwasm(obi)]

These authenticators are processed in an ANYOF mechanism. A tx needs to specify which of the authenticators its "authentcation data" is intended to be run against.

Any more complex authentication logic should be implemented in a specfic authenticator (such as a cosmwasm contract).

To preserve backwards compatibility, a default authenticator (the first of the list) can be selected if the tx does not explicitly specify it

Composibility

Ideally, authenticators should be reasonably composable.

For example, if someone creates a new signature verification scheme as a CosmWasm authenticator by having it meet the necessesary SUDO msg interface, it would be nice to also be able to reuse that signature verification scheme in a more complex smart contract authenticator.

To do this, we suggest the creation of an optional but standardized ExecMsg interface that CosmWasm authenticators implement, that is maximally similar to the Sudo interface, so that these can be called by other CosmWasm authenticators.

Design

The following items are relevant when determining how to authenticate a tx:

  • Account Each message specifies which account it should be executed as. This is currently implemented in each message's GetSigners()
  • AuthenticationData Different authenticators may require different data so this should be bytes and allow the authenticator to interpret it as needed. The only authentication data currently being used is the provided signatures. This is stored in the tx proto'sSignature and AuthInfo fields.

The methods and fields currently being used for this can be overloaded to house the more generic data needed for an account abstraction system. This allows us to retain backwards compatibility with existing

If a cleaner interface is needed, we could rename some of the methods with names that better apply to this new account paradigm.

The following section expand on obtaining the necessary data from the existing tx format

Backwards compatibility

Determining the account

If the signature gets decoupled from account ownership, then we're left with the problem of determining which account should be executing the submitted messages.

Luckily, this is currently determined by information in the message and obtained via msg.GetSigners() or the cosmos.msg.v1.signer protobuf annotation. Similarly, the "signer" specified in the message can be used as the expected account.

Once the account has been determined, its correct authenticator code to be used

Obtaining the authentication data

The data required to verify signatures in standard txs at the moment comes from the tx proto's Signatures [][]byte and AuthInfo *AuthInfo fields.

Authenticators should implement a function that processes these fields and encodes the necessary data in the format it expects

This function must be gas-limited as it will be used when determining the fee payer of the tx

Any authentication data can be encoded in the Signatures field. Currently, it contains just the signature, and the ClassicAuthenticator should be able to continue ussing it as-is.

Fees

As authentication is decoupled from signatures, authenticating accounts can require a variable amount of compute. This can be problematic as we want to be able to exit a transaction early if the provided fees are not enough. Moreover, some transaction senders may want to ensure that the fee can be paid even if the account executing the messages doesn't have enough funds. Both of these cases can be solved by explicitly including a fee payer in the tx, and requiring authentication of the fee payer is low-gas. The following subsection expands on this:

Ensuring fee availability

The fee payer is currently specified in the tx's AuthInfo.Fee.FeePayer. By default, this set to be the first signer of the first message of the transaction.

Authenticating the fee payer needs to be low-gas enough to ensure it can be processed.

  • Require the FeePayer extraction is under a maximum gas limit in the DeductFeeDecorator ante handler
  • Allow users to specify a separate fee payer that can be authenticated easily if the rest of the tx's authentication is complex.

To specify the fee payer, a tx sender can include a message with either a classic signature or a fee grant as first message of the tx (given that the default FeePayer is determined by getting the first message's signer). Any existing message would work for this, but for cleaner semantics, we should add a custom message specifically for setting the fee payer.

Fee limits per authenticator

If users can set authenticators in a permissionless manner, there is a risk that a mallicious authenticator can drain all the funds of an account by executing long-running code that consumes fees. At the same time, complex, legitimate authenticators should be allowed to consume enough fees to execute properly

To mittigate this, users should be able to set the maximum fee that each authenticator is allowed to consume. This will require us to modify the DeductFeeDecorator ante handler to be aware of authenticators

Adding and removing authenticators

Accounts should be able manage the authenticators added to their accounts. We can do this by providing AddAuthenticator / RemoveAuthenticator messages that accept any of the authenticators that the chain has registered as valid top-level authenticators

Adding and removing authenticators then behaves like any other message. If the existing authenticators authorize the tx, the message is allowed to execute.

For safety, we may want most authenticators to be unable to add or remove other authenticators. This can be achived via composibility, where authenticators are always added after checking that the message is not one for adding or removing other authenticators

Authenticators may implement a method to be called when the are added or removed so they can initialize any necessary state or reject addition/removal if the necessary conditions aren't met

Account state and state transitions

Authenticators shouldbe allowd to modify only their own state. To achieve this, we can have their storage be behind a specific prefix, give them only a context with the appropriate storage, (or verify the modified prefixes after their execution)

It is up to each authenticator how to manage this state.

The modified authenticator state should be commited regardless of whether the tx is authorized or not. It is the responsibility of the authenticators to ensure state is only modified when it's sefe

Open Questions
  • Can we make this safer by allowing authenticators to specify if their state changes should be committed or reverted?

Suggested Interfaces

type (
    Authenticator interface {
        GetAuthenticationData(tx Tx) [][]byte
        Authenticate(ctx Context, msg Msg, authenticationData []byte) bool
        ConfirmExecution(ctx Context, msg Msg, authenticated bool, authenticationData []byte) bool
        
        // Suggestion
        //MempoolAuthenticate(...) // must be low gas
        
        // Optional
        OnAuthenticatorAdded(...) bool
        OnAuthenticatorRemoved(...) bool
    }

    Tx interface {  // existing Tx interface
        ... 
        GetAuthenticators() []Authenticator  // Alternatively, it can be a func in baseapp
    }
)

GetAuthenticationData() will extract any data that is necessary for the authenticators to properly process the tx from the tx bytes. This will later be provided to the authenticators.
Authenticate() will be called before the tx executes and return a bool that determines if the tx can be executed or not. This function should only be allowed to modify state that "belongs to the authenticator" (i.e.: prefixed by a certain key)
ConfirmExecution() has the same properties as the the previous function, but is called after the tx executes. This allows implementors to verify invariants (e.g.: balance before tx and after tx is the same), and modify their state based on changes made by the messages

GetAuthenticators() will return an array of authenticators for each message.

Composing authenticators

With the interface defined above, authenticators can easily compose with each other. If an authenticator implements signature verification,

At the top level, the following authenticators would be useful:

  • SignatureVerification supports the signature verification as currently implemented by the sdk
  • UnifyingAuthenticator allows several keys to be share same authentication rules
  • AllOf takes a list of authenticators to call sequentially and only authenticates if all of the sub-authenticators also authenticate
  • AnyOf takes a list of authenticators to call sequentially and authenticates if at least one of the sub-authenticatores authenticates
  • CwAuthenticator calls a cosmwasm contract that implements the authentication interface

I'll expand on some of the potential authenticators bellow

UnifyingAuthenticator

One useful primitive is to allow several wallets to manage one account. As an example, suppose someone wants to manage their account using desktop keplr, mobile keplr, and leap wallet.

Right now this is can be done by importing the private key on each of these applications, which leads to security concerns and makes it hard to revoke acces to a rogue aplication. This could instead be implemented by having an authenticator that contains a list of key pairs that can authenticate as an account, and a sub-authenticator as its permissions.

The unifying authenticator can verify the provided signature and call another sub-authenticator under the hood acting as the original account.

This is dependent on the sub-authenticator not needing to check the signature themselves. Most authenticators should fall into this category

CwAuthenticator

The CwAuthenticator is particularly useful as it allows users to deploy new authenticators as conrtacts (without requiring a chain upgrade).

Authenticators written in cosmwasm should implement the interface provided above as execute and sudo messages, but with the difference that the account being authenticated for, is passed as part of the message.

#[cw_serde]
pub enum ExecuteMsg {
    Authenticate {
        msg: CosmosMsg,
        account: Addr,
        authenticationData: String  // or a relevant struct
    },
    ConfirmExecution {
        msg: CosmosMsg,
        account: Addr,
        authenticationData: String  // or a relevant struct
    },
}

#[cw_serde]
pub enum SudoMsg {
    // same as above
}

The reason to pass the account as part of the message is that in the cosmwasm execution model, when calling other contracts, the sender is automatically set to the caller contract. To get around this, we add the account being authenticated for as part of the message, and require contracts to validate that the sender is allowed to call it.

There are different ways to do this validation (and it should be up to each authenticator to decide), but one simple way to do this is for aithenticators to be instantiated with a field specifying who is allowed to call them. If the contract is being called via sudo() the provided account is passed directly from the chain and known to have been extracted via GetSigners(). If the contract is being called via execute(), the authenticator can validate that the sender is the contract who it was setup to serve on instantiation.

Using accounts from cosmwasm contracts

The message passing model from cosmwasm means that there is a different route for handling messages emitted by contracts. Specifically, these are executed via the handleSdkMessage() function in wasmd.

This function would need to be made aware of the account abstraction paradigm so that it can get the correct authorizer and validate it before and after the message execution.

Once this is done, allowing cosmwasm contracts to take advantage of account abstraction should be easy enough: the account just needs an authenticator that lets the contract execute on its behalf.

Open Questions

  • Do we want GetAuthenticators() on the tx, on baseapp, or on msgs?
  • Do we want to expand the Tx proto to allow for extra authentication data? I think this is useful and can be made in a backwards compatible, additive way

Implementation plan

Phase 1: Separate authentication from message validation

  • Move account validation out of validate basic and into its own handlers
  • Implement the initial validation based on signature validation
  • Add interface to allow each message to add custom authentication logic in its definition?
  • Move this logic out of the ante handler and onto its own "section" of runTx
  • Add authentication "updates" after the tx succeeds

Phase 2: Custom authenticators

  • Add a CosmWasm authenticators
    • Idea: the caller for cw authenticators could validate the signature if it exists so the contracts don't have to deal with this. If it validates, execute with the appropriate sender, otherwise execute with an empty sender
    • Idea: create an abstract cw-authenticator interface using sylvia. This should handle the composability so that other authenticator implementors don't have to reason about it

Phase 3: UI and authenticators changes

  • Allow users to switch their authenticators.
  • Proveide a UI.

References