| name | state-actor |
| description | Use this skill when a user wants to generate, boot, verify, or extend a state-actor-produced Ethereum database. Covers --client, --spec, --target-size, per-client boot recipes (geth / reth / besu / nethermind / ethrex / erigon), and the canonical 29-entity spec fixture. |
SKILL.md — how to use state-actor
This is the canonical "how to use this lib" doc for agents. The root AGENTS.md is a pointer that points here.
state-actor generates client-ready Ethereum databases for geth, reth, besu, nethermind, ethrex, and erigon without going through each client's init path. You point it at a --db path, choose a --client, optionally pass a --spec declaring concrete entities, and it writes a database the client can boot against directly.
Read these first
Read these in this order. The first one is load-bearing — read it before you read any prose about specs.
examples/full-matrix-spec-feature.yaml — the canonical syntax reference for --spec. CI-pinned, 29 entities, every feature. Read this file before reading any other doc about specs; see the Canonical spec reference section below for an intent → entity-# index.
SPEC.md — the schema reference (parser rules, validation errors, address-resolution algorithm, approximate_size_bytes semantics). Read alongside the fixture.
RUNBOOK.md — per-client boot recipes (geth / reth / besu / nethermind / ethrex / erigon).
ARCHITECTURE.md — internal architecture; cross-client determinism; per-client writer differences.
Three load-bearing flags
| Flag | What it controls |
|---|
--client | Which client format to write (geth, reth, besu, nethermind, ethrex, erigon) |
--spec | Path to a YAML file declaring concrete entities (EOAs, contracts, ERC-20s, EIP-7702 delegations) |
--target-size | Advisory budget that sizes the auto-fill of synthetic state. Not a hard on-disk cap — actual size may vary per client. |
Everything else has a sane default. Run state-actor --help for the full list (22 flags).
Canonical spec reference
The single source of truth for what a --spec YAML can express is examples/full-matrix-spec-feature.yaml. Read that file before reading anything else about specs. It is exhaustive (29 entities, every feature), self-documenting (six section banners + per-entity comments), and CI keeps it correct.
The CI guarantees that hold this file as the canonical reference:
The fixture is organised into six labelled sections; each one pins a feature cluster:
| Section banner (in the YAML) | Entities | Feature pinned |
|---|
1. Spamoor sender | 1 | Anvil dev-account-0 EOA with high balance |
2-7. ERC-20 flavor coverage | 2-7 | All three address modes × bulk-fill × storage-bloat × nonce override × skeleton |
8-9. Explicit-owners + allowances coverage | 8-9 | Granular owners list, explicit + bulk allowances |
10-12. Raw bytecode flavors | 10-12 | kind: contract + code: (no template); large code; storage synth |
13-15. EIP-7702 EOA flavors | 13-15 | 0xef0100<delegate> delegation marker × address modes × storage bloat |
16-22. Plain EOA flavors | 16-22 | All address modes, zero balance, hex-form balance, default-nonce, storage on EOA |
Intent → entity-# index
| To declare … | Look at fixture entity # | Section |
|---|
| Plain EOA at explicit address | 1, 16 | Spamoor sender / Plain EOA flavors |
| Plain EOA, name-derived address | 17 | Plain EOA flavors |
| Plain EOA, position-derived address | 18 | Plain EOA flavors |
| Zero-balance or hex-form balance EOA | 20, 21 | Plain EOA flavors |
| Plain EOA with synthesized storage | 19 | Plain EOA flavors |
| EIP-7702 delegating EOA | 13, 14, 15 | EIP-7702 EOA flavors |
| EIP-7702 EOA + storage bloat | 14 (bloated-validator) | EIP-7702 EOA flavors |
| ERC-20 at explicit address with bulk fill | 2 | ERC-20 flavor coverage |
| ERC-20 name-derived | 3 | ERC-20 flavor coverage |
| ERC-20 position-derived | 4 | ERC-20 flavor coverage |
| ERC-20 skeleton (no holders) | 5 | ERC-20 flavor coverage |
ERC-20 with explicit nonce override | 6 | ERC-20 flavor coverage |
ERC-20 with approximate_size_bytes (template + bloat) | 7 | ERC-20 flavor coverage |
ERC-20 with explicit owners and allowances | 8 | Explicit-owners + allowances |
| ERC-20 explicit + bulk combined | 9 | Explicit-owners + allowances |
| Raw bytecode contract | 10, 11, 12 | Raw bytecode flavors |
| Plain EOA with omitted-nonce default (0) | 22 | Plain EOA flavors |
How to adapt the fixture to a new spec
- Find the entity that matches your intent using the index above; jump to that entity in
examples/full-matrix-spec-feature.yaml.
- Copy the entity block into your new YAML.
- Edit the load-bearing fields for your case — addresses, balances, template parameters,
approximate_size_bytes. The field-by-field cheatsheet in examples/README.md lists exactly which fields control what and what the constraints are (e.g. decimals must be 18 for the erc20 template).
- Address mode: explicit (
address: is set), name-derived (name: only), or position-derived (neither). The mode determines stability of the resulting address; see SPEC.md § Address resolution for the algorithm.
- Omit
--target-size when running --spec alone — that way no auto-fill runs on top, eliminating the collision risk between random EOAs and spec-derived addresses. Set --target-size only when you intentionally want auto-fill to pad the headroom.
- Verify the entities landed at the addresses you expect:
cast code 0x<derived-address> returns the bytecode (contracts) or 0x plus the delegation marker (7702 EOAs); cast balance 0x<address> returns the spec's balance.
For the schema-level details the fixture's per-entity comments don't cover — parser rules, validation errors, the address-derivation algorithm, approximate_size_bytes resolution semantics — read SPEC.md. It's the schema reference; the fixture is the syntax reference; you typically need both. Internal references: internal/spec/doc.go (parser + validator), internal/specbuild/doc.go (entity resolution), internal/templates/doc.go (template registry).
Common tasks
Generate a geth DB
go run . --db=/tmp/sa-geth/geth/chaindata --client=geth --target-size=100MB
Auto-fill emits mainnet-shaped state (20 % account-trie / 10 % bytecode / 70 % contract storage) up to the --target-size cap, with a deterministic seed. Output is a Pebble database geth can boot with --db.engine=pebble. The /geth/chaindata suffix is mandatory — geth appends it to --datadir.
See RUNBOOK.md#geth for the boot command, and client/geth/doc.go for the on-disk layout and writer details.
Generate a DB for a specific client
go run . --db=/tmp/sa-reth --client=reth --target-size=100MB
go run . --db=/tmp/sa-besu --client=besu --target-size=100MB
go run . --db=/tmp/sa-neth --client=nethermind --target-size=100MB
go run . --db=/tmp/sa-ethrex --client=ethrex --target-size=100MB
go run . --db=/tmp/sa-erigon --client=erigon --target-size=100MB
besu, nethermind, reth, ethrex, and erigon require cgo (RocksDB / MDBX bindings). On macOS, build via Docker — the repo ships per-client Dockerfiles. The per-client on-disk layout and pinned upstream version live in client/reth/doc.go, client/besu/doc.go, client/nethermind/doc.go, client/ethrex/doc.go, client/erigon/doc.go.
Boot a generated DB on a client
Recipes (state-actor invocation + docker boot command + verification) live in RUNBOOK.md, one section per client.
Verify a generated DB
Recommended: run the per-client end-to-end suite. The Go oracle exercises CheckChainID, CheckCanonicalSyscontracts, CheckChainAdvanced, CheckBeaconRootsRingBuffer, and CheckInjections (see internal/e2e_testing/):
go test ./client/geth/...
Manual: boot the client per RUNBOOK.md, then:
cast chain-id --rpc-url http://localhost:8545
cast balance 0x<a-known-spec-address> --rpc-url ...
cast code 0x<a-known-spec-contract> --rpc-url ...
Reproduce a run from its manifest
Every run drops a state-actor-manifest.json at the datadir root (two levels up from --db for geth; the --db dir itself for the other clients) recording everything needed to reproduce it: the resolved flags — note the resolved seed (--seed=0 expands to a wall-clock seed) and the resolved fork (empty --fork resolves to the client's max) — the build version + git revision, the run's state root, and, when --spec is used, a content-addressed state-actor-spec-<sha256>.yaml sidecar written alongside it.
Re-run it into a fresh directory with the reproduce subcommand:
go run . reproduce --manifest /tmp/sa-geth/state-actor-manifest.json --db /tmp/sa-geth-repro/geth/chaindata
It replays the manifest's resolved flags (reading any spec from the sidecar — whose sha256 is verified first — not the original path), regenerates into --db (which must be a fresh, empty or nonexistent directory, distinct from the original), and verifies the new state root against the recorded one — exiting non-zero on mismatch. This works even for runs created with --seed=0, since the manifest captured the concrete seed. The reproduced datadir gets its own manifest too, with a reproduced_from field pointing back at the source manifest.
Reproduce a CI failure locally
CI loads examples/full-matrix-spec-feature.yaml on top of --seed=42 --target-size=100MB (mirrored across all five TestE2ESuite constants). The auto-fill (mainnet-shaped 20 / 10 / 70 split) emits synthetic state to fill the headroom between the spec's projected cost and --target-size — the spec entities are written first, the auto-fill fills the gap. Re-run:
go test -run TestE2ESuite ./client/<client>/... -v
Diagnose "wrong state root"
The cross-client genesis-root invariant says: same --seed, same spec, same client-policy → identical state root across all six MPT clients. If it diverges, see ARCHITECTURE.md#cross-client-determinism for the full check-list (per-client calibration in internal/sizecal/doc.go; canonical syscontract preamble; CI keystone job).
Constraints & gotchas
- Besu / reth / nethermind / ethrex / erigon require Docker for the writer side (cgo dependencies; no native build on macOS). Only geth has a pure-Go writer.
--seed=0 is a footgun: main.go rewrites it to time.Now().UnixNano(), i.e. randomises. For determinism use any non-zero seed. Bench convention is --seed=42.
--target-size is required when --spec is not set: drives the mainnet-shaped auto-fill (20 % account-trie / 10 % bytecode / 70 % storage) over the whole budget. With --spec, spec entities count first; the auto-fill fills the headroom on top. If the spec alone exceeds the budget, internal/specbuild truncates the entity list to the longest prefix that fits and emits a --target-size … truncated spec at entity[N] warning to stderr; no auto-fill runs in that case.
--archive is geth/reth only: rejected for besu, nethermind, and ethrex; accepted but a no-op for erigon.
--binary-trie is geth-only (EIP-7864).
- When using
--spec alone, omit --target-size — that way no auto-fill runs on top and random EOAs can't collide with spec-derived addresses. Set both only when you want the auto-fill to pad the headroom.
- Nethermind boot needs a JSON
boot.cfg pointing at the chainspec + datadir (see client/nethermind/e2e_test.go's nethermindE2EConfigTemplate). The other clients accept boot flags directly.
Testing
go test ./...
go test -run TestE2ESuite ./client/...
go test -short ./...
CI matrix lives in .github/workflows/ci.yml.
When asked to extend state-actor
The entry points cluster by the kind of extension: