EVM Object Format (EOF)

Yes! Let’s do PAY! (and/or EIP-7609)

1 Like

(removed, meant to reply to the whole thread as opposed to any specific comment)

I have been providing feedback to the ipsilon team over the last year and have helped to draft / improve some of the EOF proposals. I am in general in favor of any changes to the EVM which improve execution efficiency, provide features for compilers and make it more future proof. That being said, I think there are two areas in the design (current: eof/spec/eof.md at 890ef566b86e8f25215f5cad65e171e2c1de71d4 · ipsilon/eof · GitHub) which I think could benefit from more work before mainlining it.

  1. VLQ. Using variable length quantities instead of fixed-size quantities, both in headers and as instruction immediates. This is a nice-to-have, but I think it will save significant pain if any limits (e.g. codesize limits) need to be upgraded in the future, since they can be implemented in a “soft” limits change, rather than needing a format change. It also can provide some codesize savings (I estimate 2-5% improvement), as well as potentially save opcodes (e.g., SWAP_N vs EXCHANGE) since we don’t need to optimize opcodes for different immediates sizes anymore. I wrote about this more here: update EOF container specs with variable-length encodings by charles-cooper · Pull Request #37 · ipsilon/eof · GitHub. Again, I think it could be ok if EOF shipped without VLQs since it can theoretically be changed later, but the cost of changing this later after mainlining would be high since it would require a format change. Therefore I think the benefit to changing it now (which, per feedback on the above PR, the only downside of which is needing to redo some testing work) outweighs the cost.
  2. Global code section. Right now EOF provides subroutine functionality mainly through code sections. However, to my knowledge an EIP-2315 style design which has a single global code section (effectively allowing cross jumping between routines, which EOF does not allow) was not seriously considered. The benefit of a global code section is mainly about efficiency of compiler-generated code; there are, by definition, some code-sharing optimizations available to a global code section which are not available to the nonglobal code section design. This is a deeper and more permanent(!) design issue that I’m not sure it would be possible to easily fix in a future hard fork. It may be that the benefit of disallowing cross-jumping outweighs the cost to compilers / code generators, at this stage I’m not sure, in any case I don’t think the analysis has been done yet. But in my mind this is a serious issue that should at least be investigated and addressed in the rationale section of the EOF proposals.
1 Like

I agree technically on VLQs – you get better code size now and you future-proof a system that in principle will run forever. I can’t speak to schedule issues, but I imagine they are tight. If it helps – and I’ve forgotten the details of the VLQ scheme you are using and of the EOF immediate size issues – but some schemes would let you specify a fixed-size field with unused bits now which could become variable length later when those bits start getting used.

If someone wants to pick up the 2315 ideas in the future I don’t think that EOF forecloses them. Somewhere there is a old PR of mine proposing that EOF code sections not be functions, but modules with multiple EOF-defined entry points and no cross-module jumps, but with EOF-2315 supported inside of modules. We never discussed it much, but something along those lines should still be doable.

EIP-2315 wasn’t seriously considered in part because it I never seriously proposed it again. It’s last-minute rejection simply set my research back too far, and I became content to join a startup doing other VM work and in my spare time get the proposal to where I wanted it and make sure that EOF functions provide at least the same level of init-time validation, which earlier versions did not.

1 Like

There are popular ERCs such as ERC-721 (NFTs) that rely on checking extcodesize(addr) > 0. It would not be possible to implement them as EOF contracts.

Has this been discussed before?

2 Likes

If the intent is to differentiate EOAs from Contracts this runs headlong into the AA and Smart Contract wallets also being smart contracts. While it used to be a valuable check current user practice has significantly curtailed it’s value, as an NFT collection that only allows EOAs to own the NFT will be chasing a shrinking market share.

So it has been discussed, and the conclusion was that the AA roadmap removes the value of that check.

This is not what the check is used for in these ERCs (see ERC-721, ERC-1155).

The logic they specify is roughly this: when an asset is transferred to a recipient that has code, invoke recipient.onReceived(...), which must succeed and return a specific magic value, otherwise the transfer is reverted.

The purpose is not to prevent smart contracts from owning the asset, but to prevent errors where the asset is transferred to the wrong contract with no way to recover it. This is an error that happens a lot, so many developers consider this an important feature. Smart contract wallets are supposed to implement onReceived (and they do, see for example Safe).

It’s also used to allow a recipient contract to react to a transfer and do something with the asset as soon as it’s received.

We can debate whether these are good design patterns or not, but I think they are at least legitimate and don’t conflict with the AA roadmap (unlike patterns that force the use of EOAs). The fact is that what is arguably the second most important ERC used on the network relies on this ability and the specification can’t be changed.

So are we okay with ERC-721 being unimplementable on EOF(v1)? My assumption is that we want EOF to become the standard way to deploy new contracts, if so this should be addressed.

It would be enough to add an opcode to check if an account has code set (or to change the semantics of EXTCODESIZE on EOF to return 0/1, but I assume that’s not a good idea). Granular code introspection is not necessary.

2 Likes

I think this may be the use case we were struggling to find when we were discussing it.

One option is to change the error numbers to distinguish a return from a contract from a return to a non-contract address (empty or EOA).

i.e. instead of

0 - success
1 - revert
2 - failure

we could do

0 - successful call
1 - successful non-call
254 - failure (-2 uint 8)
255 - revert (-1 uint 8)

So we can determine from the call what happened. The downside is it would eliminate the (ISZERO RJUMPI[n]) check and require a couple of exra opcodes per call (PUSH1[127] LT RJUMPI[n]) but allow an executed onReceived to be distinguished from a transfer or no-op call. Although in cases where you only want to accept successful and want to hot-path that handling ISZERO still works

Other alternatives that were rejected

  • Use only PAY to do transfers, this prevents some calls from requiring payment, such as WETH
  • Require Value when calling a non-contract and if not failure, it felt too special cased.

I’ll bring this up in the EOF call this wendsday, as I think it is the example we were missing. Solidity had brought it up but the collective seeme persuaded by the AA argument.

1 Like

Yes this would work!

Yeah this is unfortunate, I think it makes sense to prioritize keeping that code sequence short. Another alternative could be to switch back to returning 0/1 (0: success), and instead of returning the status code immediately from the *CALL instructions add a separate opcode CALLSTATUS to put it on the stack if necessary. I assume it will very rarely be used anyway?

As a side note, in EIP-7069 it’s really not clear to me when the failure status 2 will be returned on the stack. I can’t find a definition of “failure”. This part in particular confuses me: “9. Fail with status code 1 returned on stack if any of the following is true […]”, is that correct or is it an error and it should return status 2?


Regarding another point:

EOF1 contracts can only DELEGATECALL EOF1 contracts

Is this restriction still necessary given that SELFDESTRUCT has been mostly neutralized in EIP-6780: SELFDESTRUCT only in same transaction?

There is a potentially severe failure mode for EOF upgradeable proxies: if the proxy target is upgraded from an EOF to a legacy contract, and the upgrade logic is located in the target, the proxy becomes frozen and unusable. This error could be prevented by having the upgrade code always test that DELEGATECALL works, but this seems like a footgun that should be avoided from the outset.

The rationale proposes this restriction as a way to prevent attacks/errors like the Parity Multisig, but actually introduces a new way in which a proxy contract could end up irrecoverably broken!

1 Like

From this reference, extcodesize of EOF code called from legacy code should return 2. I think that suffices, no?

Given EXTDELEGATECALL’s restrictions. I’ve also thought of is how EOF contracts can recognize other EOF contracts to know if EXTDELEGATECALL is okay to make without wasting initial gas

So currently, compilers add a check of extcodesize before making calls since calls to EOAs return 1. From this reference EXTDELEGATECALL to legacy code also behaves as though its an EOA. This means that EOF code would need to be able to detect other EOF code. Doing extcodesize won’t work as legacy code has size too.

The only solution i could think of was EXTCODECOPY the first 2 bytes and asserting that it is 0xEF00. That’s means a cost of a few more opcodes during EXTDELEGATECALL for EOF

The two issues I’ve described are about EOF contracts, where EXTCODESIZE is not available, so neither of those are relevant.

1 Like

Yeah you’re right, missed that part (extcodesize etc deprecated)

Also highly interested to know this. Since this has a huge impact on interoperability and de facto splits EVM “lego” to 2 worlds.

1 Like

I don’t think the lack of extcodesize check will affect these EIPs. Technically, yes, they both state that a correct ‘safe transfer’ function implementation must check whether the target is a contract and not call it if it is not. This check will indeed be impossible on EOF. But practically speaking, this check is redundant. Proceeding with a call in case of EOA will simply result in a success with no return data. As long as the transfer function does check the return value, this will still result in a revert. And specifically in Solidity, unless intentionally implemented as a low-level call, the compiler itself will issue a revert when the size of returned data does not match what the called function declares.

As much as I think that having a way to perform this check would still be useful as an extra sanity check, it does not seem like not having it would prevent EIPs like this from being implemented. At most, the wording of those EIPs may need to be revised to stop requiring this check on EOF (or in general).

But in the case of an EOA recipient it should not!

The extcodesize check is not redundant, it’s necessary to implement the semantics.

2 Likes

Ah, you’re totally right. I misread the purpose of the check in these EIPs. It’s there to skip the onReceived() calls without reverting. Without it the EIPs will still work for contracts but not for EOAs.

So yeah, this does seem like a new use case for the check. I still think it does not make the EIPs unimplementable though, it just neuters some of their safeguards, which previously wasn’t considered a strong enough reason for an additional call return code.

The way you’d implement it without the check would be for ‘safe transfer’ to always issue a call and check whether it’s an EOA based on the status and return data. If the call is successful and the return data is empty, it’s probably an EOA. Of course this will have some false positives, e.g. if it just happens to be a contract has an onERC1155Received() function that returns nothing. Or if the contract intercepts the call in its fallback function.

I don’t think this is qualitatively different from the case of the extcodesize check on high-level calls in Solidity that we brought up earlier. Still, if this additional case is enough to sway the decision towards making this case properly distinguishable, I’m all for it.

EOF Call 52 notes

  • Clients
    • Besu and EVMONE are adapting to recent spec changes
    • Nethermind and Geth are progressing with their implementations, currently working on state test issues
    • EELS has started an implementation
    • No updates from Reth or EthJS
  • Compilers
    • No updates from Solidity or Vyper
  • Spec Updates
    • Discussed the need to differentiate EOAs from contract accounts, for ERC-721’s safe transafer from method
      • Two major options: return codes (as outlined above) and reworking EXTCODESIZE
        • Return codes could be tweaked to set high bits for flags. bit 7 - failure, bit 6 - was an EOA bits 0-3 status.
          However, this could impose overhead for contracts that don’t care about EOA status
          Ipsilon will ping @rodiazet to see if we can quantify the overhead we would see out of solidity
        • EXTCODESIZE could be recast like DIFFICULTY/RANDOAO was recast.
          A new IS_CONTRACT opcode would overlay it, returns 0 if it has no code and 1 if it does, regardless of size. Compiler logic that only tests codesize > 0 would still work.
          The downside is this is an infringement of banning code introspection. While not a bad one as it is a simple question of “do you execute code”
    • Discussed changing max_stack_height in the types section in light of the requirements for input_size already existing, so that a double counting can be removed from CALLF checks.
      There were concerns about a non-essential change going in this late, however @chfast was not on the call it was decided to push it out two weeks when he can be present to advocate for his change
    • Banning EOFCREATE and RETURNCONTRACT referencing the same container was finalized
  • Testing updates
    • We discussed the issues around system contract actions such as beacon block, block hash, and withdrawals/deposits/consolidations.
      A new field will be added to the “env” section of t8n requests indicating if those block level action should or should not be performed. This will allow state tests to evaluate just the TX and not the whole block.
    • We also discussed differentiating initcode and runtime code in EOF container validation tests. a new field will be added allowing test consumers to differentiate. PRs will be forthcoming.
  • Devnet
    • Because we do not have multiple clients ready we will not be launching a devnet in June. Target is now July.
    • Kurtosis is ready to support EOF networks. Assertor tests need to be written to deomnstrate basic readiness for EOF create transactions.

Next call is in 2 weeks, on 10 July. Danno and Piotr will be absent.

2 Likes

The max_stack_height change is rather cleanup but it also leads to bigger change removing the need for runtime access to the type section (please discuss the details there). In my opinion this is worth the effort especially for the code chunking.

This is late, but I’d like to answer the VLQ question in the context of instructions. For EOF header this is probably fine, but I’m not sure how much it matters there.

For instructions VLQ doesn’t look good. EOF makes bytecode translation much easier (JIT/AOT compilers) but also improves situation for interpreters by a lot by removing jumpdest analysis. I.e. interpreters can start executing just after reading the EOF header.

Using VLQ to encode instruction immediates makes the interpretation more complex because for some instruction VLQ decoding is involved and it is rather impractical to cache the results or preprocess the code. For some instructions like RJUMPV VLQ cannot be used normally because random access to the table is needed so we need a workaround in this case.