"Rich transactions" via EVM bytecode execution from externally owned accounts

I’ve written up an EIP draft proposing a way for externally owned accounts to execute per-transaction bytecode, here. Feedback appreciated!

10 Likes

So, SELFDESTRUCT could be run multiple times for a given EOA?

While not critical, it may be valuable to mention how DELEGATECALL works. It should be obvious to everyone, but I generally like how you explicitly called out most opcodes even when they should have been obvious.

Can you add a line or two on the gas cost of the SELFDESTRUCT opcode? The gas refund would be inappropriate (frankly dangerous), since there is no matching CREATE.

What is the motivation for this?

A call to the precompile address from a contract has no special effect and is equivalent to a call to a nonexistent precompile or an empty address.

From the cheap seats, it seems like it would be easier (implementation wise) to have a contract calling the precompile behave the same as an EOA calling the precompile, where it essentially is a delegate call to the code supplied in the CALLDATA. This way clients don’t have to switch on caller.

Any value sent in the transaction is transferred to the precompile address before execution, and is thus inaccessible.

This seems like it would be prone to error (foot gun). Why not just have any value sent be a no-op? How does DELEGATECALL work with value currently?

A new reserved address is specified at x , in the range used for precompiles. When a transaction is sent to this address from an externally owned account, the payload of the transaction is treated as EVM bytecode, and executed with the signer of the transaction as the current account.

Wouldn’t it be cleaner to have this as new kind of signed transaction message instead of as a somewhat odd precompile? It would add functionality at a place where we don’t usually do it, but it feels more natural.

This breaks security assumptions in things like transferAndCall

Almost certainly, but no one so far has been willing to put the time into drafting a transaction versioning EIP, which requires a pretty strong understanding of the P2P protocol and has an impact on many layers of the system (all of which currently make assumptions about there being exactly one transaction format).

If you have the time, you should definitely talk to @AlexeyAkhunov about his ideas for transaction versioning system, and additional transaction types. Solving this problem would open the doors to all sorts of goodness including this, the ability to have transactions which bundle multiple other transactions, the ability to have the gas payer be different from the signer, etc.

1 Like

Can you provide a bit of detail on the pattern and why it is broken by this? isHuman checks definitely break, but I think the consensus of the dev community is that isHuman checks are already broken and they shouldn’t be used in the first place.

1 Like

Yes. It can already be run multiple times on a contract account, thanks to CREATE2.

Done!

Done!

Would it be simpler for implementers if I specified that SELFDESTRUCT reverts, instead?

Recmo points out why this would be dangerous. It allows you to force any contract that is willing to make arbitrary calls for you (presently relatively harmless in many situations) to execute arbitrary bytecode in its context. Even if the contract only calls a specified function at an address you provide, we’d have to vet the meaning of every 4-byte function signature as EVM bytecode to be sure they’re safe!

DELEGATECALL doesn’t have a value parameter. I specified it this way because that’s how CALLs with value to all other accounts behave, and I wanted to avoid adding more special cases.

I was wondering about that too. One option, for instance, would be to allow transactions with 21-byte to fields, where the first byte is interpreted as a call type. We’d define one new call type, which effectively DELEGATECALLs the target address instead of CALLing it.

There are pros and cons. It would reduce some of the special-casing required in this proposal, but you’d still have to special case some things, such as SELFDESTRUCT. It would reduce transaction size for commonly executed operations, but make doing ad-hoc operations harder.

It would also likely confuse a lot of tools that rely on being able to parse transaction objects, which is more concerning to me.

2 Likes

SLOAD and SSTORE operate on the storage of the EOA. As a result, an EOA can have data in storage, that persists between transactions.

What’s the rationale for this? I can think of some fun use cases, but wondering what you were thinking of. Also, what kind of can of worms can this open?

1 Like

It seemed like prohibiting would be adding unnecessary special-cases.

1 Like

What happens if a contract called by the EOA then calls back the EOA. Will the code sent in the transaction data field still be there or will the code be empty (as in a contract initialization) ?

2 Likes

This seems like it would be very useful. @Arachnid are you providing the implementation of this?

The EOA doesn’t have code; I tried to make that clear by specifying what EXTCODE* etc return - I’m open to suggestions on how to clarify further.

I’m not certain I’ll have time to. If there’s interest in including this in a fork, I can probably put together a geth implementation, though.

1 Like

I wonder if this opens a new attack vector against users, or negatively affect UX because the code is sent every time and there’s no immutable code deployed which can be checked/audited by everyone. Do I have to read the code every time I use a wallet website? Why not go the extra mile of assuming EoAs can have code (still different from contracts in that they can sign txes)?

[…] SLOAD SSTORE
It seemed like prohibiting would be adding unnecessary special-cases.

Leaving it in requires the special treatment of SELFDESTRUCT and maintaining state for EOA accounts. Additionally, state on EOA may complicate future state rent proposals. I’d consider these also special-cases. So something inelegant will happen either way.

It’s worth considering a proposal where SLOAD, SSTORE and SELFDESTRUCT become INVALID/DONTUSE. I don’t think this necessarily more complicated than the current proposal. (In fact, I’d argue that it’s simpler)

So let’s think about what the potential usescase of EOA state could be, to see if there is a good reason to keep it.

First, State would only be used to communicate between two EOA transactions. Inside a single tx it can just use memory.

  • This can be useful if there are multiple signers with no other means to communicate, but if you share a private key and nonce counter, we can assume you already have an offchain communication channel.
  • It can also be useful when some data is not available at the time of signing, but will be when a previous transaction finishes. I.e. a transaction does something, and a second transaction that depends on the outcome. But the whole point of this proposal is that we can merge those kinds of transactions into one.

I’m struggling to come up with a usecase for state in EOA accounts, and think it would be cleaner to not have it.

Also note that if we mark those opcodes INVALID/DONTUSE, then we can always add EOA state in a separate future proposal in a backwards compatible manner.

Finally, someone who needs EOA in a one-of case can deploy a contract that only listens to the EOA address and stores state on its behalf.

Actually, I think the current precompile address is a very good solution. Currently we have (AFAIK) two transaction types:

  • Contract calls, where to contains the contract to call and ether, gas and calldata is provided. (Plain EOA ETH transfers are a special case of this).
  • Contract deployments, where to is the special flag value 0x00 and calldata contains a constructor, to be executed in the context of the newly created account, and returns the code that should be stored there.

What we want is similar to the second, except calldata is now executed in the context of the EOA account, and we don’t store any code at the end.

So there’s already a precedent for using flag-values in to. It seems natural to add one more.

My questions stems from the observation that this new transaction type is so powerful that it can replace the other two (first one would become a CALL, the second a CREATE). And when you implement them this way, you no longer need a to field in the transaction, so you couldpropse a new transaction message that does not include a to field and implements this proposal, which can then replace all existing transaction types.

This is a drastic change in the tx format though, and before this proposal it didn’t occur to me that it could also be done using a precompile-address + flag value. This is a great insight that allows ‘rich transactions’ to be implemented separately from a transaction format refactor!

How does this differ from a contract creation transaction that doesn’t return a contract? Init code still gets called and the empty account does not get generated.

This is a pattern I already used in the Ethereum Reference Tests - https://github.com/ethereum/tests/blob/develop/src/GeneralStateTestsFiller/stSStoreTest/sstore_gasLeftFiller.json#L75