-
Notifications
You must be signed in to change notification settings - Fork 1k
EVM V2 - Stack Long Array Skeleton #10131
Description
EVM v2 Skeleton Plan
Context
Besu's EVM currently uses Tuweni Bytes as its stack operand type, with operations popping/pushing Bytes via MessageFrame. PR #9881 demonstrated a POC using long[] (4 longs per 256-bit word) for significantly better performance by eliminating object allocation on the hot path.
This skeleton PR introduces the --Xevm-go-fast feature toggle (default: disabled) and the minimal structural scaffolding to support a parallel "EVM v2" execution path that uses long[] stack operands. Both EVM versions must coexist during the transition period.
Categories of Touchpoints with Main Branch
Even though only categories 1-4 are in the skeleton PR, all categories are listed here for completeness:
| # | Category | Skeleton? | Description |
|---|---|---|---|
| 1 | CLI flag + config wiring | YES | --Xevm-go-fast through EvmOptions -> EvmConfiguration |
| 2 | EVM dispatch | YES | Branch in EVM.runToHalt() to runToHaltV2() |
| 3 | MessageFrame v2 stack | YES | long[] fields + push/pop/peek methods |
| 4 | Stub v2 operation (ADD) | YES | AddOperationV2 demonstrating the pattern |
| 5 | All arithmetic ops | deferred | ADD, SUB, MUL, DIV, SDIV, MOD, SMOD, ADDMOD, MULMOD, EXP, SIGNEXTEND, comparisons, bitwise, shifts |
| 6 | Stack manipulation ops | deferred | DUP1-16, SWAP1-16, POP, PUSH0, PUSH1-32, DUPN, SWAPN, EXCHANGE |
| 7 | Memory ops | deferred | MLOAD, MSTORE, MSTORE8, MCOPY -- long[] <-> byte[] conversion at boundary |
| 8 | Storage ops (bonsai boundary) | deferred | SLOAD, SSTORE -- long[] <-> UInt256/Bytes32 at account.getStorageValue(UInt256) boundary. The storage API (AccountState.getStorageValue/setStorageValue) uses Tuweni UInt256 and won't change; v2 ops must convert at the boundary |
| 9 | Call/Create ops | deferred | CALL, STATICCALL, DELEGATECALL, CREATE, CREATE2 -- child frame spawning, input/output marshaling |
| 10 | Environment ops | deferred | ADDRESS, BALANCE, CALLER, CALLVALUE, CALLDATALOAD, GASPRICE, BLOCKHASH, etc. -- push env values as longs |
| 11 | Control flow ops | deferred | JUMP, JUMPI, STOP, RETURN, REVERT -- RETURN/REVERT need long[] -> bytes for output |
| 12 | Log ops | deferred | LOG0-LOG4 -- topic conversion from long[] to Bytes32 |
| 13 | Tracing compatibility | deferred | OperationTracer calls frame.getStackItem() returning Bytes. V2 needs lazy conversion or tracer v2 path |
| 14 | Precompiles | deferred | Input marshaling from stack+memory to Bytes for precompile contracts |
| 15 | EOF ops | deferred | RJUMP, CALLF, RETF, EOFCREATE, DATALOAD, etc. |
| 16 | Transaction processor | deferred | MainnetTransactionProcessor pushes initial values onto stack -- needs v2 path |
| 17 | Test infrastructure | deferred | Reference tests, unit tests parameterized for v1/v2 |
Implementation Steps
Step 1: Add enableEvmV2 to EvmConfiguration
File: evm/src/main/java/org/hyperledger/besu/evm/internal/EvmConfiguration.java
- Add
boolean enableEvmV2field to the record (afterenableOptimizedOpcodes) - Update the 3-arg convenience constructor to pass
false(shields 82+ callers ofDEFAULTand 14 callers of the 3-arg constructor from any changes) - Add a 4-arg convenience constructor:
(long, WorldUpdaterMode, boolean, boolean)for explicit v2 opt-in - Update
overrides()to pass throughenableEvmV2
Step 2: Wire --Xevm-go-fast CLI flag
File: app/src/main/java/org/hyperledger/besu/cli/options/EvmOptions.java
- Add
EVM_GO_FAST = "--Xevm-go-fast"constant - Add picocli
@Optionfield:boolean enableEvmV2 = false(hidden, arity=1, fallbackValue="false") - Update
toDomainObject()to use the new 4-arg constructor
File: ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/EvmToolCommandOptionsModule.java
- Mirror the same
--Xevm-go-fastoption - Pass to
EvmConfigurationinprovideEvmConfiguration()
Step 3: Add v2 stack to MessageFrame
File: evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java
Add fields (alongside existing OperandStack stack):
private final long[] stackV2; // null when v2 disabled; 4 longs per word
private int stackV2Top; // -1 = empty, index of top wordConditional allocation in constructor (pass enableEvmV2 through Builder):
this.stackV2 = enableV2 ? new long[txValues.maxStackSize() * 4] : null;
this.stackV2Top = -1;New public methods:
pushStackLongs(long v0, long v1, long v2, long v3)-- push one 256-bit wordpopStackLongs(long[] dest)-- pop one 256-bit word into caller arraygetStackV2Long(int wordOffset, int longIndex)-- peek at specific longsetStackLongs(int offset, long v0, long v1, long v2, long v3)-- overwritestackV2Size()-- current v2 stack depth
The Builder gets a new enableEvmV2(boolean) method. When building from a parent frame, inherit the setting.
Step 4: Add runToHaltV2() to EVM
File: evm/src/main/java/org/hyperledger/besu/evm/EVM.java
At the top of runToHalt(), add early branch:
if (evmConfiguration.enableEvmV2()) {
runToHaltV2(frame, tracing);
return;
}Add new private method runToHaltV2(MessageFrame, OperationTracer):
- Same structure as
runToHalt(while loop, opcode fetch, switch, post-processing) - Switch contains only the stub ADD case calling
AddOperationV2.staticOperation(frame) - Default case falls through to
currentOperation.execute(frame, this)(v1 path -- won't work correctly for a real execution but establishes the skeleton structure) - Same gas deduction and halt handling as v1
Step 5: Add stub AddOperationV2 in package org.hyperledger.besu.evm.operation.v2
New file: evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AddOperationV2.java
- Static
staticOperation(MessageFrame)method - Pops two 256-bit values via
frame.popStackLongs() - Performs 256-bit addition using 4 longs with carry propagation
- Pushes result via
frame.pushStackLongs() - Returns
OperationResult(3, null)(gas cost 3, no halt) - No object allocation on the hot path
Step 6: Update EvmConfiguration callers (minimal)
Only files that use the full canonical constructor need updating (the 3-arg convenience constructor shields most callers):
EvmConfiguration.overrides()-- pass through field- Any test or evmtool code using the 7-arg canonical constructor directly (check
new EvmConfiguration(with >3 args)
Verification
./gradlew spotlessApplyon changed files./gradlew buildfrom project root -- all existing tests pass (v2 is disabled by default, no behavior change)- Verify the new flag is recognized:
--Xevm-go-fast=trueshould be accepted without error - Optionally: a simple unit test that enables v2, creates a MessageFrame, pushes/pops longs, and calls
AddOperationV2.staticOperation()to verify the stub works
Key Design Decisions
- Same EVM class, not a subclass: Adding a subclass would require changing the
BiFunction<GasCalculator, EvmConfiguration, EVM>return type across all fork definitions inMainnetProtocolSpecs. A branch withinrunToHaltis simpler. - Same MessageFrame, dual stack: Rather than a separate
MessageFrameV2, addinglong[]fields to the existing class avoids touching the 50+ places that construct/consume MessageFrame. The v1OperandStackstays untouched. - 3-arg constructor shields callers: The new field gets a
falsedefault in the convenience constructor, so 80+ files referencingEvmConfiguration.DEFAULTor the 3-arg constructor need zero changes. - V2 operations are new classes (not extending Operation) in a v2 package: They're static utility methods called from the v2 switch, matching the existing
*Optimizedpattern. No new interface needed.
Touchpoints to check:
- MessageProcessor (wired all over MainnetProtocolSpecs)
- ContractCreationProcessor (wired all over MainnetProtocolSpecs)
- The storage API (AccountState.getStorageValue/setStorageValue) uses Tuweni UInt256 and won't change; v2 ops must convert at the boundary(!)
- Unit tests
- Memory.java
- Tracers
- MainnetEVMs
- UInt256
Tasks:
- Support --Xevm-v2 flag in referenceTest
Notes:
- Do we need duplicate packages beyond
org.hyperledger.besu.evm.operation.v2? - For any duplicated packages, can we add some sort of gradle linting to warn that if updating v1, v2 needs updating too?
- Can we branch in EVM without impact?