Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Account abstraction for main chain #859

Open
vbuterin opened this Issue Jan 29, 2018 · 8 comments

Comments

Projects
None yet
7 participants
@vbuterin
Copy link
Collaborator

vbuterin commented Jan 29, 2018

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:

  1. Set exec_gas = start_gas - intrinsic_gas, where intrinsic_gas is 21000 + 4 gas per zero byte in data+code + 68 gas per nonzero byte in data+code.
  2. If the account code of the target is empty, then attempt to create the account with the specified code. Specifically, follow these steps:
    • Assert salt is nonempty
    • Assert sha3(salt++ code)[12:] == target
    • 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 (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.
  3. Assert that the transaction nonce matches the target account nonce. Increment the target account nonce by 1.
  4. Process a message with sender ENTRY_POINT, to address target, value 0, gasprice 0, gas exec_gas.
  5. Assert that the execution either succeeded, or PAYGAS_CALLED = True (see below). Refund unpaid gas to the target (ie. set target.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:

  1. Paying for gas.
  2. 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:

  1. Check that PAYGAS_CALLED = False; if not, then simply pop the top element off the stack, and stop and push 0 onto the stack.
  2. Subtract gasprice * tx.start_gas from the callee account's balance. If not enough funds, stop and push 0 onto the stack
  3. 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:

  1. Check the signature
  2. Call PAYGAS
  3. 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
@sohkai

This comment has been minimized.

Copy link

sohkai commented Feb 4, 2018

Refund unpaid gas to the target (ie. set target.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?

@vbuterin

This comment has been minimized.

Copy link
Collaborator Author

vbuterin commented Feb 6, 2018

Good point! Added that option to the specification.

@holiman

This comment has been minimized.

Copy link
Contributor

holiman commented Feb 6, 2018

My notes after reading through this, and asking @vbuterin about some points.

Set exec_gas = start_gas - intrinsic_gas, where intrinsic_gas is 21000 + 4 gas per zero byte in data+code + 68 gas per nonzero byte in data+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 an empty 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.

  1. 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:
    1. If code|salt are non-empty -> tx is invalid
    2. if code|salt are non-empty -> just ignore code and salt

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 throws after PAYGAS, what happens? Two options

  • The state is rolled back to the point where PAYGAS was invoked, and exits with 0.
  • The stateis rolled back to start of tx, and the full start_gas is paid by the target. This options requires checking that the target 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.

@zmanian

This comment has been minimized.

Copy link

zmanian commented Feb 6, 2018

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?

@vbuterin

This comment has been minimized.

Copy link
Collaborator Author

vbuterin commented Feb 10, 2018

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.

@PeterBorah

This comment has been minimized.

Copy link

PeterBorah commented Feb 27, 2018

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. :)

@lrettig

This comment has been minimized.

Copy link
Member

lrettig commented Feb 28, 2018

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.

@cdetrio

This comment has been minimized.

Copy link
Member

cdetrio commented Jun 18, 2018

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.