Skip to content

chore: prep for Amsterdam fork#3352

Merged
tac0turtle merged 9 commits into
mainfrom
marko/reth_2.3
Jul 15, 2026
Merged

chore: prep for Amsterdam fork#3352
tac0turtle merged 9 commits into
mainfrom
marko/reth_2.3

Conversation

@tac0turtle

@tac0turtle tac0turtle commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Overview

Summary by CodeRabbit

  • New Features

    • Added fork-aware Engine API version selection for forkchoice and payload lifecycle across Prague, Osaka-Fusaka, and Amsterdam.
    • Preserved Amsterdam-specific payload attributes (including slotNumber and blockAccessList) end-to-end.
    • Added E2E coverage for the Engine API fork-upgrade cycle and observed method sequencing.
  • Bug Fixes / Improvements

    • Improved payload handling to tolerate schema differences by preserving raw Amsterdam-era fields through the lifecycle.
    • Enhanced fallback behavior when newer Engine API methods are unsupported.
  • Documentation

    • Updated Engine API reference docs and EVM setup guidance, including an Amsterdam enablement checklist.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0605032-39c3-4107-b8c1-d3a18b4f1768

📥 Commits

Reviewing files that changed from the base of the PR and between 81c347e and 768db34.

📒 Files selected for processing (3)
  • execution/evm/engine_payload.go
  • execution/evm/engine_rpc_client_test.go
  • execution/evm/test/engine_fork_upgrade_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • execution/evm/engine_payload.go
  • execution/evm/test/engine_fork_upgrade_test.go
  • execution/evm/engine_rpc_client_test.go

📝 Walkthrough

Walkthrough

Changes

The Engine API integration now supports fork-aware method selection across Prague, Osaka, and Amsterdam. Payload envelopes preserve raw Amsterdam fields, execution forwards slot numbers and execution requests, RPC fallbacks are cached, tracing reports resolved methods, and unit/E2E tests plus documentation cover the updated behavior.

Engine API fork support

Layer / File(s) Summary
Payload envelope and RPC contract
execution/evm/engine_payload.go, execution/evm/execution.go, execution/evm/*_test.go
Adds EnginePayloadEnvelope, custom JSON handling, raw execution-payload preservation, and updated RPC interface signatures.
Fork-aware method negotiation
execution/evm/engine_rpc_client.go, execution/evm/engine_rpc_tracing.go
Adds cached fallback across forkchoiceUpdated V3/V4, getPayload V4/V5/V6, and newPayload V4/V5, with dynamic tracing method attributes.
Execution payload integration
execution/evm/execution.go, execution/evm/execution_payload_attrs_test.go
Builds payload attributes with derived slotNumber, forwards execution requests, and submits complete payload envelopes.
RPC selection and payload validation
execution/evm/engine_rpc_client_test.go
Tests fallback ordering, request shaping, Amsterdam field preservation, request encoding, and non-fork error propagation.
Fork upgrade E2E validation
execution/evm/test/*
Exercises Prague-to-Osaka-to-Amsterdam transitions against ev-reth and verifies method versions and blockAccessList forwarding.
Engine API documentation
docs/ev-reth/engine-api.md, docs/reference/api/engine-api.md, execution/evm/README.md
Documents fork mappings, Amsterdam fields, fallback behavior, genesis configuration, and enablement checks.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Execution
  participant EngineRPCClient
  participant EngineAPI
  Execution->>EngineRPCClient: Submit forkchoice with payload attributes
  EngineRPCClient->>EngineAPI: Select forkchoiceUpdated version
  EngineAPI-->>EngineRPCClient: Return payload ID
  Execution->>EngineRPCClient: Request payload
  EngineRPCClient->>EngineAPI: Select getPayload version
  EngineAPI-->>EngineRPCClient: Return Amsterdam payload envelope
  Execution->>EngineRPCClient: Submit payload and execution requests
  EngineRPCClient->>EngineAPI: Select newPayload version
Loading

Possibly related PRs

Suggested reviewers: chatton, julienrbrt

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description only contains the template comments and lacks any actual overview or PR details. Add a real Overview section explaining the change, context, goal, rationale, and any related issue or tl;dr.
Docstring Coverage ⚠️ Warning Docstring coverage is 7.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preparation for the Amsterdam fork.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch marko/reth_2.3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @tac0turtle's task in 4m 11s —— View job


Code Review: prep for Amsterdam fork

  • Gather context (AGENTS.md, PR diff)
  • Review execution/evm/engine_payload.go (new file)
  • Review execution/evm/engine_rpc_client.go (major changes)
  • Review execution/evm/execution.go
  • Review tracing, tests, and docs
  • Post final review

Overview

This PR adds Amsterdam fork support to ev-node's Engine API layer. The approach is sound: a new EnginePayloadEnvelope wrapper preserves blockAccessList through the JSON round-trip (the critical correctness concern), the RPC client auto-detects and caches the supported method version with fallback, and the slotNumber attribute is always included but stripped for V3 calls. Test coverage for the version-selection and fallback logic is thorough.

Three issues need attention before this lands in production: one silent correctness bug (missing JSON tag), one compile-time error in the E2E test, and one missing TODO on a temporary derivation rule.


Critical

1. engine_payload.go — Missing json:"blobsBundle" tag on both marshal and unmarshal structs

In both the UnmarshalJSON and MarshalJSON local struct definitions, BlobsBundle has no JSON tag:

// UnmarshalJSON (line 28-33)
type executionPayloadEnvelope struct {
    ExecutionPayload json.RawMessage `json:"executionPayload"`
    BlockValue       *hexutil.Big    `json:"blockValue"`
    BlobsBundle      *engine.BlobsBundle          // ← missing tag!
    ...
}

// MarshalJSON (line 72-77) — same issue

Go will use the field name BlobsBundle (PascalCase) as the JSON key, but the Engine API uses "blobsBundle" (camelCase). This silently drops blob bundle data on every Engine API call — blobs will never be deserialized from EL responses, and will never be included in payloads sent to the EL.

Fix: Add `json:"blobsBundle"` to both struct definitions.

Fix this →


2. test/engine_fork_upgrade_test.go line 103 — Type mismatch in E2E test

stateRoot, err := executionClient.InitChain(...)     // stateRoot: []byte
...
for _, step := range steps {
    t.Run(step.name, func(t *testing.T) {
        nextStateRoot, err := executionClient.ExecuteTxs(..., stateRoot)
        require.NoError(t, err)
        stateRoot = nextStateRoot   // ← compile error: ExecuteResult != []byte
    })
}

InitChain returns ([]byte, error), so stateRoot is []byte. ExecuteTxs returns (execution.ExecuteResult, error) — a struct with UpdatedStateRoot []byte and NextProposerAddress []byte. The assignment stateRoot = nextStateRoot is a type error that will be caught when building with -tags evm.

Fix:

nextResult, err := executionClient.ExecuteTxs(ctx, nil, step.height, time.Unix(int64(step.timestamp), 0), stateRoot)
require.NoError(t, err)
stateRoot = nextResult.UpdatedStateRoot

Fix this →


Medium

3. execution.go line 1081 — deriveSlotNumber needs a TODO

The ams_update.md design doc explicitly calls out: "The slot number should come from an explicit chain rule … this should be agreed with the ev-node/ev-reth chain semantics before enabling the fork." There is no in-code reminder:

func (c *EngineClient) deriveSlotNumber(blockHeight uint64) uint64 {
    return blockHeight   // ← no TODO, no comment
}

A future reader won't know this is provisional. Add a comment:

// TODO: slot derivation rule must be agreed with ev-reth chain semantics
// before setting amsterdamTime in any chainspec. Currently uses blockHeight
// as a placeholder.
func (c *EngineClient) deriveSlotNumber(blockHeight uint64) uint64 {
    return blockHeight
}

4. execution.go line 142 — Unreachable return

retryWithBackoffOnPayloadStatus has a final return at line 142 that is unreachable. The loop already returns at line 131 when attempt >= maxRetries:

if attempt >= maxRetries {
    return fmt.Errorf("max retries (%d) exceeded for %s: %w", maxRetries, operation, err)  // line 131
}
// ... select/backoff ...
}
// This return is never reached:
return fmt.Errorf("max retries (%d) exceeded for %s", maxRetries, operation)  // line 142

Remove the dead return at line 142.

5. engine_rpc_client.go line 82 — Cross-version coupling should be documented

When FCU V4 succeeds, the code immediately advances payloadVersion to V6:

if version == engineForkchoiceVersionV4 {
    e.payloadVersion.Store(uint32(enginePayloadVersionV6))
}

This works for Amsterdam (FCU V4 ↔ getPayload V6) but encodes an implicit fork-version coupling. If future forks decouple these version numbers, this will silently use the wrong getPayload version before the first failure. A comment stating the invariant being relied on would help future maintainers.

6. test/test_helpers.go lines 205-227 — Duplicated decodeSecret / getAuthToken

These two functions are verbatim copies of the unexported functions in execution/evm/execution.go. Since the test package (package test) can't import unexported symbols from package evm, the duplication is currently unavoidable. But consider exposing them via an internal package or a _test.go file in package evm to avoid two implementations drifting out of sync.


Minor

7. ams_update.md at repo root

Design/planning documents in the repo root clutter the project structure and age poorly. Consider moving this to docs/ or deleting it after the PR is merged (the checklist at the bottom is now implemented).

8. rawExecutionPayloadHasField — two JSON parses for one check

hasAmsterdamFields calls rawExecutionPayloadHasField twice, each time fully unmarshaling the JSON:

return rawExecutionPayloadHasField(e.RawExecutionPayload, "slotNumber") ||
    rawExecutionPayloadHasField(e.RawExecutionPayload, "blockAccessList")

This is fine for correctness but wastes one parse when the first check fails. Consolidate into a single unmarshal that checks both keys:

func rawPayloadHasAmsterdamFields(payload json.RawMessage) bool {
    if len(payload) == 0 { return false }
    var fields map[string]json.RawMessage
    if err := json.Unmarshal(payload, &fields); err != nil { return false }
    _, hasSlot := fields["slotNumber"]
    _, hasBAL  := fields["blockAccessList"]
    return hasSlot || hasBAL
}

9. execution.gotime.After in retry loop

time.After allocates a timer that won't be reclaimed by the GC until it fires, even if the context is cancelled first. Prefer:

timer := time.NewTimer(backoff)
select {
case <-ctx.Done():
    timer.Stop()
    return ...
case <-timer.C:
    backoff *= 2
}

This is pre-existing, but it's worth fixing since the retry logic is newly refactored.


Positive Notes

  • EnginePayloadEnvelope design is correct — keeping RawExecutionPayload as json.RawMessage and only decoding what ev-node needs is exactly the right approach to avoid dropping future EL fields (blockAccessList, etc.).
  • Fallback/cache logic in engine_rpc_client.go is clean and atomic-safe. The version starts at V4 (Prague default) and only advances on actual server negotiation.
  • forkchoiceArgsForMethod correctly strips slotNumber for V3 and enforces its presence for V4, so the attributes map in execution.go can always include it without needing fork-awareness at the call site.
  • Test coverage for the version-selection FSM is excellent — Prague, Osaka, Amsterdam, fork upgrade, non-fork errors, and BAL passthrough are all covered.
  • Tracing correctly records the resolved method name after the call (post-fallback), not the attempted version.

@tac0turtle tac0turtle changed the title chore: update reth 2.3 chore: prep for Amsterdam fork Jun 11, 2026
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

The latest Buf updates on your PR. Results from workflow CI / buf-check (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped✅ passed⏩ skippedJul 15, 2026, 1:06 PM

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-15 13:37 UTC

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.32%. Comparing base (67b2dbc) to head (768db34).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3352      +/-   ##
==========================================
- Coverage   62.36%   62.32%   -0.04%     
==========================================
  Files         120      120              
  Lines       13425    13425              
==========================================
- Hits         8372     8367       -5     
- Misses       4114     4119       +5     
  Partials      939      939              
Flag Coverage Δ
combined 62.32% <ø> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tac0turtle tac0turtle marked this pull request as ready for review July 15, 2026 12:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
execution/evm/engine_rpc_client.go (1)

147-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap NewPayload errors with the selected method.

Compute the method once and include it in both preparation and RPC errors.

Proposed fix
 func (e *engineRPCClient) NewPayload(ctx context.Context, payload *EnginePayloadEnvelope, blobHashes []string, parentBeaconBlockRoot string, executionRequests [][]byte) (*engine.PayloadStatusV1, error) {
+	method := newPayloadMethod(payload)
 	payloadParam, err := payload.executionPayloadParam()
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("prepare %s payload: %w", method, err)
 	}
@@
-	err = e.client.CallContext(ctx, &result, newPayloadMethod(payload), payloadParam, blobHashes, parentBeaconBlockRoot, requests)
+	err = e.client.CallContext(ctx, &result, method, payloadParam, blobHashes, parentBeaconBlockRoot, requests)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("%s failed: %w", method, err)
 	}

As per coding guidelines, “Wrap errors with context using fmt.Errorf in Go code.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@execution/evm/engine_rpc_client.go` around lines 147 - 165, Update NewPayload
to compute the selected method from newPayloadMethod(payload) once before
payload preparation. Wrap both executionPayloadParam preparation errors and
client.CallContext RPC errors with fmt.Errorf, including the selected method as
context, while preserving the existing return behavior and avoiding a second
method computation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/reference/api/engine-api.md`:
- Around line 121-123: Update engine_getPayloadV6 response examples to include
the executionRequests field, label the final array in engine_newPayloadV5
examples, and mirror the documentation changes in
docs/reference/api/engine-api.md (121-123 and 169-175) and
docs/ev-reth/engine-api.md (140-144). In execution/evm/README.md (62-64), note
that ev-node forwards both executionPayload and executionRequests.

---

Nitpick comments:
In `@execution/evm/engine_rpc_client.go`:
- Around line 147-165: Update NewPayload to compute the selected method from
newPayloadMethod(payload) once before payload preparation. Wrap both
executionPayloadParam preparation errors and client.CallContext RPC errors with
fmt.Errorf, including the selected method as context, while preserving the
existing return behavior and avoiding a second method computation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 241c8f25-5c6d-4f62-9a12-b089aab44040

📥 Commits

Reviewing files that changed from the base of the PR and between 67b2dbc and 81c347e.

📒 Files selected for processing (13)
  • docs/ev-reth/engine-api.md
  • docs/reference/api/engine-api.md
  • execution/evm/README.md
  • execution/evm/engine_payload.go
  • execution/evm/engine_rpc_client.go
  • execution/evm/engine_rpc_client_test.go
  • execution/evm/engine_rpc_tracing.go
  • execution/evm/execution.go
  • execution/evm/execution_payload_attrs_test.go
  • execution/evm/execution_reconcile_test.go
  • execution/evm/proposer_test.go
  • execution/evm/test/engine_fork_upgrade_test.go
  • execution/evm/test/test_helpers.go

Comment on lines +121 to +123
"excessBlobGas": "0x0",
"slotNumber": "0x...",
"blockAccessList": []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n## File sizes\n'
wc -l docs/reference/api/engine-api.md docs/ev-reth/engine-api.md execution/evm/README.md

printf '\n## Relevant excerpts: docs/reference/api/engine-api.md\n'
sed -n '100,190p' docs/reference/api/engine-api.md

printf '\n## Relevant excerpts: docs/ev-reth/engine-api.md\n'
sed -n '120,170p' docs/ev-reth/engine-api.md

printf '\n## Relevant excerpts: execution/evm/README.md\n'
sed -n '40,90p' execution/evm/README.md

printf '\n## Search executionRequests across docs\n'
rg -n "executionRequests|blockAccessList|slotNumber|engine_forkchoiceUpdatedV4|engine_newPayloadV5" docs execution -S

Repository: evstack/ev-node

Length of output: 13672


🌐 Web query:

Ethereum executionRequests Engine API Prague newPayloadV5 forkchoiceUpdatedV4 executionRequests docs

💡 Result:

In the Ethereum Prague upgrade, the Engine API introduced support for execution requests, a mechanism defined by EIP-7685 that allows the execution layer to trigger protocol-level operations [1][2]. These requests are communicated between the execution layer and consensus layer through updated Engine API methods [1]. Key Engine API changes for Prague include: 1. New Method Versions: The Prague upgrade introduced engine_newPayloadV4 and engine_getPayloadV4 to support the inclusion and validation of execution requests [1][3][2]. 2. Execution Requests Parameter: In engine_newPayloadV4, an executionRequests parameter was added [1][2]. This parameter is an array of byte arrays, where each element represents a request [1][2]. Each request is formatted as a byte array beginning with a single-byte request type, followed by the specific request data [2]. 3. Validation and Ordering: Requests must be ordered by their request type in ascending order [2]. The execution layer computes a commitment to these requests (the requestsRoot), which is included in the block header and validated against the data provided via the Engine API [1][4][2]. Regarding the specific methods mentioned in your query: - newPayloadV5 and forkchoiceUpdatedV4: These represent later versions of the Engine API methods introduced in subsequent upgrades (e.g., the Amsterdam fork) to support additional features like block access lists and further protocol evolutions [5][6]. They are not the methods used for the initial Prague execution requests implementation, which utilized V4 versions [1][2]. - Documentation: Official specifications for these changes are maintained in the ethereum/execution-apis repository, specifically within the prague.md and related specification documents [7][8][6]. In summary, while Prague established the foundation for execution requests using V4 engine methods, V5 (newPayloadV5) and V4 (forkchoiceUpdatedV4) methods represent subsequent API evolutions required for later network upgrades [5][6].

Citations:


🏁 Script executed:

set -euo pipefail

printf 'docs/reference/api/engine-api.md\n'
grep -n "executionRequests\|blockAccessList\|slotNumber" -n docs/reference/api/engine-api.md || true

printf '\ndocs/ev-reth/engine-api.md\n'
grep -n "executionRequests\|blockAccessList\|slotNumber" -n docs/ev-reth/engine-api.md || true

printf '\nexecution/evm/README.md\n'
grep -n "executionRequests\|executionPayload" -n execution/evm/README.md || true

Repository: evstack/ev-node

Length of output: 1320


Document executionRequests in the Engine API docs

engine_getPayloadV6 responses omit executionRequests, and the engine_newPayloadV5 examples leave the last array unlabeled. Add the field/label in both Engine API docs and note in execution/evm/README.md that ev-node forwards both executionPayload and executionRequests.

📍 Affects 3 files
  • docs/reference/api/engine-api.md#L121-L123 (this comment)
  • docs/reference/api/engine-api.md#L169-L175
  • docs/ev-reth/engine-api.md#L140-L144
  • execution/evm/README.md#L62-L64
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/api/engine-api.md` around lines 121 - 123, Update
engine_getPayloadV6 response examples to include the executionRequests field,
label the final array in engine_newPayloadV5 examples, and mirror the
documentation changes in docs/reference/api/engine-api.md (121-123 and 169-175)
and docs/ev-reth/engine-api.md (140-144). In execution/evm/README.md (62-64),
note that ev-node forwards both executionPayload and executionRequests.

Source: MCP tools

@tac0turtle tac0turtle merged commit e437a62 into main Jul 15, 2026
33 checks passed
@tac0turtle tac0turtle deleted the marko/reth_2.3 branch July 15, 2026 13:35
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