Skip to content

feat: upgrade to reth v2.4.1 - #147

Open
sprites0 wants to merge 5 commits into
node-builderfrom
feat/reth-v2.4.1
Open

feat: upgrade to reth v2.4.1#147
sprites0 wants to merge 5 commits into
node-builderfrom
feat/reth-v2.4.1

Conversation

@sprites0

@sprites0 sprites0 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Ports nanoreth from its 1.8.2-based fork to one based on upstream v2.4.1 — 3,127 upstream commits later.

The fork

hl-archive-node/reth@v2.4.1-fork, pinned at 5c43bf8cdc362fad776cdd7040dcabac67c648e8, four commits on top of the upstream tag:

Commit What
c8db7d76d hl: drop state root verification — unconditional, since we don't support chains with non-zero state roots
cbdf9d7ee fix(db): do not panic on tables outside the default TableSet — upstream bug; nanoreth's BlockReadPrecompileCalls tripped it
70c06137c hl: pin revmc — revmc main has moved to a build against a newer revm and no longer compiles against revm 41
5c43bf8cd feat(evm): re-export revm_evm::JitEvm — lets nanoreth wrap its own EVM without a direct revmc dependency

The last two are generic and would be reasonable upstream PRs.

Dependency moves

alloy 1.0.37 → 2.1.1/2.2.0 · revm 29 → 41 · alloy-evm 0.21 → 0.37 · vergen 9 → 10. Storage is now storage_v2, so state lives in static files and RocksDB rather than MDBX tables.

Fixes found on the way

  • Legacy transactions stopped round-tripping through msgpack. alloy 2.x serializes TxLegacy::gas_price as U128 but deserializes it through U256, so S3 blocks failed to decode with invalid length 16, expected 256 bits. WireTxLegacy mirrors the wire shape; two regression tests cover it. Confirmed a regression by decoding the same blocks with the pre-upgrade binary.
  • The pseudo peer never connected. local_node_record() returns the NAT-resolved address even with disable_nat(), so the node dialled a public IPv6 address instead of the in-process peer and sat at connected_peers=0. It now dials local_addr().
  • --debug-cutoff-height was half-honoured — the block poller respected it, the forkchoice trigger still used the real tip.

New: opt-in JIT

revmc JIT for HL execution, opt-in at both build and run time, matching reth's posture. Build with --features jit, then start with --hl.jit true; a build without the feature ignores the flag. Also --hl.jit.hot-threshold and --hl.jit.worker-count. A plain build drops revmc and its LLVM 22 requirement from the graph entirely.

JIT is disabled below block 243,538 on mainnet, where BLOCKHASH goes through a patched instruction handler that JIT-compiled code would bypass.

Validation

  • 5,000,000 mainnet blocks from genesis, all 13 stages, zero errors. Block 5,000,000 hashes 0x917a0420060727e291a569c6de3c4688d2e0ed3a7f737590448b477bc973ffc4, matching the forkchoice trigger.
  • JIT equivalence over 1,000,000 blocks: execution 593.1s with JIT vs 903.1s without (1.52×), and 14 table checksums byte-identical — headers, transactions, receipts, transaction-senders, both change-set segments, the three RocksDB history tables, hashed accounts/storages, both tries, and bytecodes. (PlainAccountState/PlainStorageState hash zero entries under storage_v2 and are excluded rather than counted as passes.)
  • 21 tests pass; cargo check is warning-clean.
  • Docker image builds and runs, reporting the real commit SHA and jemalloc.

Docker

The jit feature needs LLVM 22.1, newer than the base image carries, so image builds leave it off unless you pass --build-arg ENABLE_JIT=true with a suitable base. Added a .dockerignore allowlist — the build context was 1.85GB — and cache mounts for target and the cargo registry, since cooking reth's dependency tree wrote a single 74GB layer under lto = "thin".

Ports nanoreth from the 1.8.2-based fork to one based on upstream v2.4.1,
3,127 upstream commits later. The fork is hl-archive-node/reth v2.4.1-fork,
carrying four commits: the HL state-root drop, a metrics panic fix, the revmc
pin, and a `JitEvm` re-export.

Dependency moves that drove most of the diff: alloy 1.0.37 -> 2.1.1/2.2.0,
revm 29 -> 41, alloy-evm 0.21 -> 0.37, vergen 9 -> 10. Storage is now
storage_v2, so state lives in static files and RocksDB rather than MDBX tables.

Notable fixes along the way:

* Legacy transactions no longer round-trip through msgpack. alloy 2.x
  serializes `TxLegacy::gas_price` as `U128` but deserializes it through
  `U256`, so S3 blocks failed to decode with "invalid length 16, expected 256
  bits". `WireTxLegacy` mirrors the wire shape and is covered by two tests.
* The pseudo peer never connected: `local_node_record()` returns the
  NAT-resolved address even with `disable_nat()`, so the node dialled a public
  IPv6 address instead of the in-process peer. It now dials `local_addr()`.
* `--debug-cutoff-height` was honoured by the block poller but not by the
  forkchoice trigger, which still used the real tip.

New: a runtime-configurable revmc JIT for HL execution (`--hl.jit`, default on,
with `--hl.jit.hot-threshold` and `--hl.jit.worker-count`). Measured over
1,000,000 mainnet blocks from genesis, execution runs 593s with the JIT against
903s without it, and 14 table checksums are identical across both.

Validated by syncing 5,000,000 mainnet blocks from genesis through all 13
stages with no errors.

The revm inspector selfdestruct backport carries over: revm 41 does not have
the fix (it landed upstream in 42, and no released alloy-evm or revm-inspectors
supports 42 yet), so the patch is rebased onto the 41.0.0 series as
hl-archive-node/revm@ed244b83. Its regression test fails without the handler
change and passes with it.
sprites0 added 4 commits July 26, 2026 08:58
`EvmTr`'s ctx, ctx_ref, ctx_instructions, ctx_precompiles and frame_stack all
have `#[inline]` defaults that project out of `all()`/`all_mut()`, which this
impl already overrides with the field references. The five hand-written
accessors returned exactly what those projections produce.

`to_range` is now `pub` in reth-provider and byte-identical to the copy here.

Verified by resyncing 1,000,000 mainnet blocks from genesis: all 14 table
checksums are unchanged, covering headers, transactions, receipts, senders,
both change-set segments, the three RocksDB history tables, hashed state,
both tries and bytecodes.

Left alone, despite also having defaults: `Encodable::length` (the default
allocates and encodes just to count bytes), `recover_unchecked_with_buf` (the
default discards the caller's buffer, costing an allocation per transaction
during batch sender recovery) and `send_raw_transaction` (the default decodes
before reaching the unreachable stub).
Every job failed at `llvm-sys`: the `jit` feature is on by default and needs
LLVM 22 to link, which the runners do not carry. Install it the way reth does,
with the same `install_llvm_ubuntu.sh` script, rather than dropping the feature
from CI -- the JIT path is default-on and should not go untested.

The release build had the same problem waiting for the next tag: it passes
`--features` without `--no-default-features`, so `jit` was already in its graph.
revmc is pinned with `llvm-prefer-static`, so the released binary stays
self-contained.

Also fixes the redundant closure the clippy job would have failed on once it
got far enough to run.
udeps flags it, and it is genuinely unused: nothing in src, tests or build.rs
has ever referenced it, on this branch or on node-builder. Only tikv-jemallocator
is used, for the global allocator in main.rs.

It has been declared since the first commit; what changed is that the job
installs cargo-udeps unpinned, so a newer version now catches it.
Matches reth's posture more closely. reth ships `jit` in the binary's default
features but leaves the runtime flag off, opting in with `--jit` or the
`reth_jit` RPC method. This goes one step further and drops the feature from
the default set too, so a plain `cargo build` needs no LLVM 22 at all.

Both gates must now be open: build with `--features jit`, then start the node
with `--hl.jit true`. A build without the feature ignores the flag.

Knock-on changes:

* The Dockerfile no longer needs `--no-default-features --features jemalloc`.
  That existed only because dropping `jit` also dropped `jemalloc`; with `jit`
  out of the defaults it is just `--features jit` when ENABLE_JIT=true.
* release.yml no longer installs LLVM. Its feature set does not pull revmc any
  more, confirmed with `cargo tree -i revmc`.
* Cargo Test now runs `--features jit`. Without this the feature would ship
  untested, since a plain `cargo test` no longer compiles it. Cargo Check keeps
  the default set, so both configurations stay covered.

Verified: default, jit, and release feature sets all compile, and the suite
passes under `--features jit`.
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