Skip to content

feat(manifest): per-run reproducibility manifest + reproduce subcommand#105

Open
skylenet wants to merge 2 commits into
mainfrom
reproducable-metadata
Open

feat(manifest): per-run reproducibility manifest + reproduce subcommand#105
skylenet wants to merge 2 commits into
mainfrom
reproducable-metadata

Conversation

@skylenet

@skylenet skylenet commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

Every run now writes a state-actor-manifest.json to the output datadir capturing everything needed to reproduce it, plus a reproduce subcommand that regenerates a run from that manifest and verifies it.

What's in the manifest

Written to the datadir root (two levels up from --db for geth, alongside geth-genesis.json; the --db dir itself for the other five clients, beside their chainspec sidecars):

  • flags — resolved config. Critically the resolved seed (--seed=0 expands to wall-clock) and resolved fork (empty --fork → client max), not just the raw inputs, so the run is reproducible even when the flags alone aren't.
  • command — full os.Args.
  • spec — when --spec is used, the YAML is written verbatim to a content-addressed state-actor-spec-<sha256>.yaml sidecar and referenced by relative filename + sha256 + original input path (not inlined — keeps the manifest readable and the spec directly reusable).
  • state_actor — version, go version, os/arch, and git revision/time/dirty (Go's automatic VCS stamping).
  • result — state root, account/contract/slot counts, db size, elapsed.

Manifest-write failures are warnings, not fatal — a successful generation never exits non-zero just because the manifest couldn't be written.

reproduce subcommand

state-actor reproduce --manifest <state-actor-manifest.json> --db <new-output-dir>

Replays the manifest's resolved flags through the identical generation pipeline into a fresh --db (refuses the original datadir), reads any spec from the sidecar next to the manifest, then verifies the regenerated state root against the recorded one — exiting non-zero on mismatch. Works even for --seed=0 runs, since the concrete seed was captured.

Versioning

  • main.Version (the -X main.Version target was previously a dead no-op) now exists and falls back to the Go-auto-stamped short commit when not linked in — so go run and plain docker build still record a meaningful version.
  • Docker images stamp the real git describe via a STATE_ACTOR_VERSION build-arg, wired through the Makefile and the deploy-docker workflow (including a v*.*.* tag → version: v1.2.3).

OCI labels

Added org.opencontainers.image.{title,description,source,licenses,version,revision} to all 7 Dockerfiles (the 5 cgo clients had none). version/revision come from build-args (STATE_ACTOR_VERSION / new STATE_ACTOR_REVISION), wired through the Makefile and deploy workflow.

Verification

  • New internal/manifest package with unit tests (version resolution, spec sidecar, load/round-trip); go build ./..., go vet, gofmt clean.
  • Reproduce proven end-to-end: an original run with --seed=0 reproduced to the identical state root via the captured resolved seed; spec runs reproduce via the sidecar; same-dir guard and usage verified.
  • All six client Docker images built and run on this branch: manifest written at the correct datadir location, version = git-describe, vcs_revision populated, OCI labels present (verified on alpine + debian-cgo variants), and all six produce the identical genesis state root (instrumentation is side-effect-free).

Docs

Added a "Reproduce a run from its manifest" task to docs/SKILL.md.

Example

Generate a besu DB from examples/spec-minimal.yaml via the Docker image:

mkdir -p /tmp/sa-spec && \
docker run --rm \
  -v /tmp/sa-spec:/data \
  -v "$(pwd)/examples:/specs:ro" \
  ghcr.io/ethereum/state-actor-besu:main \
  --client=besu --db=/data \
  --spec=/specs/spec-minimal.yaml \
  --seed=42 --chain-id=1337

The datadir then contains the manifest plus the content-addressed spec sidecar:

$ ls /tmp/sa-spec
besu-chainspec.json
database/
DATABASE_METADATA.json
state-actor-manifest.json
state-actor-spec-5309e337ea6c0f88fb86f318c500ed197c1488636216c429ea0051d0824013e4.yaml

state-actor-manifest.json:

{
  "schema_version": 1,
  "state_actor": {
    "version": "a2099cf-dirty",
    "go_version": "go1.25.11",
    "os": "linux",
    "arch": "arm64",
    "vcs_revision": "a2099cf6dc853cea687a7d2a47172bcdd617d330",
    "vcs_time": "2026-06-25T09:51:55Z",
    "vcs_modified": true
  },
  "generated_at": "2026-06-25T12:51:13Z",
  "command": [
    "/usr/local/bin/state-actor",
    "--client=besu",
    "--db=/data",
    "--spec=/specs/spec-minimal.yaml",
    "--seed=42",
    "--chain-id=1337"
  ],
  "flags": {
    "client": "besu",
    "db": "/data",
    "seed": 42,
    "seed_input": 42,
    "fork": "osaka",
    "fork_input": "",
    "chain_id": 1337,
    "gas_limit": 30000000,
    "timestamp": 0,
    "binary_trie": false,
    "group_depth": 8,
    "archive": false,
    "spec_path": "/specs/spec-minimal.yaml"
  },
  "spec": {
    "input_path": "/specs/spec-minimal.yaml",
    "sha256": "5309e337ea6c0f88fb86f318c500ed197c1488636216c429ea0051d0824013e4",
    "output_file": "state-actor-spec-5309e337ea6c0f88fb86f318c500ed197c1488636216c429ea0051d0824013e4.yaml"
  },
  "result": {
    "state_root": "0x123d9412948ca0f92f8b855420b9f34ee36be5dfaf2b1101baf0f6743e1c1cfd",
    "accounts_created": 1,
    "contracts_created": 5,
    "total_db_size_bytes": 201287,
    "elapsed_ms": 53
  }
}

For a run created with --seed=0, seed_input would be 0 while seed holds the resolved wall-clock value — which is what makes the run reproducible via reproduce.

Reproduce it

Recover the run into a fresh directory and verify it matches:

mkdir -p /tmp/sa-spec-repro && \
docker run --rm \
  -v /tmp/sa-spec:/orig:ro \
  -v /tmp/sa-spec-repro:/out \
  ghcr.io/ethereum/state-actor-besu:main \
  reproduce --manifest /orig/state-actor-manifest.json --db /out
Reproducing run from /orig/state-actor-manifest.json
  client=besu seed=42 fork=osaka → /out
...
Reproduction: PASS — state root matches 0x123d9412948ca0f92f8b855420b9f34ee36be5dfaf2b1101baf0f6743e1c1cfd

Notes:

  • Mount the original datadir read-only at a different container path (/orig) and the new output at /out. The same-dir guard compares container paths, and the manifest recorded db=/data, so the reproduce --db must differ (/out).
  • /orig must contain both the manifest and its state-actor-spec-<sha>.yaml sidecar — reproduce reads the spec from beside the manifest.

skylenet added 2 commits June 25, 2026 13:19
Every run now drops a state-actor-manifest.json at the client datadir root
(two levels up from --db for geth; --db itself for the others) capturing
everything needed to reproduce it:

- resolved flags — note the *resolved* seed (--seed=0 expands to wall-clock)
  and the *resolved* fork (empty --fork → client max), not the raw inputs
- the full command, chain params, target size, etc.
- the --spec config, written verbatim to a content-addressed sidecar
  (state-actor-spec-<sha256>.yaml) and referenced from the manifest
- build provenance: version, go version, os/arch, and the git
  revision/time/dirty that Go embeds automatically
- result: state root, counts, db size, elapsed

Version sourcing: main.Version (previously a dead -X target) now exists and
falls back to the Go-auto-stamped short commit when not linked in, so
go-run and plain docker builds still record a meaningful version. Docker
images stamp the real git-describe via a STATE_ACTOR_VERSION build-arg
(wired through the Makefile and deploy-docker workflow).

Also add OCI image labels (title/description/source/licenses/version/
revision) to all Dockerfiles, with the commit passed via a new
STATE_ACTOR_REVISION build-arg.

Manifest write failures are warnings, not fatal — a successful generation
never exits non-zero because the manifest could not be written.
…ts manifest

state-actor reproduce --manifest <state-actor-manifest.json> --db <new-dir>

Loads the manifest's resolved flags (the concrete seed + fork, so runs
created with --seed=0 reproduce exactly), replays them through the same
generation pipeline into a fresh --db (refusing the original datadir), and
reads any spec from the content-addressed sidecar next to the manifest
rather than the original --spec path. After regenerating it verifies the
new state root against the recorded one and exits non-zero on mismatch.

main() is split into subcommand dispatch + a shared generate(); adds
manifest.Load. Documents the manifest + reproduce flow in docs/SKILL.md.
@skylenet skylenet requested a review from CPerezz June 25, 2026 12:55
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