Skip to content

feat(forks,tests): implement EIP-7979 Call and Return Opcodes - #3575

Open
gcolvin wants to merge 2 commits into
ethereum:forks/amsterdamfrom
gcolvin:eips/amsterdam/eip-7979
Open

gcolvin wants to merge 2 commits into
ethereum:forks/amsterdamfrom
gcolvin:eips/amsterdam/eip-7979

Conversation

@gcolvin

@gcolvin gcolvin commented Sep 11, 2026

Copy link
Copy Markdown

Reference implementation and tests for EIP-7979, proposed for Hegotá (PFI, ethspecs/pm#72). All 130 fixtures fill against the spec; 'just static' is clean.

EIP-7979 adds explicit call and return instructions to the EVM: a per-frame return stack and three opcodes, CALLSUB (0xB0), CALLDEST (0xB1) and RETURNSUB (0xB2), using the EIP's placeholder values.

Spec (src/ethereum/forks/amsterdam):

  • Evm gains return_stack and valid_call_destinations; RETURN_STACK_LIMIT = 1024.
  • The JUMPDEST scan becomes a single pass, get_valid_destinations, returning both the jump and the call destination sets. A CALLDEST is both, so JUMP and JUMPI need no change. The EIP-8024 immediate handling is preserved.
  • callsub/calldest/returnsub in control_flow.py; ReturnStackOverflowError and ReturnStackUnderflowError; gas costs mid (8), jumpdest (1) and low (5).

Framework (packages/testing):

  • Opcode table entries and the EIP7979 fork mixin, picked up by Amsterdam automatically.

Tests (tests/amsterdam/eip7979_callsub, 44 tests, 130 fixtures):

  • The EIP's five vectors byte-for-byte; invalid destinations (JUMPDEST, PUSH data, DUPN immediate, out of range, uint64 overflow, self, stack underflow); return-stack underflow; the 1024 limit by counted recursion; per-frame isolation across all four call opcodes; call elimination via JUMP/JUMPI, tail call and mutual recursion at constant depth; exact gas by measurement; initcode, CREATE, CREATE2 and 7702 contexts; fork transition.
  • tests/frontier/opcodes/test_all_opcodes.py: CALLSUB and RETURNSUB get the same special-casing as JUMP in the generic harness.

Description

Related Issues or PRs

N/A.

Checklist

  • [ X] Ran fast static checks to avoid CI fails, see Code Standards & Verifying Changes: just static
  • [X ] PR title has the form <type>(<area>): <title>, where <type> and <area> come from an appropriate C-<type>, respectively A-<area>, label. The title should match the target squash commit message.

Cute Animal Picture

image

EIP-7979 adds explicit call and return instructions to the EVM: a
per-frame return stack and three opcodes, CALLSUB (0xB0), CALLDEST
(0xB1) and RETURNSUB (0xB2), using the EIP's placeholder values.

Spec (src/ethereum/forks/amsterdam):
- Evm gains `return_stack` and `valid_call_destinations`;
  RETURN_STACK_LIMIT = 1024.
- The JUMPDEST scan becomes a single pass, `get_valid_destinations`,
  returning both the jump and the call destination sets. A CALLDEST is
  both, so JUMP and JUMPI need no change. The EIP-8024 immediate
  handling is preserved.
- callsub/calldest/returnsub in control_flow.py; ReturnStackOverflowError
  and ReturnStackUnderflowError; gas costs mid (8), jumpdest (1) and
  low (5).

Framework (packages/testing):
- Opcode table entries and the EIP7979 fork mixin, picked up by
  Amsterdam automatically.

Tests (tests/amsterdam/eip7979_callsub, 44 tests, 130 fixtures):
- The EIP's five vectors byte-for-byte; invalid destinations (JUMPDEST,
  PUSH data, DUPN immediate, out of range, uint64 overflow, self, stack
  underflow); return-stack underflow; the 1024 limit by counted
  recursion; per-frame isolation across all four call opcodes; call
  elimination via JUMP/JUMPI, tail call and mutual recursion at constant
  depth; exact gas by measurement; initcode, CREATE, CREATE2 and 7702
  contexts; fork transition.
- tests/frontier/opcodes/test_all_opcodes.py: CALLSUB and RETURNSUB get
  the same special-casing as JUMP in the generic harness.
@gcolvin

gcolvin commented Sep 11, 2026

Copy link
Copy Markdown
Author

EIP-7979: Call and Return Opcodes for the EVM

Checklist revision: 2 (28 anchors)

Link: https://eips.ethereum.org/EIPS/eip-7979

Self-assessment by the EIP author, offered as a starting point for the STEEL evaluator. Every row cites the reference implementation, ethereum/execution-specs#3575, rather than an estimate.

Execution Specs

Specs

Implemented in execution-specs#3575 on eips/amsterdam/eip-7979, from the reference code in the EIP. Nine spec files, about forty lines of logic:

  • Evm gains return_stack: List[Uint] and valid_call_destinations: Set[Uint]; RETURN_STACK_LIMIT = 1024.
  • The JUMPDEST scan becomes one pass returning both destination sets. A CALLDEST is in both, so jump and jumpi are unchanged.
  • callsub, calldest, returnsub in control_flow.py; two new ExceptionalHalt subclasses; three GasCosts entries (mid, jumpdest, low); three Ops entries.

No transaction, block, state, precompile, system-contract, or fork-activation code is touched.

Testing

Tests in tests/amsterdam/eip7979_callsub/: 44 tests, 130 fixtures, all filling against the spec. Static checks (just static) clean.

Checklist

Anchor Score (0–3) Rationale
EVM Gas rule changes 0 Three new opcodes with constant costs (8, 1, 5) through the existing constant-gas path. No accounting mechanism is added or changed.
State-access ordering within opcode execution 0 None of the three opcodes reads or writes state.
Blob gas accounting changes 0 None.
State gas accounting changes 0 None.
New EVM gas refund 0 None.
Patterns affecting pre-existing tests 1 One file: tests/frontier/opcodes/test_all_opcodes.py, the generic every-opcode harness, needs CALLSUB and RETURNSUB special-cased exactly as JUMP already is (a jump to nowhere halts). Five edits. No other existing test changes; 2,217 Osaka and Amsterdam cases of that harness re-fill unchanged, as do the EIP-8024 vectors and transition tests.
New invariant on pre-existing tests 0 Pre-existing tests assert nothing new.
Transition-tool interface changes 0 None. The return stack is frame-local machine state and never crosses the t8n boundary.
New test-framework primitives 1 Three entries in the opcode table and a 30-line EIP7979 fork mixin, both copies of existing patterns (EIP-7939). No new expectation or modifier types; CodeGasMeasure and StateTestFiller suffice for every test.
Cryptography-related testing 0 None.
Edge/boundary conditions 2 Two mechanisms: the return stack (empty on RETURNSUB, the 1024 limit, per-frame reset across message calls) and CALLSUB destination validity (which reuses JUMPDEST analysis: PUSH data, EIP-8024 immediates, out of range, uint64 overflow). Neither needs an elevated case count; both are fully covered by the 44 tests, including the 1024 boundary from both sides.
Block syncing changes 0 None.
Engine API changes 0 None.
Added system contracts 0 None.
Modified system contracts 0 None.
Added opcodes 2 Multiple new simple opcodes: no data portion, constant gas, at most one stack item popped.
Modified opcodes 3 Scored by the letter of the anchor: JUMP and JUMPI gain CALLDEST as a valid destination. In effect the change is nil: the only affected inputs are jumps to a 0xB1 byte, which halted unconditionally before (undefined opcode) and no deployed contract can have relied on; jump/jumpi code is untouched, the scan simply adds positions; no existing test vector changes outcome. An evaluator may reasonably score this 0.
Added precompiles 0 None.
Modified precompiles 0 None.
Encoding changes (RLP/SSZ) 0 None.
New transaction types 0 None.
New or modified transaction validity mechanisms 0 None.
New block / header fields 0 None.
New fork activation mechanism 0 None. The new machine-state fields are initialized per frame, which the anchor excludes.
Performance risks 1 Benchmarkable in isolation (the EIP's assets include call-tree and multiply-chain kernels for evmone-bench). The return stack is allocated per frame and touched only by the new opcodes, so existing code paths gain no per-instruction cost.
Security risks 1 Self-contained. Return addresses live in a stack inaccessible to EVM code, so control flow cannot be corrupted from the data stack. The one new resource is bounded: 1024 entries per frame times 1024 frames, 8 MiB worst case at 8 bytes per entry. No existing invariant is altered.
Unspecified behavior requiring cross-client consensus 1 Semantics are fully determined; the fixtures pin every case the tests construct, including the implicit STOP past end of code. The opcode values (0xB00xB2) are placeholders pending assignment, and final gas costs await benchmarking; changing either re-baselines the fixtures once, mechanically.
Cross-EIP interactions (uncapped) 1 EIP-8024 (Amsterdam): the shared destination scan must skip 8024 immediates for CALLDEST as it does for JUMPDEST; handled in the same function and covered by a test (0xB1 inside a DUPN immediate is not a destination). Tested independently otherwise. No dependency on, modification of, or conflict with any other EIP; EIP-8337 (validation) depends on this EIP but is not proposed.

Total: 13 (10 if Modified opcodes is scored 0; 12 if Cross-EIP interactions is scored 0; 9 if both.)

Special Considerations

None. No dimension of the test matrix multiplies against another: the destination cases, the return-stack cases, and the execution-context cases (four call opcodes, two creation paths, transaction initcode, EIP-7702 delegation) are independent, and the full product is already filled in 130 fixtures.

Notes

  • The reference implementation is the EIP's own EELS code; the PR adds it to the current development fork with the EIP-8024 scan logic preserved.
  • If the EIP is scheduled for Bogotá rather than Amsterdam, the fork marker and the transition fixture (for_bpo2toamsterdamattime15k) are re-targeted; no test logic changes.
  • Tracing: EIP-3155 has no field for the return stack. Not a consensus or testing concern; noted for client implementers.
  • The 1024 limit is deliberately tested from both sides (recursion 1023 deep succeeds, 1024 halts) and distinguished from out-of-gas (an overflow halt leaves the caller its withheld gas).

Final Assessment

Category Description Value
Total Score Sum of all anchor scores 13 (9–13 across the two judgment rows)
Complexity Tier Computed from total score 🟡 at the boundary by the letter; 🟢 under either alternative reading

For comparison within this fork's table: FOCIL scored 15 and Frame Transactions 38 on revision 1's scale. Among execution-layer feature EIPs proposed for Hegotá, this is the smallest surface: three constant-gas opcodes and one frame-local stack, with no state, transaction, block, or gas-accounting component.

ethereum-spec-lint's UintLenHygiene rule requires ulen(...) rather than
Uint(len(...)).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant