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.
You can review planned EOF devnet-1 changes, and also review the EOF Testnet plan.
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.
From experiments with a preliminary PoC that was largely unoptimized we did not see any bytecode regressions for widely used contracts. The tested contracts all demonstrated a reduction in bytecode size.
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.