| Series | fukuii engineering |
| Audience | Node operators, client engineers, Scala developers, anyone consuming fukuii’s libraries |
| Tags | fukuii, ethereum-classic, ethereum, snap-sync, scala-3, release |
| Reading time | ~6 minutes |
Where we are
fukuii is a Scala 3 execution client that runs two independent chain families from one codebase. On Ethereum Classic (mainnet and the Mordor testnet) it is a proof-of-work client: Ethash, the ECIP-1017 fixed-supply emission schedule, and hard forks activated by block number. On Ethereum (mainnet, Sepolia and test networks) it is a post-merge proof-of-stake execution client driven by a consensus-layer client over the Engine API, with forks activated by timestamp. It began as a fork of IOHK’s Mantis.
This month we finished refactoring the SNAP sync code (spec 016) and started moving the build to the Scala 3.9 LTS line (spec 020). That move is why the next release is 0.10.0 rather than another patch.
Spec 016: SNAP sync, split into phases
SNAP lets a fresh node fetch the state trie from peers (account ranges, storage ranges, bytecode) and then "heal" the nodes that changed during the download. On ETH mainnet that can take weeks, so the code must be correct and restart-safe.
When spec 016 was written, SNAPSyncController.scala was 6,646 lines, and its four coordinators (account range, storage range, bytecode, and trie-node healing) added roughly another 8,200. One syncing behaviour served five phases, so a fix to healing was made inside code every other phase also ran through.
The split took that apart in small, separately reviewed PRs:
- Phase modules. The controller’s work moved into modules under
snap/controller/. Each coordinator’s phase work, such as peer dispatch, done-markers, frontier persistence and heal verification, moved into files undersnap/actors/{account,storage,bytecode,healing}/. The controller file is now 1,872 lines. - Moves that are provably moves. Most slices were byte-for-byte relocations. A checker,
scripts/snap-split/verify.py, ran on every PR to confirm that moved code arrived unchanged and that storage references never rose without a recorded justification. Golden-byte tests pinned every persisted format. - A narrow storage boundary. The coordinators no longer take raw database handles. Instead they go through
StateIngest, a capability that names what SNAP may write. Under the current counting rules, the number of storage references in the SNAP tree fell from 635 to 544. - One shared response window. Three coordinators each had an identical copy of the logic that sizes peer requests. Those copies are now one
AdaptiveResponseBytesclass (#1620).
We also made a scope call. The spec capped each coordinator file at about 1,000 lines, and three still exceed it: healing at 1,532, storage at 1,471 and account at 1,147. Rather than add more slices for size alone, we amended the criterion on 2026-10-17 to accept the as-built sizes. The phase work already lives in modules a reviewer can open on their own.
The slices that touch persistence were checked on a live Mordor node (ETC, Hash scheme), with results posted on the PRs. At the storage done-marker slice (#1602), the node was restarted mid-account and mid-storage, and both times it resumed from its checkpoint, skipping work it had already finished. At the healing slices (#1613 and #1619), it was restarted mid-heal. Both times the persisted frontier was empty, so the node fell back to rebuilding it by walking the full state, and converged. In the final run (#1619), recovery resumed at the same pivot and skipped the completed storage and bytecode phases. SNAP then completed at block 17,206,665 with zero error lines after the restart.
Beyond tidiness, StateIngest is the first half of a typed split between code that ingests state and code that accesses it during execution. The second half, StateAccess, underpins spec 017’s execution witnesses.
Along the way: rpc-compat grew, and we kept up
Meanwhile the upstream hive rpc-compat suite grew from 257 to 273 tests. Instead of pinning the old suite, #1616 added eth_getHeaderByNumber and eth_getHeaderByHash and made eth_simulateV1 reject unknown override keys. The gate passed 273 of 273.
Now: spec 020, moving to Scala 3.9 LTS
Scala 3.9.0, released 2026-09-03, opens a new Long Term Support line with at least three years of maintenance. The 3.3 line that fukuii pins stays maintained for only about a year after that. The plan is Scala 3.3.8 → 3.9.0 and sbt 1.10.7 → 1.13.0, with no behaviour change. We are doing it before spec 017 so that the next specs are written once, against one compiler.
Before planning, we ran an inventory spike (GT-1) against the real tree. It found that the rewrite chain must run on the 3.9 compiler, and that eight small hand fixes are needed first. Only two rewrite steps change source: 3.4 touches 16 files, and 3.7 inserts using at 514 sites. Scala’s change to given prioritisation flags 371 sites (from the 3.6 step), 364 of them from one duplicated-given pattern in the RLP codecs, so the fix goes at the definitions. The migrated tree has 91 compiler warnings, down from 365 today.
-source:3.9, not -source:future. On 3.9, -source:future also makes every remaining implicit an error (about 1,541 sites) and enforces the new control syntax, which would turn a toolchain move into a language rewrite. We use -source:3.9 instead. A scalafix rule plus a CI grep step keeps enforcing import x.*, which future used to do.
One step per commit, and consensus checked by differential. Each -source:3.N-migration rewrite is its own commit, never hand-edited in the same commit. For consensus safety, a frozen datadir imports the same fixed block range with a 3.3-built jar and a 3.9-built jar. Every block must validate against its canonical header’s state root, receipts root and gas used, and the final roots must match byte for byte across the two jars. The same runs hold block-import p50 and p99 within 10% of the 3.3 baseline. Mordor covers ETC, and Ephemery (#1628) covers ETH.
New: Ephemery support
Ephemery is a public Ethereum testnet that resets to a fresh genesis every 28 days. Each iteration N is a new chain with chain ID 39438000 + N. fukuii hard-codes no iteration. Instead:
scripts/ephemery/fetch-genesis.shdownloads the iteration’s release and generates the chain config and genesis that the existingcustom-chains-dirloader reads, with a per-iteration datadir.- The script re-encodes fields our loader reads as hex. Ephemery publishes the genesis timestamp as a decimal string, and loaded verbatim that silently produces a genesis hash no peer would accept. The script also refuses releases that declare anything fukuii cannot represent.
EphemeryGenesisSpecbuilds the iteration-164 genesis from the script’s output. It checks the published genesis hash and the live network’s fork IDs. A datadir holding another iteration’s genesis is refused at startup.
One known limitation: blob parameters are still hard-coded to the mainnet/Sepolia schedule, which differs from Ephemery’s. A block that hit the blob target would stop import loudly on an excessBlobGas mismatch rather than diverge silently. No block sampled on Ephemery this month carried blobs. The real fix, a per-chain blob schedule, is consensus work with its own plan. ETC configuration is untouched.
What’s next after 020
The spec order of record is 016 → 020 → {017 ∥ 021} → 018 → 019, with one exception: typed-transaction parity is underway now, ahead of 020’s compiler step.
- Typed transactions on every interface (#1629), underway now. We moved this ahead of spec 020’s compiler step: operators need fukuii at capability parity with Ethereum mainnet clients to trust it. Today
eth_sendTransactionbuilds only legacy transactions. Every JSON-RPC method that takes or returns a transaction, plus GraphQL and keystore signing, will handle legacy, EIP-2930, EIP-1559, EIP-4844 and EIP-7702 types as geth does. Local submissions also get the same admission checks as gossiped transactions. That makes every fukuii interface Ethereum-mainnet compliant. ETC behaviour is confirmed separately, because ETC has no EIP-1559. - Spec 017, state capabilities and execution witnesses. Adds
StateAccessand execution witnesses (the execution-specsExecutionWitnessformat, served overdebug_executionWitness). - Spec 021, fixed-width
UInt256. Replaces theBigInt-backed EVM word with an allocation-light fixed-width type that gives byte-identical results. It runs in parallel with 017. - Spec 018, storage architecture. Reworks storage beneath the two capability gateways for much larger blocks, choosing the engine by a measured bake-off. ETC is included.
- Spec 019, a zkVM guest program. Stateless block validation, gated on a feasibility spike.
Release 0.10.0: what to expect
When the last spec 020 PR merges, staging gets a manual minor bump to 0.10.0 before promotion to main.
For node operators: node behaviour, configuration, RPC and the database format are unchanged.
For library consumers: this release is breaking. The com.chipprbots modules fukuii-bytes, fukuii-crypto, fukuii-rlp, fukuii-scalanet and fukuii-scalanet-discovery move to Scala 3.9 together and carry Scala 3.9 TASTy, which Scala 3.3 projects cannot consume. Rebuild on Scala 3.9 or later. No workflow currently runs sbt publish, so in practice this affects anyone who builds the modules from source. rlp‘s duplicate anonymous given codecs are also removed. Use the named xEncDec givens or RLPImplicits.{given, *}.
We aren’t committing to a date. 0.10.0 ships when the differential, the test tiers and hive say it is ready.
Follow along
Development happens in the open at github.com/chippr-robotics/fukuii. The full reasoning is in specs/016-snap-controller-split/ and specs/020-scala-3-9-lts-migration/, and the Ephemery runbook is at docs/runbooks/ephemery.md. Issues and PRs are the best place for questions.

