execution, node: test RPC state across unwind phases - #23543
Conversation
There was a problem hiding this comment.
Pull request overview
Adds deterministic node-level coverage for RPC state-cache correctness across forkchoice unwind phases.
Changes:
- Adds synchronous state-transition observers and test wiring.
- Tests RPC reads across overlay, commit, database fallback, and stale-view completion.
- Ensures shutdown fully drains detached execution work.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
node/eth/backend.go |
Wires observers, cache decorators, and execution draining. |
execution/execmodule/state_transition.go |
Defines lifecycle observation points. |
execution/execmodule/forkchoice.go |
Emits observations at unwind and publication boundaries. |
execution/execmodule/execmoduletester/exec_module_tester.go |
Exposes observer configuration to tests. |
execution/execmodule/execmoduletester/exec_module_tester_test.go |
Verifies lifecycle observations. |
execution/execmodule/exec_module.go |
Adds construction options, cache constructor, and draining. |
execution/execmodule/exec_module_internal_test.go |
Tests execution draining. |
execution/engineapi/engineapitester/state_cache.go |
Observes RPC cache-view binding. |
execution/engineapi/engineapitester/state_cache_test.go |
Verifies synchronous view observation. |
execution/engineapi/engineapitester/engine_api_tester.go |
Wires instrumentation into full-node tests. |
execution/engineapi/engine_api_state_churn_reorg_test.go |
Extracts the shared churn helper. |
execution/engineapi/engine_api_rpc_unwind_test.go |
Adds the end-to-end unwind regression test. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Code review of the current head. Nice test — the lifecycle barriers are much better than sleeps. Findings below, ordered by severity. 1.
|
|
@AskAlexSharov Thanks for the detailed review. I pushed 2510fc0 with the test hardening that I agree is needed:
I do not think the remaining points require changes in this PR:
Validation on the updated head: 10 consecutive normal runs, 3 consecutive race-detector runs, repeated clean lint, and successful |
Part of #21860. Complements the lower-level cache tests in #23005 with running-node coverage for the RPC/unwind failure class in #22463.
Why
The missing safety net was the complete interaction between HTTP RPC requests and an Engine API reorg:
latestrequests observe B through overlay publication, durable commit, and database fallback;StateCachewith dead-fork data.Timing sleeps cannot define these windows reliably, so the test pauses execution at synchronous lifecycle boundaries.
Scenario
sequenceDiagram participant CL as Mock CL participant FCU as ExecModule FCU participant OldRPC as RPC views bound to A participant Overlay as SharedDomains overlay participant DB as MDBX participant NewRPC as newly bound RPC CL->>FCU: forkchoiceUpdated(A) FCU->>Overlay: publish A OldRPC->>OldRPC: bind tagged views and pause before reading FCU->>DB: commit A FCU->>Overlay: clear A CL->>FCU: forkchoiceUpdated(B) FCU->>FCU: unwind to B Note over FCU,DB: committed A remains visible until B is published FCU->>Overlay: publish B NewRPC->>Overlay: assert B before commit FCU->>DB: commit B NewRPC->>Overlay: assert B after commit FCU->>Overlay: clear B NewRPC->>DB: assert durable B OldRPC-->>OldRPC: finish from the old A views CL->>FCU: forkchoiceUpdated(C) FCU->>Overlay: publish transaction-free C NewRPC->>Overlay: repeat canonical reads through C Note over NewRPC,Overlay: untouched keys probe StateCache for stale A refill FCU->>Overlay: clear CThe B overlay publication is the visibility switch. Before it, newly bound requests see committed A; from publication onward, they must consistently see B. The final C step is an explicitly transaction-free child of B, built through
testing_buildBlockV1with a non-nil empty transaction list. This bypasses the txpool, so unwound transactions cannot enter C and its fresh overlay cannot answer the probed keys from its own writes. Repeated reads therefore exercise the sharedStateCacheand expose any stale A refill.Assertions
Each lifecycle assertion uses a separate persistent
StateChurncontract, so an earlier RPC cache fill cannot mask a later path. Their selected slot is absent at B and written only on A, exercising removal of dead-fork storage while preserving the account. A fifth contract exists only on A, exercising complete account and code removal. The generated A storage value is required to be non-zero, so the distinction cannot silently collapse. Non-zero value restoration remains covered by the broader StateChurn suite and #23005.Design and production impact
StateTransitionObserverexposes five inline boundaries: RPC view bound, unwind complete, overlay published, commit complete, and overlay cleared.engineapitestercache decorator, leaving the productionCache.Viewpath unchanged.eth_getStorageAt,eth_getCode, andeth_getTransactionCountcalls and real Engine API forkchoice updates. Channels establish ordering; timeouts only bound failures.testingRPC namespace is enabled only for thisengineapitesterfixture so C can bypass the txpool; production RPC configuration is unchanged.latest-nonce workaround is removed; rpc/rpchelper: invalidate cached pending block once the chain moves past it #22326 now supplies the normal pending-nonce behavior.Relationship and scope
#23005 tests cache admission and commit/publication interleavings directly. This PR tests the surrounding HTTP RPC, Engine API, overlay, commit, and fallback wiring. It intentionally does not duplicate every internal midpoint.
Coverage is a deterministic in-process full node. Crash windows, randomized reorg schedules, real-CL/Kurtosis runs, and the exact DB-commit/cache-publication midpoint remain separate #21860 layers.
Review order
execution/execmodule/state_transition.goandforkchoice.go: boundary semantics and placement.node/eth/backend.goandexecution/engineapi/engineapitester: full-node wiring, test-only RPC/cache hooks, empty-payload construction, and shutdown draining.execution/engineapi/engine_api_rpc_unwind_test.go: reorg scenario and cache-refill assertions.engine_api_state_churn_reorg_test.go: shared churn helper and pending-nonce path.