Account abstraction for main chain
Closed this issue · 10 comments
The following is a copy of the account abstraction proposal discussed here but coalesced into one piece and adapted for the ethereum main chain by adding back in mandatory in-transaction nonces.
Specification
A new type of transaction is allowed, with the following format:
[
chain_id, # 1 on mainnet
target, # account the tx goes to
nonce, # transaction nonce for replay protection
data, # transaction data
start_gas, # starting gas
code, # initcode of the target (for account creation, most of the time should be empty)
salt, # contract creation salt (must be 0 or 32 bytes)
]
Executing a transaction of this format is done according to the following rules:
- Set
exec_gas = start_gas - intrinsic_gas
, whereintrinsic_gas
is 21000 + 4 gas per zero byte in data+code + 68 gas per nonzero byte in data+code. - If the account code of the
target
is empty, then attempt to create the account with the specifiedcode
. Specifically, follow these steps:- Assert
salt
is nonempty - Assert
sha3(salt++ code)[12:] == target
- Attempt to create a contract with to address
target
, init codecode
, sender ENTRY_POINT (ie. 0xffff...ff), value 0, gasremaining_gas - CONTRACT_CREATION_COST
, gasprice 0 (calldata equals init code, just like contract creations work now) - Assert that the contract creation succeeds.
- Decrease
exec_gas
to the amount of gas remaining after the contract creation finishes.
- Assert
- Assert that the transaction nonce matches the
target
account nonce. Increment thetarget
account nonce by 1. - Process a message with sender ENTRY_POINT, to address
target
, value 0, gasprice 0, gasexec_gas
. - Assert that the execution either succeeded, or
PAYGAS_CALLED = True
(see below). Refund unpaid gas to thetarget
(ie. settarget.balance += remaining_gas * PAYGAS_GASPRICE
).
If any of the asserts fails, the transaction is invalid. Note that this includes the assert in step 5; that is, if a top-level transaction execution fails, and PAYGAS_CALLED is False, then the entire transaction is invalid.
PAYGAS
The abstraction is simplified with a new PAYGAS opcode. PAYGAS simultaneously serves two purposes:
- Paying for gas.
- Serving as a logical demarcator of the "verification parts of a transaction" and the "execution parts of a transaction".
We add two variables to the execution context (ie. in a similar position as the selfdestructs list): PAYGAS_CALLED and PAYGAS_GASPRICE, initialized to False and 0 respectively. The PAYGAS opcode takes a single stack argument, gasprice
. Its logic is as follows:
- Check that PAYGAS_CALLED = False; if not, then simply pop the top element off the stack, and stop and push 0 onto the stack.
- Subtract
gasprice * tx.start_gas
from the callee account's balance. If not enough funds, stop and push 0 onto the stack - If steps (1) and (2) passed, set PAYGAS_CALLED = True, and PAYGAS_GASPRICE = gasprice, and push 1 onto the stack.
Account strategy
The owner of an account will generally want to have account code that looks something like:
- Check the signature
- Call PAYGAS
- Call the actual destination account
Where the transaction data encodes the signature as well as the destination, value, gasprice and data of the intended message that is to be sent from an account.
It will be possible to send ETH to not-yet-created accounts by simply computing their address from the hash of the init code, and initializing the code for such an account would be done at the same time as sending the first transaction.
Miner strategy
We note that any transaction whose execution reaches the PAYGAS opcode is guaranteed to pay for gas, even if the execution exits with an exception after that point, and any transaction that exits with an exception before reaching PAYGAS will not be includeable into a block.
Miners can use the following strategy to accept transactions. Every miner can set a private value, CHECK_LIMIT, eg. to 200000. When a miner or network node sees a transaction, they execute it on top of the current head state for a maximum of CHECK_LIMIT gas (the 200-per-byte cost of creating a new contract does NOT count toward the limit). If the transaction execution hits PAYGAS before this limit, then the miner or network node accepts the transaction and acts as though the gasprice called with PAYGAS is the transaction's gasprice; if it does not, then the miner or network node rejects the transaction.
When a miner actually includes transactions in a block, PAYGAS may pay a different gasprice than when the miner first saw it, for example if the argument to PAYGAS depends on state; in this case, throw out the transaction if the gasprice is lower than it was during the first scan.
Setting CHECK_LIMIT is a simple tradeoff: if CHECK_LIMIT is higher, miners can accept transactions from accounts that make more complex checks before calling PAYGAS (eg. Lamport sigs, threshold sigs), but setting CHECK_LIMIT higher also makes miners more vulnerable to DoS attacks. Miners may want to start off with high CHECK_LIMIT but dynamically adjust it downwards if they detect a DoS attack to keep CPU usage below some threshold.
Specification version 1.1
- Add a PAYGAS_CALLER parameter; if/when PAYGAS is called successfully, set the value to equal the address of the current executing account (ie.
msg.to
) - Refund the PAYGAS_CALLER instead of the transaction
target
What does this abstract and what does it not abstract?
- It will be possible for accounts to use whatever signature scheme they want
- Nonces will remain mandatory; this is a compromise to preserve the "each transaction can only appear once in the chain" invariant
- It will be possible for verification conditions to be more complex, saving gas in a variety of scenarios. One particularly interesting usecase is capped ICOs. For example, if there are 10000 transactions going into an ICO but the cap can only accept 2000, then currently all 10000 would get included with the last 8000 being no-ops, but with this scheme one could make a setup where the 8000 transactions that fail all cannot be included in the chain
- It will continue to be difficult to use ERC20s to pay for gas
Refund unpaid gas to the
target
(ie. settarget.balance += remaining_gas * PAYGAS_GASPRICE
).
What is the rationale behind "refunding" the gas to the target
rather than the callee? Since the callee's paying for the gas, shouldn't it also get the refund?
Good point! Added that option to the specification.
My notes after reading through this, and asking @vbuterin about some points.
Set
exec_gas = start_gas - intrinsic_gas
, whereintrinsic_gas
is 21000 + 4 gas per zero byte indata+code
+ 68 gas per nonzero byte indata+code
.
Note, this is the same as Gtxdatanonzero
and Gtxdatazero
from YP, no change.
Notable differences:
- Contract creation does not 'just happen' when
to
is missing. Instead, whenever sending tx to anempty
target, the contract creation process is started. - The new contract-creation is a two-step thing, where first there's a contract creation (load init-code, execute init-code), followed by a second
CALL
into the newly created contract. This is new, and possibly introduces new complexities in the execution/revertal process.
- Attempt to create a contract with to address target, init code code, sender ENTRY_POINT (ie. 0xffff...ff), value 0, gas remaining_gas - CONTRACT_CREATION_COST, gasprice 0
During this step, CALLDATALOAD
will return code
-- the initcode of the contract, and not the data
.
- Process a message with sender ENTRY_POINT, to address
target
, value 0, gasprice 0, gas exec_gas
This is the step where data
is actually used -- this step does not have access to the code
-part of the transaction: a CALLDATALOAD
will load data
into memory.
The differences here are important; since data
is not take part of the address calculation, whereas code
does. If a user wants to create a multisig, and set himself as owner
, the actual constructor argument would have to be part of code
(since there's no point in using ORIGIN
or CALLER
(since they're 0xFF..F
)). This means that the reorg-attack[1] is avoided.
- Q:If in step 2, the account code of
target
is non-empty, what happens?
- A: If code && salt are empty -> non-creating transaction to
target
. Otherwise, there are two options:- If code|salt are non-empty -> tx is
invalid
- if code|salt are non-empty -> just ignore
code
andsalt
- If code|salt are non-empty -> tx is
Note: Vitalik preferred option 2, reasoning there might be situations where a user submits multiple transactions, and it's difficult to know which will make it in first.
Clarification, if a transaction throw
s after PAYGAS
, what happens? Two options
- The
state
is rolled back to the point wherePAYGAS
was invoked, and exits with0
. - The
state
is rolled back to start of tx, and the fullstart_gas
is paid by thetarget
. This options requires checking that thetarget
holds sufficient funds.
[1]: Reorg-attack in brief: When a user creates a contract A (perhaps his own multisig-wallet), a reorg (perhaps created by a malicious contract), can create a replica contract at A. Any transactions the user then makes to the contract (e.g. sending money to it), will now go to a multisig-wallet created by another user.
Does this design imply that ethereum implementation must store and check for uniqueness against all published nonces for each transaction in an account for the entire existence of an account. This sounds inefficient. How about a counter instead of a nonce?
Does this design imply that ethereum implementation must store and check for uniqueness against all published nonces for each transaction in an account for the entire existence of an account.
No. This design still requires transactions with the same target account to have sequentially incrementing nonces. It's the sharding abstraction proposal that doesn't have that feature, and which doesn't even care (at least at protocol level) if the same transaction gets included in the chain multiple times.
It may be too late, but listening to the last core dev call it sounded like this isn't going to make it into Constantinople. This makes me sad, so I wanted to make an attempt to get people to reconsider.
As a dapp dev, this is probably the EIP I've been most excitedly waiting for, since it was EIP 101 in 2015. (The main competitor was EIP 211, which happily was released with Homestead.)
Requiring non-abstract accounts is an ugly hack that makes a lot of things I want to build hard or impossible. The most obvious is that projects I've worked on have often wanted to pay for their users' gas, so that users aren't required to purchase Ether on an exchange in order to use our application. Without account abstraction, you have to do a difficult and imperfect dance involving sending small amounts of gas to each user's account, reimbursing them over time, topping them up if transaction costs rise, etc.
A related annoyance is that it is harder to experiment with alternative ownership models, like native multisig wallets, zero-knowledge-proof-based contracts, ring signatures, etc. You can mostly do it, but you have to keep an account with a small (but not too small) amount of ether around, to actually send the transactions.
Finally, I've been planning a project that attempted to use account abstraction in conjunction with on-chain decentralized exchanges to pay for gas using ERC-20 tokens (I'm pretty sure you can do it in under 200k gas if you're careful), which I won't get to do until account abstraction is released.
I understand that development time is limited, but account abstraction has much more relevance to my job and my hobby projects than Casper does, so I thought I'd at least make the argument. :)
For the record, here are the notes from the discussion of this EIP in the last all core devs call. The sentiment seemed to be that 1. this is complex, 2. we don't have time to do this properly before Constantinople, and 3. this will happen along with sharding anyway.
Discussion around account abstraction for Metropolis (i.e. Byzantium) took place in #208.
After the Metropolis EIP was deferred, discussion continued in the first ethresear.ch thread on abstraction. More discussion followed in a second thread.
There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.
This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment.