MystenLabs / sui

Sui, a next-generation smart contract platform with high throughput, low latency, and an asset-oriented programming model powered by the Move programming language
https://sui.io
Apache License 2.0
6.26k stars 11.21k forks source link

Feature request: Account Abstraction #8157

Open JackyDisc opened 1 year ago

JackyDisc commented 1 year ago

1. Motivation

We propose the following account abstraction model to enable general purpose smart contract wallet.

Compared with EOA, smart contract wallet can support numerous use cases such as account recovery, granulated permission control, role based multi-account management, pre-approve transactions, meta transactions, multi-factor authentication, e.t.c. We believe this will fundamentally change user experience with wallets, help adoption of applications on Sui.

Few examples of smart contract wallet capabilities:

  1. Add customize validations other than EOA signature (E.g. Add 2FA validation), thus more customization and security.
  2. Customized, granulated permission control (E.g. Require different level of validation for different coin transfer amount)
  3. Multi-signature wallet - Transactions can only be executed with multiple signatures that reaches a pre-defined threshold / rules.
  4. Account recovery - User is able to reclaim the wallet in case his/her private key is lost.

However, a general purpose smart contract wallet cannot be implemented in the current SUI MOVE implementation because:

  1. Missing of dynamic function dispatch - Move functions and structs cannot be constructed within the MOVE language, thus it's impossible for a smart contract to call arbitrary functions designated by user.
  2. Object cannot be used as an account. Only EOA accounts can submit and execute a transaction, and TxContext can only be constructed for EOA accounts.

Goals and scope

Here we propose the framework of account abstraction (Abbr. AA in following) that enables:

  1. Introduce the schema and data structure for abstracted account with a new signing schema, compatible with SUI object based system.
  2. The abstracted account can define a customized transaction validation rule and post transaction processing logic written in a move package, which are executed during transaction processing.
  3. The execution of abstracted account transactions can have the flexibility to follow fast path or BFT consensus, to fully utilize the flexible SUI move system.
  4. Sponsored gas fee (pay master) is not in the scope of this feature request. Although sponsored gas fee can utilize the same account model & storage structure, the interface for sponsored gas fee would better be discussed in a follow-up issue.

2 Design

2.1 Abstracted account

2.1.1 Definition

  1. Abstracted account is an account type that run some customized code in MOVE during transaction processing.
    1. The customization includes but not limit to: transaction signature (inputs) validation, txn prologue, post txn validation, e.t.c.
    2. More details about the customized code execution allowed for AA please see 2.2.1.
  2. Different from EOA account which is connected to a private key of a specific schema, each abstracted account is connected with an object (See 2.1.2) that points to move packages with a bunch of predefined AA operations.
  3. Abstracted account follows that same pattern as EOA accounts for storage & transaction execution.
    1. AA can call entry function as transaction payload.
    2. TxContext is constructed for AA in rust.
    3. In entry function calls, AA can only access owned/shared objects.
    4. AA can follow fast path consensus if accessed objects are are owned by the AA account itself.

2.1.2 AbstractedAccountData

image

  1. Each Abstracted account has a strict one-to-one mapping to a special object called AbstractedAccountData.
  2. The move object AbstractedAccountData is owned by the abstracted account, and is not transferrable.
  3. AbstractedAccountData points to an immutable object with data MovePackage which contains the compiled code of the AA account.
  4. Move object AbstractedAccountData has the following data structure.

    struct AbstractedAccountData has key, store {
      id: VersionedID,
      /// most recent version of the package. begins at 0. Mutable
      current_version: u64
      /// ID of the package object this cap has the permission to upgrade. Will be
      /// equal to package_id when latest_version is 0, unequal otherwise. Mutable
      current_package_id: ID,
    }

2.1.3 AA public key & address

**Public key**

Since an abstracted account is uniquely defined by the the special object AbstractedAccountData, the public key and address of the AA can be dependant on the object ID of AbstractedAccountData. Public key can be defined as the concatenation of:

  1. AA scheme specifier (E.g. 0x5) for AA account;
  2. Object ID of AbtractedAccountData.
PubKeyAA = AA_SCHEME (0x5) | AbstractedAccountData.ObjectID 

**Address**

The address of the AA is the hash of the public key, just as other accounts.

AddressAA = hash(PubKeyAA)

2.1.4 AA creation & upgrade

Both AA creation and upgrade can be done through a native function call. The native function call can be written in a SUI framework move file (e.g. abstracted_account.move)

**AA Creation**

// sui-framework/sources/abstracted_account.move
native fun create_aa(aaPackageID: UID, ctx: mut &TxContext)

Behind the native function call, the following logic will be executed:

  1. Move module of aaPackageID is validated against the AA function signatures (2.2.1).
  2. Object AbstractedAccountData is created with an object ID derived from TxContext.
  3. The owner of AbstractedAccountData is set to the AA address as computed from 2.1.3.

****AA code upgrade****

AA code can only be upgraded by the abstracted account himself.

// sui-framework/sources/abstracted_account.move
native fun upgrade_aa(newAAPackageID: UID, ctx: &mut TxContext)
  1. Check whether the transaction is sent by the abstracted account.
  2. New move module of newAAPackageID is validated against the AA function signatures (2.2.1).
  3. Object AbstractedAccountData is updated with new package id and version.

2.2 Transaction execution

2.2.1 AA package signature

Within the AA move package, some pre-defined function name and signatures must be implemented. The signatures are defined by the system and is called during transaction execution from abstracted accounts.

module 0xexample::aa_example {

    entry fun validate_tx(AAObjects: vector<UID>, vCtx: ValidateContext): bool

    entry fun tx_exec_prologue(AAObjects: vector<UID>, pCtx: PrologueContext, ctx: mut &TxContext)

    entry fun post_tx_exec(AAObjects: vector<UID>, peCtx: PostExecContext, ctx: mut &TxContext)
}

struct ValidateContext {
    input: vector<u8>,
    txPayload: TxPayload,
}

struct PrologueContext {
    input: vector<u8>,
    txPayload: TxPayload,
}

struct PostExecContext {
    txPayload: TxPayload,
    execEventBag: Bag,
}
  1. validate_tx : Customized validation rule being executed at transaction mem pool.
    1. The validation shall have a system-wide maximum gas that can be consumed. Validation that consumed more gas than the cap will fail.
    2. AAObjects is a list of objects that contains data for validation.
    3. Argument ValidateContext contains data input and txPayload which is constructed in rust to provide more context for transaction validation.
    4. If a transaction fails validate_tx will be kicked out of mem pool, and does not consume gas fee.
  2. tx_exec_prologue(Optional for AA) : Logic being executed before the execution of the transaction payload.
    1. Logic defined in tx_exec_prologue will consume gas in abstracted account whether fails or not.
    2. Argument PrologueContext provides additional message from transaction arguments for extra validation.
  3. post_tx_exec(Optional for AA): Logic being executed after the execution of transaction payload.
    1. Logic defined in post_tx_exec will consume gas in abstracted account whether fails or not.
    2. Argument PostExecContext is constructed in rust, and contains information about the transaction execution result (Events are put into execEventBag)
  4. All transactions contains arguments AAObjects: vector<UID> for AA processing.
    1. AA Objects can only be objects accessible by the abstracted account, either owned by AA, or are shared.
    2. The list of UIDs is passed in transaction inputs.
    3. Whether a transaction can go fast path consensus is decided by all UIDs accessed in a certain transaction.
    4. It’s the code defined in AA code that infer and get the data from UID, and use the objects for AA processing.

2.2.2 Transaction inputs

Abstracted accounts is able to execute any TransactionKind as other EOA accounts. The different is that EOA accounts use signatures for validation, and AA use some customized inputs instead of the signature field for validation.

Here is the full list of data need to be included in the “signature” field for AA transactions:

  1. List of objects accessed in AA function validate_tx.
  2. List of objects accessed in AA function tx_exec_prologue (if the function exists in AA code).
  3. List of objects accessed in AA function post_tx_exec (if the function exists in AA code).
  4. Customized input which is set in ValidateContext.input and PrologueContext.input.

Thus the “signature” field for AA transactions can be defined as:

sig_aa = bcs_encode( validate_objects | prologue_objects | post_objects | input ) 

2.2.3 Transaction execution

Thus we have the full process of transaction exeuction process for abstracted account.

  1. Transaction is fully composed at client side, and sent to the validator node.
  2. The validator node will evaluate whether the transaction can be executed in fast path or full BFT.
    1. A transaction can go fast path only if all objects accessed in the transaction is owned by the abstracted account.
    2. Depending on whether the transaction can be executed in fast path, the transaction is relayed to the corresponding validator node.
  3. When the transaction enters the tx mem pool, the transaction is validated.
    1. Validate the AA account address, public key & AbstractedAccountData (See 2.1.3)
    2. Rust constrcut ValidateContext, and execute the AA function validate_tx
    3. If the validation fails, the transaction is kicked out of the mem pool.
  4. The transaction is executed.
    1. tx_exec_prologue is executed with PrologueContext.
    2. Transaction body is executed
    3. post_tx_exec is executed with PostExecContext
    4. If any of the execution fails / aborts, the transaction fails.
    5. The gas fee consumed consumed in the process will not be reimbursed.

2.3 Discussion

2.3.1 gas DDoS attacks

One issue of the AA is about DDoS attack on gas. Since the transaction sent by AA will consume gas fee from AA account, it’s possible that some malicious account send overwhelmingly large number of transactions that drains the gas fee from AA account.

The solution to this issue is to introduce the function: validate_tx as in 2.1.1. validate_tx shall be a light-weighted validation function with a hard gas cap that does not consume gas fee if validation fails. The operation is supposed to consume gas fee which have the same magnitude as a single signature validation of EOA accounts. Thus, the issue of DDoS gas attack is resolved by validate_tx.

Also executing the validate_tx in the tx mempool prevents the DDoS attack from node level - It shall be not much more computation/resource resource consumed than normal transaction execution for signature verification.

2.3.2 Flexibility & Horizontal scale

The framework of AA is designed for horizontal scalability as SUI object system. The transaction code (AA code and transaction execution) have the flexibility to execute the transaction in fast path or global BFT consensus.

  1. If a transaction from an abstracted account only access objects that is owned by the sender, it can go with fast path, thus resulting in horizontally scale. For AA cases, object accessed contains both objects calls in AA methods, and transaction payload.
  2. The AA code can also access and mutate shared objects. In this case, AA execution will need to be confirmed by BFT process.

Thus, the AA design will inherit the full flexibility as SUI move system.

3 Extensive use cases

3.1 Smart contract wallet

As mentioned in motivation, one use case of the account abstraction is to implement the smart contract wallet that will provide a bunch of new features in addition to EOA accounts. The following features can be fully supported for abstracted smart contract wallet:

  1. Additional 2FA verification based on permission settings.
  2. Account recovery in case the owner lost his private key.
  3. Maintain an allowlist of recipient & protocols interact with.
  4. Granularized permission control
  5. E.t.c

3.2 Multi-sig wallet

For multi-signature wallet, although there is a native support of multi-account signer schema (https://github.com/MystenLabs/sui/issues/7187), it is not able to fulfill the full need for smart contract multi-sig, since:

  1. The native multi-sig wallet does not allow user to change owner / threshold / weight.
  2. Every transaction sent by the native multi-sig wallet must reach the same weight and from the same group of signers. It is not able to meet the requirements for some extensive solutions where role based user groups and granulated permission control is needed.
  3. A work-around for multi-sig is to use the smart contract with shared objects for object management. However, this requires the multi-sig wallet to go through BFT instead of fast path consensus.

Multi-sig with account abstraction is able to implement all features with horizontal scale.

3.3 DAO management tools

DAO governance & management can be implemented with account abstraction. The DAO tooling can take user held tokens as votes to vote on a specific proposal, and the proposal can be executed as the payload of the abstracted account transaction.

3.4 Sponsored gas fee (paymaster)

Sponsored gas is also able to be implemented with abstracted account. By adding some additional pre-defined functionality in AA code, an account can use another account to pay for gas fee. This will help reduce the friction of mass adoption of SUI network. The feature will be requested in another issue.

3.5 Some other use cases

Account abstraction can be the core feature that support the decentralized implementation of the following features:

  1. Pre-approval transactions.
  2. Meta transaction (for some use case).
  3. Account recovery.
  4. Granularized Multi-factor authentication.

Disclaimer:

We have been working on the design internally with careful considerations, sharing it here to get community feedbacks and suggestions.

Would love to explore the options with you and the opportunities it will bring.

References:

  1. [EIP-4337: Account Abstraction Using Alt Mempool](https://eips.ethereum.org/EIPS/eip-4337)
  2. [SUI third party package upgrade proposal](https://github.com/MystenLabs/sui/issues/2045)
  3. [SUI multi-account proposal](https://github.com/MystenLabs/sui/issues/7187)
JackyDisc commented 1 year ago

@sblackshear @kchalkias

tnowacki commented 1 year ago

Thanks for the writeup! I'm still reading things through, but at a first glance for these abstractions around validation/prologues/epilogues, you should be able to achieve that with programmable transactions #7790

You can make a module and an object that represents the account. Then set up entry functions for validation prologue and epilogue. You can enforce the state transition of validation then prologue then epilogue with capabilities. Changing your examples a bit

module 0xexample::aa_example {

struct ValidationReceipt { /* some useful validation metadata */ ... }
struct PrologueReceipt { /* some useful object metadata */ ... }

entry fun validate_tx(ctx: &mut TxContext): ValidationReceipt {
...
}

entry fun take_object<T: key + store>(validation: &ValidationReceipt, id: ID, ctx: mut &TxContext): (T, PrologueReceipt) {
...
}

entry fun return_object<T: key + store>(t: T,  receipt, PrologueReceipt, ctx: mut &TxContext) {
...
}

This doesn't sending objects to the account, but I'll write something up about that in a bit.

JackyDisc commented 1 year ago

Similar but totally different.

  1. The core of the account abstraction is to create a new account type that do transaction signature verification through code, instead of private key / public key. The new account type is orthogonal to transaction kinds, and execute all existing transaction types (including programmable transaction kind when implemented).

  2. An exemplary use case of account abstraction is multi-sig (The existing multi-account schema cannot fulfill user needs as mentioned in 3.2). But with account abstraction, a multi-sig account can be created, which does not belong to any single account, and go fast path consensus. And the application can define customized threshold & role based permission control, owner change e.t.c. This will give full flexibility

  3. I am aware SUI has many internal discussions regarding the extensive use cases of wallet user-experience, and have many ongoing proposals. Adding more support on node infra is straight-forward but not scalable since a framework is not supposed to handle the same level of complexity as in applications. I would recommend account abstraction as the key framework that allows application defining logics for account execution, offering a solution for work-around in absence of dynamic function dispatch in SUI move. All applications can define customized rules and enable following features to be built in a decentralized way on top of AA: Multi-signature, MFA, role based permission control, granularized access control, account recovery, general purpose DAO toolings, pre-approve transactions, e.t.c.

tnowacki commented 1 year ago
  1. The core of the account abstraction is to create a new account type that do transaction signature verification through code, instead of private key / public key. The new account type is orthogonal to transaction kinds, and execute all existing transaction types (including programmable transaction kind when implemented).

It's a neat idea! But a bit tricky to get right. We could add more signature schemes over time, but being able to do signature verification through code means that the code has to be metered. And to be metered it means you need a certificate from the validators. In other words, you can't really pay to do the verification until you've already verified that you have some SUI to pay for gas. And you need a certificate to do that.

In other words, I don't see a way to allow for arbitrary code to be executed safely during signing/certificate generation. The only way I see is to have an object that anyone can access (a shared object), and then do the verification in the Move layer, which brings us to...

  1. An exemplary use case of account abstraction is multi-sig (The existing multi-account schema cannot fulfill user needs as mentioned in 3.2). But with account abstraction, a multi-sig account can be created, which does not belong to any single account, and go fast path consensus. And the application can define customized threshold & role based permission control, owner change e.t.c. This will give full flexibility

We can't get into the single-writer path with arbitrary code being executed for signature verification. This was the point of my example above, you can implement this behavior with programmable transactions on top of shared objects.

I am aware SUI has many internal discussions regarding the extensive use cases of wallet user-experience, and have many ongoing proposals. Adding more support on node infra is straight-forward but not scalable since a framework is not supposed to handle the same level of complexity as in applications. I would recommend account abstraction as the key framework that allows application defining logics for account execution, offering a solution for work-around in absence of dynamic function dispatch in SUI move. All applications can define customized rules and enable following features to be built in a decentralized way on top of AA: Multi-signature, MFA, role based permission control, granularized access control, account recovery, general purpose DAO toolings, pre-approve transactions, e.t.c.

Programmable transactions + capabilities (as showing in the example with things like ValidationReceipt) can let you do adhoc things without having to publish new code. Though to simulate this all with shared objects, you will need the ability to send to objects instead of an address, which is something we are also working on.

JackyDisc commented 1 year ago

In other words, I don't see a way to allow for arbitrary code to be executed safely during signing/certificate generation. The only way I see is to have an object that anyone can access (a shared object), and then do the verification in the Move layer, which brings us to...

Exactly! The validation definitely need to be metered to prevent DDoS on both node level and gas level. As I mentioned in 2.3.1 gas DDoS attack, the signing/certificate validation is validated in AA code validate_tx, which has a hard limit in gas used in this function. The limit shall have the same magnitude as a signing schema verification. It's supposed to only do light signature verifications, no heavy lifting.

We can't get into the single-writer path with arbitrary code being executed for signature verification. This was the point of my example above, you can implement this behavior with programmable transactions on top of shared objects.

100% agree. single-writer path is not possible under the current framework. This is why I am proposing this feature request. Account abstraction allows implementation without shared objects and can fit into single-writer path. Basically, the shared object of the multi-sig is abstracted to a single account. Please check 2.2.3.

Programmable transactions + capabilities (as showing in the example with things like ValidationReceipt) can let you do adhoc things without having to publish new code. Though to simulate this all with shared objects, you will need the ability to send to objects instead of an address, which is something we are also working on.

Yes, programmable transactions is very very powerful, and it can also be used as a work around for dynamic function dispatch to some extend. These are all great! But it is different from the problem AA is trying to solve.

  1. AA allows single-writer path for extended account model.
  2. AA allows a new account to be created with some address, to own objects, and execute entry function.
  3. AA data is fully verifiable as move code, while programmable transactions is more ad-hoc.

So I think these two features are orthogonal and is trying to solve different problems, although some functionality can be possibly implemented in either ways :)

JackyDisc commented 1 year ago

Here is my understanding of the programmable transactions for a scenario where three users A, B, C want to swap some coin owned by mutual fund of A, B, C.

With programmable transaction kind:

  1. A, B, and C Create the shared move object multi-sig via smart contract, and put some assets in the multi-sig.
  2. A collect signature from B and C to send some amount of coin from multi-sig to A
  3. A programmable transaction is submitted to do the coin swap, including steps: a. withdraw coin from multi-sig to A's account. b. A account execute the coin swap. c. A return the swapped coin to multi-sig account.

The biggest issue with this solution is that, B & C can only approve step 3.a for fund withdraw. But step 3.b & 3.c is only proposed by A and cannot be verified by B and C because of the ad-hoc nature of the programmable transaction.

Do you think this is the right way to use programmable transaction?

JackyDisc commented 1 year ago

On the other hand, account abstraction is able to implement in a more straight forward way:

  1. A, B, C create an abstracted account as a multi-sig with its own address.
  2. User A propose a transaction to swap a the coin.
  3. User B, C approve the transaction.
  4. The abstracted account execute the swap.

If the transaction to be executed is single writer friendly, the whole process can go with single-writer path.

Do you guys have any concerns regarding executing the move code in abstracted account? Please let me know any concerns you may have. Thanks!

JackyDisc commented 1 year ago

I know the account abstraction is not a trivial feature. But the gains could be potentially even bigger compared to the difficulties to implement it :)

tnowacki commented 1 year ago

Here is my understanding of the programmable transactions for a scenario where three users A, B, C want to swap some coin owned by mutual fund of A, B, C.

With programmable transaction kind:

  1. A, B, and C Create the shared move object multi-sig via smart contract, and put some assets in the multi-sig.
  2. A collect signature from B and C to send some amount of coin from multi-sig to A
  3. A programmable transaction is submitted to do the coin swap, including steps: a. withdraw coin from multi-sig to A's account. b. A account execute the coin swap. c. A return the swapped coin to multi-sig account.

The biggest issue with this solution is that, B & C can only approve step 3.a for fund withdraw. But step 3.b & 3.c is only proposed by A and cannot be verified by B and C because of the ad-hoc nature of the programmable transaction.

Do you think this is the right way to use programmable transaction?

On the other hand, account abstraction is able to implement in a more straight forward way:

  1. A, B, C create an abstracted account as a multi-sig with its own address.
  2. User A propose a transaction to swap a the coin.
  3. User B, C approve the transaction.
  4. The abstracted account execute the swap.

If the transaction to be executed is single writer friendly, the whole process can go with single-writer path.

Do you guys have any concerns regarding executing the move code in abstracted account? Please let me know any concerns you may have. Thanks!

With the exception of the single writer path, there is nothing in account abstraction as proposed that cannot be implemented with programmable transactions + send to object (which I know I have not described here but bear with me).

You can implement exactly the AA way with shared objects.

  1. A, B, C create an account shared object with a custom multi sig. This shared object can have objects sent to it with dynamic fields. And its own object store can be maintained with dynamic fields. Essentially you can have something like struct Account has key { id: UID, /* signature information */, objects: Bag<ObjectID> }
  2. A proposes a transaction to swap the coin from the bag. This could be done either by creating a new shared object that B and C can come and sign, or by coordinating off chain and signing a programmable transaction that does this all lock-step.
  3. Execute the swap.

Send to object would expose something that would allow people to call transfer where instead of passing in an address they pass in a ObjectID for the Account

JackyDisc commented 1 year ago

I have some concerns for step 2.

This could be done either by creating a new shared object that B and C can come and sign.

AFAIC, one of the biggest limitation in this step is absence of dynamic function dispatch. If the multi-sig functionality is limited to swap or a certain number of entry functions, the described solution will work. But what if the multi-sig owners want to execute an arbitrary kind of entry functions (E.g. Stake, lend, swap, GameFi, or some protocol governance entry function calls)? Is there any work around other than the multi-sig smart contract writing adapters for each smart contracts to interact with?

Coordinating off chain and signing a programmable transaction that does this all lock-step.

Swap is a stateless function, and the process might create some problem when the application is stateful. E.g. A certain smart contract is recording the account address for future airdrops. From what we have in the programmable transaction, user will need to withdraw the assets to some account, and it's the account holding the asset that execute the transaction, not the shared object. Thus the beneficiary is account A instead of the shared multi-sig object. This also applies to most account based games where the account is the beneficiary for consuming some in-app assets, and shared account is not possible in this case.

How do you think programmable transaction can solve these issues?

JackyDisc commented 1 year ago

@tnowacki Adding one more concerns for the programmable transaction: Currently an object cannot cannot be the recipient of the transfer::transfer function. The dynamic_object_field can partially solve the issue, but there will be some friction when other wallet is transferring some object to the multi-sig. How do you think this issue can be resolved?

poelzi commented 2 months ago

@JackyDisc I think this is solved and an object can be the owner of an object now.