EOF is extremely complex. It introduces two new contract creation semantics (EOFCREATE and TXCREATE), removes and adds over a dozen opcodes. Furthermore, the purported benefits don’t require EOF, they could be implemented in the current EVM in much less invasive ways. Let’s explore these:
Reducing compiler complexity:
EIP-663 opcodes allow access deeper in the data stack. This is useful, but not a requirement for modern compilers – all modern compilers know how to implement stack/register spilling. This is a shortcoming of solc, not of the underlying VM. (As an example, Vyper does not have stack-too-deep as a result of compiler and language design.)
Improving bytecode size due to JUMPDEST removal:
We could just remove JUMPDESTs.
Makes it easier to upgrade the EVM, e.g. adding or removing opcodes:
We can add validation rules to the existing EVM. Bytecodes of contracts can be analysed at creation, and we can disallow deploying contracts with specific opcodes - or change semantics thereof - if they are deployed after a given fork_blocknum.
Allows for opcodes with immediates:
Again, we don’t need to change semantics of existing contracts. Rather, validate and apply updated semantics to newly created contracts.
Remove gas introspection:
You can just add the EIP-7069 (CALL2, etc) opcodes, which don’t access gas.
Validate that code does not include the GAS opcode.
Because of the 63/64ths rule, contract behaviour depends on gasleft anyways (subcalls can OOG without the outer contract OOG).
Remove code introspection:
You can add validation rules to legacy EVM to remove the relevant opcodes. New creation opcodes would need to be introduced, but it wouldn’t need to be shipped at the same time as all these other changes.
Remove dynamic jumps:
This can also be addressed simply by updating pricing and providing a carveout for PUSH2JUMP(I) in the gas schedule.
Subroutines:
There are existing, less invasive proposals to the EVM which include subroutines and give preference to static jumps.
Address space expansion:
Can change semantics of existing opcodes (like BALANCE) to not zero the top 12 bytes of the address, again via contract versioning.
Remove codesize limits:
We can just do that, since initcode is metered via EIP-3860.
ZK-friendliness
EOF is purportedly more ZK-friendly. However, we have not seen arguments for why this is a hard requirement.
In other words, all the benefits of EOF can be introduced in more piecemeal, less invasive updates to the EVM.
Meanwhile, the complexities of EOF cannot be ignored:
Legacy EVM needs to be maintained, probably indefinitely.
Tooling needs to support EOF. This requires coordination and effort across many different teams.
Risk of vulnerabilities due to complexity. For instance, send() and transfer() now allow reentrancy. This was not noticed until a month before Osaka was scheduled to be finalised, even though EOF has been in development for nearly 4 years.
Related to that, EOF has been a moving target: EOF is looking to get shipped even though parts of the spec keep changing, making it difficult for compiler and app devs to review the entire package.
Work to target the new format for compilers and app devs.
EVM contracts get much more complicated due to headers. An empty contract is now at least 15 bytes.
Bytecode size tradeoffs. For instance, subroutines cost several bytes just to declare, which penalises contracts with many, small subroutines.
Update: I have co-authored a new and longer EOF deep-dive: EOF: When Complexity Outweighs Necessity. We break down its supposed benefits and argue they’re more “nice-to-haves” than essential upgrades. Instead of adding complexity, we highlight cleaner, less disruptive solutions that achieve the same goals. EOF’s objectives are solid—but there’s a smarter way to get there.
Please give it a read and let us know your thoughts in this thread.
I’ll spend the time to give a more detailed response later this week, but I would like to point out that all of these questions were already asked and answered as a part of ACD 192 9 months ago. I do not see any new concerns raised here.
Perhaps as a meta point there seems to be disagreement about whether major EVM changes are desirable in general (including in the notes from the call you mentioned). I would argue that a stable VM, on which people can invest in building up excellent tooling and apps with confidence, is much more valuable than the ability to evolve the VM to compete with other VMs, since this has the cost of scaring away investment in something that’s gonna potentially change in the future. (I’m speaking in part here as someone who becomes much more hesitant about my investments in the EVM, both as user and dev, when I see that stability of the VM isn’t being prioritised.)
I just want to echo this sentiment - As someone who has been building on the EVM for a long time now, all of these changes adding in unnecessary complexity and changing assumptions is a huge turn off for developers building on the supposed “world computer”.
IMHO tracking deployment height and applying different semantics to a contract on that basis has drawbacks. It means that if you have deployment transactions, they might produce different results if you cross the hardfork boundary. Although this is technically true already with gas schedule changes, this seems much more likely to cause issues. This is also true if you work in an Osaka environment and then switch networks. And we haven’t addressed contracts creating contracts which could also be messy. By contrast, versioning seems more elegant and allows explicit opt-in.
That does not mean that all of EOFv1 belongs as a bundle together. In fact it would be possible to introduce versioning on its own and nothing else, but there would be no incentive to upgrade. If we want a carrot, a good choice would be stack validation + relative jumps, which enable optimizations that could enable a gas discount. This pulls in roughly half of the EOF EIPs, which constitute a more minimal set and exclude some of the more problematic changes like gas inobservability. This may be inaccurate because I haven’t been following EOF closely, but the message is: versioning per se uniquely unlocks a more efficient EVM afaict.
If something has to be included, I think I could get behind adding relative jumps (although not yet convinced they are really worth it).
I don’t believe stack validation needs changed semantics. It can be done already via analysis of status-quo EVM code. Is there reason to think otherwise?
I take this opportunity to call out another pattern I have observed in discussion of EOF so far: that proponents seem keen to equivocate between “EOF enables X” and “X requires EOF”, when these are not in fact logically equivalent statements. I hope readers of these discussions take heed that demonstrating “Y enables X” is much easier than proving that “X requires Y”, but it’s the latter that we need proven before biting the bullet on adopting Y (assuming X is highly desired).
Thanks for your point about versioning. On that point my previous comment about stability still feels relevant: I would rather not make plans (like versioning) that make major EVM upgrades easier, because I would rather not have major EVM upgrades and for the benefits of this to be appreciated.
As I stated over here EOF creates new problems for application-layer developers like me. Seeing as it also doesn’t really solve any of the existing problems that I have, I would not adopt EOF. Also seeing as legacy EVM will need support for the foreseeable future, I’m not enthusiastic about EOF.
My 3 major gripes are:
TXCREATE’s functionality (or lack thereof) not supporting CREATE3-like patterns.
The insistance around lack of inspectability of bytecode (and how you can’t completely hide the bytecode if you have anything resembling CREATE2)
Gas rules (not allowing capped-gas *CALL, the 2300 gas rule, 63/64ths rule)
After thinking about it, I personally believe that it would be possible to do stack validation without pulling in EOF. It would involve analyzing “legacy” bytecode using a similar algorithm as EIP-5450. Specific sequences involving the PC opcode would be recognized as static relative jumps, (a bit like aiupc in RISC-V) and PUSH-JUMP as absolute. Then if validation fails, the contract would be marked as non-compliant, but still remain usable with backwards compatible semantics as before. If it is compliant, it would be marked as such and potentially benefit from gas discounts and a higher size limit. This would still require a hardfork, but would be much more transparent to the application layer.
There would need to be an EIP to flesh this out and give it a chance. I am willing to help write it if there is interest.
The question I always ask is who would use EOF if L2s dominate the market share?
With such a huge EVM update it’s not enough for Ethereum to just be a “North Star”. Existing L2 stacks have to upgrade as well or an official stack need to be provided and maintained by EF.
Honestly, there are three things that scare me the most:
Instantly added complexity to EVM.
Even wider divergence of L2s from L1.
Short term concentration on the app layer “fixes”, while not addressing scalability issues and not bringing new ideas to L1.
Also with the current rollup-centric roadmap Ethereum may well become a pure DA layer. So all these EVM shenanigans may not really worth the effort.
Every L1 hard fork needs to be adopted by L2s, which they consistently do. What makes you think L2s wouldn’t adopt this given the exceptionally strong demand for full Ethereum equivalence?
At least for OP Stack EOF support would be inherited from the L1 client implementations quite easily and I don’t see any reason it wouldn’t be supported.
Can you elaborate on this one please? TXCREATE as specced out in EIP-7873 is designed to allow anyone to deploy a CREATE3-like EOF factory to a deterministic address on different chains. By CREATE3-like I’m assuming you mean one which doesn’t include the initcode hash in the address.
If EIP-7873 misses any deployment pattern, please let us know.
Ahh, I wrote that original comment before the changes to 7873 that removed the inithash from the address calculation. I did not re-read the EIP to pick up the changes. I think that as it exists right now, 7873 supports every usecase that I can think of
IMHO static jumps on their own are not really worth it because they can be done with combos of PC PUSH ADD/SUB and JUMP(I).
As far as I can tell this type of analysis can be done. Since we wouldn’t have dedicated jump and call instructions, we would have to “bless” certain opcode sequences as the official ways to do a relative jump or a subroutine call and statically track the stack that way. Then you would be able to add protocol-level flags to the contract accounts that pass validation, which is something that EOF wanted to avoid. Someone could also explicitly trigger validation of an existing contract, which isn’t possible with EOF.
It seems as though if we already specced stack validation for EOFv1, we can port it back to legacy EVM. Plus, it seems compilers most likely generate compliant bytecode already. I would love if compiler people and client devs could check me on this though.
That is a very fair concern for sure. It also made me think that if Ethereum is aiming to eventually ossify, at some point there would be an ultimate EOF version and we would stop changing semantics. However, we would be left with a lot of overhead due to all the previous versions. If we can avoid than and still get most of the benefits that would be nice.
A lot of these questions have been asked and answered in other forums. But it’s great to get a centralized list.
Compilers indeed can solve the famous “stack too deep” problem with register allocation but they cannot guarantee optimal solution. But even if they could, stack/register spilling is very cost ineffective without reliable access to cheap memory. The EVM’s non-linear gas cost for memory makes access to larger variable pools more expensive and fragile than a paged memory model like one in typical silicon processors.
With this in mind, solving this problem on the compiler level is just a trade-off, rather than a direct improvement. Efficient and safe upgrade to EVM’s stack management is important, EIP-663 achieves that and in order to do so safely, requires immediate arguments (see other points).
Removing JUMPDESTs will have very negative downstream impact. JUMPDEST addressed an inherent defect in dynamic jumps. Without JUMPDEST compilation via Intermediate Representations (IR) (e.g. LLVM) was infeasible.
There are also security implications related to preventing execution of data carried in the contract. Details should be better explained in a stand alone article.
This describes an approach that was tried and rejected in 2019 (EIP-1702). You need to know which rule set to follow not only when validating, but also when executing a contract, so information about the version must be stored alongside the code. EIP-7702 also greatly complicates account versioning for contract validation. This is why EOF opts for storing it inside bytecode container.
Secondly, to allow for validation we also would need to allow for data segments. We would need to mark code as being EOF, then mark where the code ends and the data starts. This is the essence of the EOF container, and falls naturally from first principles once the need for validation is established.
Finally, EOF aims for changing the version as little as possible, ideally never. New rule set at every fork would mean a lot of versions co-existing together in the state, which greatly complicates reasoning about overall execution rules, any future (EVM- or adjacent) upgrade must take into account all possible versions it affects, and EVM implementations must maintain multiple rule sets in parallel. These are the points that EOF is already being criticized for, while introducing only one additional version. Piecemeal changes would have a net grater complexity if not aggregated in an upgrade like EOF.
EOF forbids deploying undefined opcodes, which allows to introduce new opcodes in the future without changing EOF version. The system where new opcodes require new validation rules at every fork would be more complicated (see previous point).
This is exactly how EOF removes gas introspection.
This is exactly how EOF removes code introspection.
Devising and shipping an intermediate container format which just enables code introspection removal and then iterating on it to achieve other benefits of the structuring means more EVM versions co-existing together in the state (see previous point). At the same time, achieving just code non-introspection by itself would not warrant the effort or complexity required for the containerization.
This proposal has zero benefit for static analysis, because this pattern exists today and static analysis tools already recognize it. Dynamic jumps are not banned and this is what the tooling struggles to analyze.
Furthermore, detecting this carveout would slow down VM implementations as they would need to take processor time to detect the pattern at every jump. Gas charged will go down but so will gas per second and transactions per second.
If this refers to EIP-2315, that one was rejected by ACD after arguments were made it would not be adopted. The argument was that there wasn’t enough benefit and enough gas saving provided by it. EOF’s approach to subroutines (functions) have strong support from at least one compiler team.
Important consideration here is that we want to prevent breaking of already deployed contracts if/when an ASE proposal is introduced. There are operational contracts that depend on the trimming of the address argument. This is what derailed previous attempts at address space extension.
EIP-3860 addressed a JUMPDEST analysis exploit that was best executed at initcode time, however there is no metering to loading contracts from state. JUMPDEST analysis still needs to be performed or cached. And this needs to be loaded from the cache or performed every time the contract is executed. Piecemeal changes to existing analysis rules would also require cached validations to be updated.
EOF code validation is performed only when the code is first introduced to the blockchain via the initcode transaction. Hence any other interactions with EOF code (reading the contract from state, executing other contracts, factory contracts creating new contracts, etc) are performed without needing any further validation. Validation is asserted for the life of the bytecode.
We do not claim it is a hard requirement. But ZK-friendliness is an argument in favor of EOF. Initial implementations have shown a 2.5 to 3x speed improvement in the cost of ZK proving a contract transaction. Driving down the prices of ZK transaction is needed to achieve the “nickle” goal of Ethereum scaling.
Piecemeal improvements have a greater cost over the longer term of their implementation. Multiple backwards incompatible changes will cause the clients to have to support multiple incompatible versions of the EVM. This is why EOF is batching proposed backwards incompatible changes in to as few steps as possible, ideally one.
Piecemeal changes also have a tendency to complicate each other when not properly coordinated. One previously considered alternative was to add contract versioning (EIP-1702). The new EIP-7702 contract delegate feature in Pectra breaks any safety guarantees that this EIP would have provided, as the actual code the account contains could easily be updated, and now two different accounts need to be considered in the logic, increasing the number of ways a client optimizing this interaction could introduce unexpected bugs.
Packaging the changes in aggregate allows the net impact of all of the changes to be smaller and less invasive than if each change were to be introduced slowly. It also removes the need for intermediate states between each piecemeal change, where novel attacks could be launched.
Piecemeal would mean we need to maintain multiple non-compatible EVMs. Also the piecemeal approach has been proposed before and rejected by ACD/the community as not useful enough and problematic (EIP-2315, EIP-1702, EIP-615, for example).
It is much more rational for EOF to ship all breaking changes in only one backwards-incompatible step.
EOF is at its core a container format for the EVM, and is designed to be run within the same VM code. The same operation dispatch, contract calling code, storage access, memory handling, arithmetic, etc. et. al. is all handled by the same client code whether the contract is in an EOF container or is a legacy contract. EOF shares more with the legacy EVM than what differentiates it.
By “legacy EVM” in this context we should clarify that we are talking about 16 removed opcodes, making up for roughly 10% of current opcode count. Some of these opcodes share large portions of logic with their legacy counterparts (e.g. EXT*CALL, EOFCREATE), bringing that figure further down.
Also, the potential for a legacy → EOF bytecode migration still has not been definitely ruled out, albeit it is a daunting task best approached once EOF is in production.
This is true for all changes to the EVM. Coordination is done via the bi-weekly EOF implementers call everyone is welcome to. Besides the EOF implementers and EIP authors, there are members of testing, compiler and tooling teams participating. Some teams have already shipped devnet-0 support in their tools.
Solidity’s current plan is to deprecate send() and transfer() for EOF contracts. Other means to prevent reentrancy are encouraged (TSTORE/TLOAD), especially so since relying on gas to block reentrancy is subject to break unexpectedly on gas repricings, which is also one of the reasons gas introspection is removed in EOF in general.
Other developments like the PAY opcode may impact that decision, but given the subtleties between the implementations Solidity may choose to expose it via a new API (such as pay()) while providing compiler warnings when mapping send and transfer to the PAY opcode, but this is a decision for the Solidity team.
Answering the notion that the issue remained unnoticed for nearly 4 years: the first PR with initial EOF Solidity support was pushed on July 23rd 2024. This is a problem which is related to language implementation and how it deals with reentrancy issue. It was actually noticed by the Solidity team soon after that PR opened and before the official release with experimental support for EOF.
Specifications being updated prior to activation is the norm in Ethereum, not the exception. For instance, EIP-7702 which had a change merged one month before Holesky activation. EIP-2537’s spec was updated after the testnet hard fork. This EIP is even older than the main EOF specifications.
The changes to spec are driven mostly by feedback from ACD and the community, or they are immaterial clarifications and renamings. For context and point of reference: we are still months away from EOF going to testnet and intend to freeze breaking changes with devnet-1, months from the public testnet rather than weeks we have seen in other major features.
Solidity has already updated their compiler to the current devnet-0 spec and end users will not need to update the source code of their contracts unless they are depending on deprecated features, such as the current incarnation of SELFDESTRUCT.
With EOF, contracts get much simpler thanks to separating data and code reliably, as well as replacing dynamic jumps with static ones and function calls. Contract size cannot be seen as a metric to compare complication (or complexity).
In all but the simplest contracts the number of bytes used in the header is offset before the function dispatch code or first function is complete.
Small subroutines indeed may be inlined by compiler optimizations, which is already the case when targeting legacy EVM.
The current Solidity EOF implementation still have almost all optimizations steps disabled for EOF in assembly level. This means that this version’s 10% reduction in code size when using EOF should not be used to measure the final impact. We have experimental version with most important optimizations enabled which confirm code size and gas cost reduction for EOF. It’s going to be pushed to solidity repo soon. Also CPU time benchmarks confirm gains in execution speed of EOF compiled contracts.
This is a fully general argument that could be applied to any change in the protocol, not only EVM. Any change for the sake of scalability, security, usability, goes against “stability” of ossified protocol.
Ossifying the protocol at the wrong time presents more risk to the future of Ethereum than not ossifying at all.
This is a more nuanced take on the “one step” argument than the prior arguments and it deserves consideration in a less adversarial environment (such as delaying no code introspection and no gas intospection to a hypothetical ‘EOFv2’). However, we have seen problems in getting any EVM change adopted of any size, so there is no guarantee that breaking EOF into multiple parts would lead to the latter pieces being adopted.
The space that needs to be carved out is to ensure that no “compiler bombs” are allowed. While most contracts are well behaved the presence of a malicious contract can remove all the gains seen by compliant contracts. The sad truth of Ethereum security is we always optimize for the worst case, because these will be posted to the network (such as the Shanghai attacks in 2016).
Also, the time spent to separate compliant from non-compliant contracts could be more than the time saved by applying the compiler optimizations.
TXCREATE as specced out in EIP-7873 is designed to allow anyone to deploy a CREATE3-like EOF factory to a deterministic address on different chains. This is a core feature of the EOF devnet-1 plans and has been part of the mega spec for two years. It was removed from Prague as a concession to the size of the fork and was always intended to ship at some point. The current plan is to ship it with the Osaka specced EOF version.
Vitalik makes the best arguments for banning code introspection. It separates the representation of the contract from the consensus about its execution and outcome at the protocol level, allowing code to be transpiled to other formats, such as RISC-V. Allowing the code to enter or leave system memory locks in one particular representation of the contract as part of consensus.
Relying on a fixed gas schedule has been a problem for a long time and best practices are to not rely on it. EOF formalizes this recommendation in a way that allows future EVM repricings to have minimal impact on user code. There is a working group working on dramatic changes, including things like multi-dimensional pricing and code merkelization, which will have dramatic effects on the gas schedule as a whole.