EIP-3074: AUTH and AUTHCALL opcodes

Creates a new precompile, analogous to CALL, that sets CALLER based on an ECDSA signature.

This EIP creates two precompiles:

  • CALL_PRECOMPILE - forwards a CALL, setting msg.sender according to an ECDSA signature, using a relayer-sponsee nonce for replay protection.
  • NONCE_PRECOMPILE - provides access to relayer-sponsee nonces expected by CALL_PRECOMPILE.
7 Likes

The proposal has several problems, the two most important are:

  • It’s incompatible with EIP-712 and the mapping selected could be used to confuse existing wallets into signing an unintended message.

  • It doesn’t include the chainID in the signed fields , as advertised. Therefore it’s open to network reply attacks.

This is the list of fields signed, and how they map to a normal Ethereum transaction:
Normal -> EIP-3074
Nonce -> Nonce
GasPrice -> GasLimit
GasLimit -> ReceiveAddress
ReceiveAddress -> Value
Value -> Data
Data -> relayer
chainId (not present)
empty (not present)
empty (not present)

2 Likes

Thanks for the feedback! I updated the signed fields to include chainid.

Is EIP-712 final yet? I wanted to avoid it until it’s finalized.

If a malicious website wanted to confuse a user, they’d have to present something like this:

Nonce: 3
GasLimit: 66000000000 (66 Gwei, current gas price)
ReceiveAddress: 0x0000000000000000000000000000000000Be8c72 (current block gas limit)
Value: 611382286831621467233887798921.843936019654057231 ETH (some address)
...

I’ve probably missed some of the specifics of the RLP offsets, but it seems like it would be a very suspicious transaction. I’m not opposed to modifying the format to make it impossible though, if you have any suggestions.

2 Likes

Recommend having the transaction be a 2718 typed transaction, which means grabbing a number and signing over secp256k1(keccak256(TransactionType || rlp([nonce, gaslimit, to, value, data, invoker, chainid]))). This would make it so we don’t have to worry about having the signature collide with some other future transaction type.

This wouldn’t technically be a 2718 transaction because it has no receipt and doesn’t end up in a block header, but we should reserve the transaction type number and follow the signing rules.

2 Likes

Other changes I would like to see:

  1. Signature should be yParity, r, s. v shouldn’t really be used anymore.
  2. Signature first. This enables some minor optimizations and comes at no cost.
  3. data last. This is pretty minor, but having the variable length thing at the end makes some stuff a tad easier and this change comes with no cost.
  4. Remove chainId. This can be addressed by the invoker with CREATE2 using CHAIN_ID as a salt. In the case of a fork (chain ID change), the invoker can be redeployed. This can be automated with a factory. By not having a chain ID, transactions and invokers can be constructed that intentionally support replays across chains.
  5. Remove nonce and nonce checking and only include 32 bytes of extra data in the signature. The invoker can then do nonce management (or not!).
  6. Use ABI Encoding or SSZ instead of RLP encoding. We don’t need the space savings provided by RLP, and ABI encoding and SSZ are a bit easier to work with when you have fixed width data elements followed by a single dynamic length data element.
  7. Rename gaslimit to mingas. This value doesn’t represent a gas limit, but instead represents the minimum amount of gas that needs to be supplied.

Being able to replay a transaction-like package across multiple chains is a bit of a niche feature. How often do you think a user would want to do that? I think the common case (one chain per transaction) is good enough, and matches what users will want to do.

Safely implementing the one-chain-per-transaction rule in the invoker would require either stuffing the chainID into nextra, or checking that the chainID hasn’t changed before every sponsored call.

1 Like

I expanded the section on why I don’t believe using the revert reason to indicate precondition failures is a good idea.

I’ve also reordered the signature input, so hopefully that takes care of everything!

Still outstanding (ignoring the things we disagree on):

  • yParity instead of v (which aligns with 2930 and 1559 transactions)
  • data as the last field in the signed data. It is the only variable length item, and having it last has potential to simplify things at essentially no extra cost other than moving it in the spec.

New comments:

  • You pass value twice, once as an actual attached ETH amount and again in the data blob. This is unnecessary given they must be equal.
  • I feel like we should be putting everything on the stack, rather than a mix of stack and encoded data:
    IMPERSONATECALL gas, value, y_parity, r, s, sponsee, type, nextra, to, mingas, data_start, data_length
    
  • Add rationale as to why STATICCALL context is allowed at all. I can’t think of a situation where this would be useful in a static context.

Is this just renaming v to yParity, or is it also removing chain id?

I’m inclined to leave it in, even if I can’t think of a good reason for it. After all, you can CALL inside of a static context.

It removes the chainId and the +27. From 2930:

YParity The parity (0 for even, 1 for odd) of the y-value of a secp256k1 signature.

Also, so we have it documented, I recommend renaming TXCALL to IMPERSONATECALL to make it more clear what this version of call actually does.

While I am opposed to removing chainid, I am not opposed to encoding it separately. Thought?

IMPERSONATECALL is used by the draft EIP-2997. I’m down with renaming the opcode, but I’m not sure which name I prefer:

  • IMPERSONATECALL - decent name, and I guess you can impersonate yourself if you want?
  • CALLAS - breaks the pattern of *CALL, but it’s short and descriptive.
  • SIGNEDCALL - Less descriptive.
  • SIGNCALL - seems backwards, since you aren’t signing a call, you’re making a call with a signed package, plus it’s also not descriptive.
  • TXCALL - call with transaction-like package

What do you mean by “encoding it separately”?

Instead of packing yParity and chainid into v, having separate fields for yParity and chainid

2 Likes

I created a (relatively) simple example for what an invoker contract for EIP-3074 could look like:

We may be saying the same thing, but I want to verify:

chain_id would be part of the signed data, but would not be part of the data passed to the opcode. When validating the signature, it would use whatever chain_id the EVM has internally for the chain it is executing on. The signature would just be y_parity, r, s.

I have several requests / questions concerning this EIP:

  • Can you please extend the description of the semantics of the opcode (it mostly focuses on the arguments and the return values)?
  • Why is so much data put into the arguments instead of using stack slots? I would assume almost everything to be taken from the stack apart from the actual payload.
  • EDIT: It does exactly that, sorry for the confusion! PREVIOUS TEXT: Maybe I didn’t get it but what is the idea behind setting tx.origin / caller instead of msg.sender / sender? Until now, the distinguishing feature of tx.origin is that this is the account that pays for the gas. This property would be destroyed. Also if this is kept as it is, please mention that you ALSO set msg.sender because that is probably also the idea, isn’t it?
  • It would be nice to improve the presentation a bit. It is a bit hard to keep track of 5 different addresses plus other data - is that really needed?
  • There are many more points where this specification is not really clear. For example, the behaviour of the function abi.encode strongly depends on the types of its arguments, but those are not specified.

Since this is not really about a transaction (for me, transactions are objects that are stored in blocks), I would prefer a name similar to impersonatecall or substitutecall.

Any thoughts on CALLFROM / CALLAS? They would break the *CALL scheme, but are short and intuitive. Alternatively, SIGNEDCALL / SIGCALL?