-
Notifications
You must be signed in to change notification settings - Fork 1
Account Unification #49
Description
Overview
We propose an account and transaction model that unifies all accounts into a single smart contract-based account type. Under this proposal, unlike Ethereum today, Spacemesh would have no "EOA" (externally owned account) or "simple", keypair-based account (which cannot contain code). This abstracts or encapsulates the logic that allows nodes to verify transactions (e.g., by ensuring that they have valid signatures and nonces) into smart contract logic as part of a special verify method. It means that this verification logic, like all smart contract execution logic, runs in the VM and may in theory be arbitrarily complex (although in practice it will be subject to certain restrictions to prevent denial of service and griefing attacks, and will be optimized).
We introduce a subclass of account known as a "Principal" account, so-called because it's able to act as a principal, or source of funds, in a transaction. (The user who generates, signs, and broadcasts the transaction acts as its agent.) The only difference between a Principal and non-Principal account is that a Principal account implements the verify method that is executed to verify transactions sent on its behalf, and is thus allowed to act as a source of funds, while a non-Principal account implements no such method and is forbidden from acting as a source of funds. (Equivalently, all accounts can be thought of as Principal accounts, but those that don't explicitly implement verify() instead have an implicit implementation that always returns false and may therefore never be used as a source of funds.)
EOAs are replaced by a "personal wallet" smart contract template with simple, cheap, sanctioned verification logic that is effectively identical to the existing verification logic.
For background reading, please see:
- research forum thread and tag
- Account abstraction (single account type) product#86
- EIP-2938 for the most recent, mature, and complete proposal (in particular, the Rationale section)
- EIP-2938 Account Abstraction Explained
- Account Abstraction Beyond EIP-2938
- Account Abstraction
- Account Abstraction (EIP-2938): Why & What
- Implementing account abstraction as part of eth1.x - EIPs - Fellowship of Ethereum Magicians
Goals and motivation
- simplicity and elegance: having only a single account type makes the core code simpler and makes it easier to reason about the protocol
- more flexibility in how transactions are funded. Rather than every transaction needing to be funded from an EOA, a smart contract can self-fund transactions that target it. Think of a multisig where the fees for transactions that move things into and out of the multisig are funded by the multisig itself, or an application such as a decentralized exchange that pays gas fees for its users. This also leads to...
- better privacy, since it breaks the link between transaction sender and source of funds. It fixes the catch-22 that currently requires users to acquire coins in order to move coins out of a mixer or privacy tool such as tornado.cash.
- Users are not bound by a single, sanctioned transaction verification scheme and are free to experiment with whichever signature scheme (Scnorr, BLS, post-quantum, etc.) or other transaction verification method they like: single sig, multisig, social recovery, etc. Note that this extends to replay protection and nonce verification too: different principal accounts may choose to implement different classes of replay protection, or even to omit it entirely.
- It’s easier to prune certain classes of “failed” transactions–i.e., those that fail arbitrary validation checks (e.g., involving transaction ordering)
Tradeoffs/downsides/challenges
- Makes mempool management harder for miners. It’s harder for a miner to determine whether a transaction will pay any gas (mitigated by certain constraints).
- There is a potential DoS attack vector if “spam” transactions can be generated that require a lot of verification work without paying gas (in practice, it must not be possible to generate transactions that require significantly more work than validating the signature on a “simple” tx requires now)
- In order to prevent replay attacks (and maintain the uniqueness constraint on txids), the nonce of a Principal contract must never be reset to zero, even if the deployed smart contract is self-destructed
- The multi-tenant use case (e.g., a DEX or a multisig shared by many users) is tricky because one transaction can invalidate multiple pending transactions with the same target (by, e.g., increasing the nonce). There are potential workarounds including nonce malleability.
High-level design
- the protocol implements a single account model. An account is a data structure consisting of the following:
address-> [code,balance]. - the
senderfield of a transaction is replaced by aprincipalfield. All transactions contain this field, and it must never be nil (with the possible exceptions of coinbase and reward transactions). Theprincipalis the account whose balance is debited for gas fees and sends (inside or outside the VM). If theprincipalof a transaction is not set, or is set to the address of an account that is not a Principal account, or if the transaction nonce does not match the Principal account's nonce, that transaction is invalid. If the balance of the Principal account is not sufficient to pay gas and cover all funds transfers contained in the transaction, the transaction fails. - all accounts whose
codeimplements theverify()method are considered Principal accounts. - there are certain restrictions on which state
verify()can read and write. See "State management", below, for more. - funds may be sent to an account that does not yet exist (as in Ethereum). When this happens, the account is spontaneously created and its balance is updated (but its
coderemains unset). A smart contract may later be deployed at this address, in which case it "inherits" the pre-existing balance. (This allows a user to receive funds into a Principal account "counterfactually", as if the account code were already there, and only requires the user to deploy the account code when they need to transfer or spend the funds.) Note that a Principal account may not serve as a source of funds for any transaction, including deploying code to the account itself, until after that code has been deployed. - in order to faithfully replicate EOA behavior, whereby a user can immediately spend funds accumulated into such an account "counterfactually" without needing an external source of funds, we allow a special self-funded spawn transaction. When a Spawn transaction specifies a template address, no
principal, and thedestinationaccount contains funds,verifydatais passed into the template'sverify()method, and if it returns true, the template is spawned todestinationanddestinationpays gas for the spawn operation. - when a node receives a new transaction, it performs the following checks, in order, to determine whether the transaction is valid:
- ensure that the transaction is syntactically valid and that all required fields (
principal,destination,amount,calldata,verifydata) are set - ensure that the
principalfield is set to the address of a Principal account (i.e., that this account implements theverify()method) and that the account contains enough gas to verify the transaction - call the
verify()method of the referenced Principal account, passing in the transactionverifydata. Ensure that, withinMAXVERIFYGASgas usage, the method either returns true or else calls aPAYGASopcode, and that it does not read or modify state that is not Protected (it may only read its own state, not state from any other account). If the account balance is insufficient to cover the gas spent whenPAYGASis called, the transaction is invalidated. - if
verify()ultimately returns true, the transaction is verified, and if it returns false, the transaction is invalidated.
- ensure that the transaction is syntactically valid and that all required fields (
- after verification, for transactions with
calldata, thedestinationsmart contract is called withcalldata(as in Ethereum today). Note thatdestinationmay or may not be be the same asprincipal.
Prior art
Proposed implementation
- modify transaction and account data model along the lines described above
- add the required opcodes (
PAYGAS) to SVM - modify SVM to pass
verifydatato a Principal account'sverify()method and enforce the constraints described above - write a generic, efficient, sanctioned Principal smart contract "Simple wallet" template that can be used to mimic the behavior of an EOA (one keypair, one signer, sequential integer nonce, no logic other than transaction verification). The smart contract should run in SVM, but may offload heavy logic, such as signature verification, to the native client code as a "precompile." This template should be deployed at genesis or shortly thereafter, and will be the only available, sanctioned wallet template in the beginning.
Transaction format
The specifics of transaction format are outside the scope of this proposal (see #23, #37). However, Account Unification requires that transactions contain at least the following fields:
principal(address) // pays the gas fees and source of funds forvalue, must not be nil (except for self-funded spawn)destination(Address) // receiver of funds and destination smart contract for method calls (withcalldata)amount(Amount) // coin value to be transferred todestinationcalldata(binary) // optional - smart contract method execution call data: method selector, call valuesverifydata(binary) // passed toverify(), must not be nil. Typically contains a signature and a nonce.
State management and transaction ordering
An important invariant in both Spacemesh 0.1 and Ethereum is that no transaction that originates from sender A can invalidate a transaction from sender B (where A != B). This is due to the fact that a transaction can only be invalidated if the sender account balance is too low to pay the gas, or if the sender nonce doesn't match, and an account's balance can only decrease, and its nonce can only change, as a result of a transaction from the same account. Without this invariant, mempool management and transaction selection become much harder because transactions are more order-dependent: processing any transaction from any sender could invalidate any other transaction from any sender.
If we allow verify() to implement arbitrary logic, maintaining this invariant becomes much harder. We propose to start with restrictions on verify() in an initial phase and relax them somewhat in a later phase, pending further research.
Phase I: Stateless verify
In the first phase, verify() must be totally stateless. It may only read immutable state such as storage items set when the principal contract was initially deployed. It must not read state from any other contract (since the existence or non-existence of such state is possibly mutable).
Certain opcodes that read external state, such as BLOCKHASH, COINBASE, TIMESTAMP, NUMBER, DIFFICULTY, GASLIMIT, BALANCE, and those that perform external call/create/state access (CALL , CALLCODE, STATICCALL, CREATE, CREATE2, EXTCODESIZE, EXTCODEHASH, EXTCODECOPY, DELEGATECALL) are also disallowed.
Phase II: Limited access to state
Pending further research, in future it should be possible to relax the restrictions on verify() somewhat, along the following lines:
Level one vs. level two state
In order to verify a transaction with a given principal, a node must already read the principal account data from the trie, including its code and balance. If we think of this account data as occupying a single "page" of memory, for some fixed page size, then the additional work associated with reading a small amount of "level one" storage is negligible. Intuitively we can think of this "level one" storage as consisting of a small number of 256-bit storage values: enough to store a few signatures (for a multisig), a nonce, and possibly a per-signature daily spending key.
Under this scheme, verify() may read and write this level one state for its own account but may not read or write any other state.
Protected state
Under this scheme, the account storage of a Principal account is divided into two sections: Protected and Unprotected. Unprotected state may be read and modified by any method other than verify(). Protected state may only be modified by the verify() method, and the verify() method may only read Protected state. (Any method may read, but not write, Protected state.)
Note that we may decide to combine level one state and protected state.
Slow reads and writes
We introduce new opcodes for "slow" read and write of account storage. A slow write only takes effect at the next state checkpoint: typically, the following layer. A slow read returns the value as of the last checkpoint: typically, as of the previous layer. Under this scheme, verify() may perform slow reads and writes only, since they can never invalidate other transactions before the next checkpoint.
Questions
- How do we enable multi-tenancy: where many agents can transact using the same Principal account? The challenge here is that we can no longer use sequential nonces since, if nonces need to be sequential, then one agent's transaction may invalidate those of many other agents. Without sequential nonces, how do we allow transaction batching, replace by fee, or child pays for parent (CPFP)?
- Does the proposed "parked" or "lower bound" conservative balance design proposed by research and used in transaction selection, and needed to prevent conflicting transaction spam, still work in this model? In particular, is it compatible with a non-stateless
verify()method that can read and write state, even state that's protected? (Cheaply calculating conservative balances requires being able to verify transactions in a fashion that is not order-dependent. Any version of AU that is not purely stateless results in transactions whose validity is, at least partially, order-dependent. Sequential nonces help somewhat but not entirely since it's trivial to compute a "matrix" of multiple transactions per nonce such that pathfinding through that matrix is computationally infeasible.) - Do we want to allow an account to pre-fund a deploy transaction as well, before the account code has been deployed? We could add an exception case for this (at the cost of some simplicity). How to handle self-funded spawn of a wallet account? We need this in order to, e.g., allow users to spend rewards they've accumulated?
A: yes, see the description of self-funded spawn, above
- What should
MAXVERIFYGASbe set to? - How can this model be made compatible with our "ED++" signature scheme (where the
principalof a transaction is implicit via signature extraction rather than explicit)? - How expensive is it to run
verify()inside the VM to verify transactions rather than doing so entirely in native code? Note that the signature check operation itself can still be implemented natively (and called from inside the VM as a "precompile" host function). The cost associated with doing so involves context switching into and out of the VM. - As a concrete case study, how can our vault design, especially features that rely on state such as daily per-user spending limits, work with this design?
- What's the signature of
verify()? In particular, can it return or pass data into the "main" contract code to, e.g., indicate which signatures were verified in a multisig/vault? - How are account addresses determined? Do we want two methods like Ethereum's
CREATEandCREATE2?
A: we want both. CREATE will set the account address based solely on spawning transaction principal account and nonce. CREATE2 will instead use principal account, salt, and init code (see EIP-1014). This is orthogonal to Account Unification.
Dependencies and interactions
- SVM
- state processing
- account, transaction data models
- mining, transaction selection and validation
Implementation plan
- benchmarking and performance testing
- rewrite the account model along the lines described
- rewrite native transaction verification as described
- introduce the necessary SVM opcodes as described
- update SVM to handle
verifydata, find and runverify(), and disallow reading/writing of dynamic state as described - write the basic, default single-tenant principal wallet smart contract
- update the vault contract design to work with AU
Stakeholders and reviewers
Testing and performance
tbd