Skip to content

EVM V2 - Stack Long Array Skeleton #10131

@siladu

Description

@siladu

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

  1. ./gradlew spotlessApply on changed files
  2. ./gradlew build from project root -- all existing tests pass (v2 is disabled by default, no behavior change)
  3. Verify the new flag is recognized: --Xevm-go-fast=true should be accepted without error
  4. 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:

  • 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?

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions