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 enableEvmV2 field to the record (after enableOptimizedOpcodes)
- Update the 3-arg convenience constructor to pass
false (shields 82+ callers of DEFAULT and 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 through enableEvmV2
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
@Option field: 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-fast option
- Pass to
EvmConfiguration in provideEvmConfiguration()
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 word
Conditional 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 word
popStackLongs(long[] dest) -- pop one 256-bit word into caller array
getStackV2Long(int wordOffset, int longIndex) -- peek at specific long
setStackLongs(int offset, long v0, long v1, long v2, long v3) -- overwrite
stackV2Size() -- 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 spotlessApply on changed files
./gradlew build from project root -- all existing tests pass (v2 is disabled by default, no behavior change)
- Verify the new flag is recognized:
--Xevm-go-fast=true should 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 in MainnetProtocolSpecs. A branch within runToHalt is simpler.
- Same MessageFrame, dual stack: Rather than a separate
MessageFrameV2, adding long[] fields to the existing class avoids touching the 50+ places that construct/consume MessageFrame. The v1 OperandStack stays untouched.
- 3-arg constructor shields callers: The new field gets a
false default in the convenience constructor, so 80+ files referencing EvmConfiguration.DEFAULT or 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
*Optimized pattern. No new interface needed.
Touchpoints to check:
Tasks:
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?
EVM v2 Skeleton Plan
Context
Besu's EVM currently uses Tuweni
Bytesas its stack operand type, with operations popping/pushingBytesviaMessageFrame. PR #9881 demonstrated a POC usinglong[](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-fastfeature toggle (default: disabled) and the minimal structural scaffolding to support a parallel "EVM v2" execution path that useslong[]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:
--Xevm-go-fastthroughEvmOptions->EvmConfigurationEVM.runToHalt()torunToHaltV2()long[]fields + push/pop/peek methodsAddOperationV2demonstrating the patternUInt256/Bytes32ataccount.getStorageValue(UInt256)boundary. The storage API (AccountState.getStorageValue/setStorageValue) uses Tuweni UInt256 and won't change; v2 ops must convert at the boundaryOperationTracercallsframe.getStackItem()returning Bytes. V2 needs lazy conversion or tracer v2 pathMainnetTransactionProcessorpushes initial values onto stack -- needs v2 pathImplementation Steps
Step 1: Add
enableEvmV2toEvmConfigurationFile:
evm/src/main/java/org/hyperledger/besu/evm/internal/EvmConfiguration.javaboolean enableEvmV2field to the record (afterenableOptimizedOpcodes)false(shields 82+ callers ofDEFAULTand 14 callers of the 3-arg constructor from any changes)(long, WorldUpdaterMode, boolean, boolean)for explicit v2 opt-inoverrides()to pass throughenableEvmV2Step 2: Wire
--Xevm-go-fastCLI flagFile:
app/src/main/java/org/hyperledger/besu/cli/options/EvmOptions.javaEVM_GO_FAST = "--Xevm-go-fast"constant@Optionfield:boolean enableEvmV2 = false(hidden, arity=1, fallbackValue="false")toDomainObject()to use the new 4-arg constructorFile:
ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/EvmToolCommandOptionsModule.java--Xevm-go-fastoptionEvmConfigurationinprovideEvmConfiguration()Step 3: Add v2 stack to MessageFrame
File:
evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.javaAdd fields (alongside existing
OperandStack stack):Conditional allocation in constructor (pass
enableEvmV2through Builder):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 depthThe Builder gets a new
enableEvmV2(boolean)method. When building from a parent frame, inherit the setting.Step 4: Add
runToHaltV2()to EVMFile:
evm/src/main/java/org/hyperledger/besu/evm/EVM.javaAt the top of
runToHalt(), add early branch:Add new private method
runToHaltV2(MessageFrame, OperationTracer):runToHalt(while loop, opcode fetch, switch, post-processing)AddOperationV2.staticOperation(frame)currentOperation.execute(frame, this)(v1 path -- won't work correctly for a real execution but establishes the skeleton structure)Step 5: Add stub
AddOperationV2in packageorg.hyperledger.besu.evm.operation.v2New file:
evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AddOperationV2.javastaticOperation(MessageFrame)methodframe.popStackLongs()frame.pushStackLongs()OperationResult(3, null)(gas cost 3, no halt)Step 6: Update
EvmConfigurationcallers (minimal)Only files that use the full canonical constructor need updating (the 3-arg convenience constructor shields most callers):
EvmConfiguration.overrides()-- pass through fieldnew 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)--Xevm-go-fast=trueshould be accepted without errorAddOperationV2.staticOperation()to verify the stub worksKey Design Decisions
BiFunction<GasCalculator, EvmConfiguration, EVM>return type across all fork definitions inMainnetProtocolSpecs. A branch withinrunToHaltis simpler.MessageFrameV2, addinglong[]fields to the existing class avoids touching the 50+ places that construct/consume MessageFrame. The v1OperandStackstays untouched.falsedefault in the convenience constructor, so 80+ files referencingEvmConfiguration.DEFAULTor the 3-arg constructor need zero changes.*Optimizedpattern. No new interface needed.Touchpoints to check:
Tasks:
Notes:
org.hyperledger.besu.evm.operation.v2?