|
| 1 | +# Review guide: in-process ethrex, scoped down |
| 2 | + |
| 3 | +What to look at, what to be suspicious of, and what is still unfinished. |
| 4 | + |
| 5 | +**Branch:** `feat/ethrex-inprocess` — one commit (`134dcb4`) off `origin/main` (`b4a8f78`). |
| 6 | +**Not pushed yet.** Nothing is force-pushed and PR #530 is untouched pending your call (§7). |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## 1. What this is |
| 11 | + |
| 12 | +Run the execution layer **in-process**: ethrex linked in as a library, driven by |
| 13 | +direct function calls. One binary, no Engine API, no JSON-RPC, no JWT. |
| 14 | + |
| 15 | +Per your scoping call, all out-of-process machinery was removed. That work still |
| 16 | +exists as PR #367, so nothing is lost — this branch simply stops overlapping it. |
| 17 | + |
| 18 | +| | Previous PR #530 | This branch | |
| 19 | +|---|---|---| |
| 20 | +| Commits | 12 (3 merges of main, plus #367 absorbed) | **1**, off current main | |
| 21 | +| Files changed | ~80 | **43** | |
| 22 | +| Engine-API code | ~790 lines (JWT, JSON-RPC client, wire test) | **0** | |
| 23 | +| EL interface | `ExecutionEngine` trait, Engine-API methods, `PayloadId`, payload cache, wire types | **3 direct methods** | |
| 24 | +| CLI | `--execution-mode` + 3 external flags | **`--el-genesis`** | |
| 25 | + |
| 26 | +Diff: 43 files, +3444 / −779. |
| 27 | + |
| 28 | +## 2. Suggested reading order |
| 29 | + |
| 30 | +Reviewing in this order means each file makes sense before you reach its callers. |
| 31 | + |
| 32 | +1. `crates/net/ethrex-engine/src/lib.rs` — **the whole EL surface**, three methods. |
| 33 | + Read this first; everything else is wiring. |
| 34 | +2. `crates/net/ethrex-engine/src/conversion.rs` — the payload ⇄ block mapping. The |
| 35 | + only genuinely fiddly code; check the field table in the guide against it. |
| 36 | +3. `crates/blockchain/src/el_integration.rs` — the four actor hooks and the |
| 37 | + never-stall-consensus policy. |
| 38 | +4. `crates/blockchain/src/lib.rs` — where those hooks attach to the tick loop |
| 39 | + (interval 0 head update, interval 4 build, gossip import). |
| 40 | +5. `bin/ethlambda/src/main.rs` — engine construction and the **genesis seeding** |
| 41 | + (§4, decision 3). |
| 42 | +6. `crates/blockchain/state_transition/src/execution_payload.rs` and the type |
| 43 | + changes — the consensus-side schema (§5). |
| 44 | +7. Everything else is test literals, docs and tooling. |
| 45 | + |
| 46 | +## 3. The claim most worth challenging |
| 47 | + |
| 48 | +**Some code that arrived via #367 stays, and it is not Engine-API code.** |
| 49 | + |
| 50 | +| Kept | Why it is required in-process | |
| 51 | +|---|---| |
| 52 | +| `ExecutionPayloadV3` in `BlockBody` | The proposer embeds the payload so **peers execute it in their own embedded EL**. Without it, no peer can replicate execution. | |
| 53 | +| `process_execution_payload` (STF) | Validates the payload's parent hash and slot timestamp on import. | |
| 54 | +| `latest_execution_payload_header` in `State` / `StateDiff` | Reconstructed states must keep the EL block-hash chain, or the parent-hash check breaks after a diff replay. | |
| 55 | +| `State::from_genesis_with_el_hash` | Seeds the consensus genesis with the EL genesis hash. | |
| 56 | + |
| 57 | +If you disagree that these belong here, that is the conversation to have — it is |
| 58 | +the one place where "only in-process changes" is a judgement call rather than a |
| 59 | +mechanical deletion. |
| 60 | + |
| 61 | +## 4. Decisions to scrutinise |
| 62 | + |
| 63 | +Each is reversible; the cost of reversing is noted. |
| 64 | + |
| 65 | +**1. Direct API instead of the `ExecutionEngine` trait.** (your D1=B) |
| 66 | +`build_payload` / `execute_payload` / `set_head`. This deleted `PayloadId`, the |
| 67 | +`Mutex<HashMap<[u8;8], _>>` payload cache, and the build-then-fetch two-step — |
| 68 | +all artefacts of the Engine API being stateless and networked. |
| 69 | +*Reversing:* reintroduce the trait, which #367 already contains. |
| 70 | + |
| 71 | +**2. No fee-recipient configuration.** ← *the one I am least sure about* |
| 72 | +#367 read `suggested_fee_recipient` from `validator-config.yaml`; main has no such |
| 73 | +plumbing. Rather than re-add config parsing for something the integration does not |
| 74 | +need, the EL is handed the zero address with a comment. Lean has no fee market or |
| 75 | +block rewards, so nothing is being directed anywhere. |
| 76 | +*Reversing:* ~20 lines — a config field, a hex parser, and one more `BlockChainConfig` field. |
| 77 | + |
| 78 | +**3. The EL genesis hash is derived, not configured.** |
| 79 | +The engine bootstraps from `--el-genesis`, so its startup head *is* the EL genesis |
| 80 | +block; `main.rs` reads it back and seeds the consensus anchor. The external path |
| 81 | +needed a flag because the EL was a separate process. |
| 82 | +*Why it matters:* forgetting this seed fails **silently** — consensus looks healthy |
| 83 | +while the EL sits frozen at genesis and every proposal falls back to a synthetic |
| 84 | +payload. Worth confirming you find the derivation trustworthy. |
| 85 | + |
| 86 | +**4. `execute_payload` is synchronous.** |
| 87 | +`Blockchain::add_block` is a sync ethrex call, so the gossip-import path no longer |
| 88 | +awaits. Simpler, but it does mean EL execution happens on the actor thread. |
| 89 | +*Consider:* whether block execution time on the actor is acceptable, or whether it |
| 90 | +should move off-thread later. |
| 91 | + |
| 92 | +**5. Single ethrex revision across the workspace.** |
| 93 | +`crates/net/p2p` was pinned to an older ethrex for ENR parsing; it now follows the |
| 94 | +workspace revision, which required porting `parse_enrs` to v15's typed |
| 95 | +`NodeRecord`. This touches a crate unrelated to the feature. |
| 96 | +*Why it is not optional:* `ethrex-crypto` bundles a C SHA3 with non-namespaced |
| 97 | +symbols, so two ethrex versions multiply-define them under GNU `ld`. macOS `ld64` |
| 98 | +tolerates it — it only fails in the Linux release build. |
| 99 | + |
| 100 | +**6. In-memory EL store.** EL state resets on restart. Fine for a PoC; persistence |
| 101 | +is an `ethrex-storage` feature away and pairs with EL-aware checkpoint sync. |
| 102 | + |
| 103 | +**7. Mock-EL test seam dropped.** (your D4) No trait means nothing to mock; the |
| 104 | +engine tests drive a real embedded ethrex instead. |
| 105 | + |
| 106 | +## 5. Consensus-path changes to check carefully |
| 107 | + |
| 108 | +These touch the tick loop, so they deserve more attention than the rest: |
| 109 | + |
| 110 | +- **Interval 4** — `build_execution_payload` runs inline, immediately before the |
| 111 | + block is assembled. Failure returns `None` and `build_block` falls back to |
| 112 | + `synthetic_payload`. |
| 113 | +- **Interval 0** — `notify_execution_layer` updates the EL head, spawned |
| 114 | + fire-and-forget. |
| 115 | +- **Gossip import** — `import_gossiped_block` executes the payload *before* the |
| 116 | + store sees the block. A rejection drops the block; anything else proceeds. |
| 117 | +- **Own block** — after building, we execute our own payload, because nobody |
| 118 | + gossips it back to us and the EL head would otherwise never advance. |
| 119 | + |
| 120 | +The invariant throughout: **the execution layer never stalls consensus.** Only an |
| 121 | +explicit rejection of a received payload drops a block; every other failure logs |
| 122 | +and continues. |
| 123 | + |
| 124 | +## 6. Verification status |
| 125 | + |
| 126 | +| Check | Status | |
| 127 | +|---|---| |
| 128 | +| `cargo build --workspace` | ✅ clean | |
| 129 | +| `cargo clippy --workspace --all-targets -- -D warnings` | ✅ clean | |
| 130 | +| `cargo fmt --all --check` | ✅ clean | |
| 131 | +| Tests (blockchain, state-transition, engine, bin, p2p) | ✅ **299 passed, 0 failed** | |
| 132 | +| Engine roundtrip + beacon-root rejection tests | ✅ pass | |
| 133 | +| 3-node devnet **with** the embedded EL | ✅ finalized at slot 40 | |
| 134 | +| 3-node devnet **without** the EL (control) | ✅ finalized at slot 41 | |
| 135 | + |
| 136 | +The EL-enabled run matches the consensus-only control, so the execution layer |
| 137 | +costs nothing in liveness. All three nodes agreed on the same finalized root, and |
| 138 | +each executed exactly 43 payloads — lockstep. |
| 139 | + |
| 140 | +### 6.1 Bug found by the devnet and fixed: `parent_hash mismatch` |
| 141 | + |
| 142 | +Worth reading, because it is the one real defect the scope-down introduced and no |
| 143 | +unit test could have caught it — it only appears with **multiple independent |
| 144 | +execution layers**. |
| 145 | + |
| 146 | +**Symptom.** With the EL enabled the chain never finalized: 22 `parent_hash |
| 147 | +mismatch` errors, peer imports halved (13 vs 30), no aggregation coverage, no |
| 148 | +finality. Silent from the proposer's side — zero EL rejections, zero warnings. |
| 149 | +The block simply did not stick anywhere. |
| 150 | + |
| 151 | +**Cause.** The state transition requires |
| 152 | + |
| 153 | +``` |
| 154 | +payload.parent_hash == state.latest_execution_payload_header.block_hash |
| 155 | +``` |
| 156 | + |
| 157 | +— the parent the *consensus chain* expects. `build_payload` instead derived the |
| 158 | +parent from `store.get_latest_canonical_block_hash()`, this node's *own* EL head. |
| 159 | +With three independent ELs those drift apart, so a proposer's payload named the |
| 160 | +wrong parent and every node's STF rejected the block. |
| 161 | + |
| 162 | +#367 did not have this bug: its build-mode `forkchoiceUpdated` pointed the EL at |
| 163 | +`el_hash_at(store.head())` before building. Collapsing that two-step into one call |
| 164 | +dropped the step that set the parent. |
| 165 | + |
| 166 | +**Fix.** `build_payload` takes `parent_el_hash` explicitly; `el_integration` passes |
| 167 | +`el_hash_at(head_root)` — the consensus head's payload hash — and the engine |
| 168 | +re-points the EL at that block before building. safe/finalized are deliberately |
| 169 | +left unset there: pinning them would forbid a later build on an earlier block. |
| 170 | + |
| 171 | +**Regression test.** `builds_on_the_requested_parent_not_the_el_head` advances the |
| 172 | +EL two blocks, then asks for a payload extending block 1 and asserts the payload |
| 173 | +names *that* parent. It fails against the old code. |
| 174 | + |
| 175 | +**Method note.** The first hypothesis — that `import_gossiped_block` was dropping |
| 176 | +blocks on EL rejection — was wrong, and the logs disproved it (zero rejections) |
| 177 | +before any code was changed. |
| 178 | + |
| 179 | +## 7. Open questions for you |
| 180 | + |
| 181 | +1. **Publishing.** Force-push this onto `feat/ethrex-inprocess-poc` (keeps PR #530 |
| 182 | + and its discussion) or push `feat/ethrex-inprocess` as a new PR and close #530? |
| 183 | + Force-push rewrites the remote branch, so it needs your say-so. |
| 184 | +2. **Decision 2** — fee-recipient config: leave dropped, or restore it? |
| 185 | +3. **Decision 4** — EL execution on the actor thread: acceptable for now? |
| 186 | +4. Anything in §3 you think should not be in this PR. |
| 187 | + |
| 188 | +## 8. Known remaining work |
| 189 | + |
| 190 | +- The two published artifacts still describe the trait / two-mode design and need |
| 191 | + updating once the code settles. |
| 192 | +- `docs/plans/scope-down-to-inprocess.md` (the proposal) and this file can both be |
| 193 | + dropped from the PR if you would rather not carry planning docs. |
| 194 | +- Prague / `ExecutionPayloadV4` support is out of scope; the EL genesis must be |
| 195 | + Cancun. |
0 commit comments