Skip to content

feat: verify slot-level state transition - #9

Open
eigmax wants to merge 12 commits into
mainfrom
feat/slot-state-check
Open

eigmax wants to merge 12 commits into
mainfrom
feat/slot-state-check

Conversation

@eigmax

@eigmax eigmax commented Dec 31, 2025 •

Copy link
Copy Markdown
Member

What this branch is

The GOAT stateless block verifier on top of main: GOAT osaka hardfork, slot-level state-transition verification (ClientExecutor::execute(input, storage_info); the shipped guest passes an empty list, so the check is a library hook), the GOAT debug_executionWitness source (JSON headers), handling of witnesses that lack a storage trie or carry precompile addresses, and an RPC fallback for incomplete witnesses. The GOAT execution rules themselves (zero-gas system transactions, fee split to the foundation and locking contracts, requests derived from logs) live in the ziren-patches/reth fork, branch patch-1.9.3. On testnet 48816 the post-execution validation skips the receipts-root/bloom check (receipt encoding differs between the testnet geth and mainnet geth).

Verified against GOAT testnet3

Chain id 48816 (rpc-legacy.testnet3.goat.network, GOAT geth v0.1.12, debug_executionWitness served). Stateless execution of five recent blocks — three empty ones, one with a user transaction (40,213 gas, secp256k1 recovery), one with a zero-gas GOAT system transaction — all executed and verified against the header state root.

Fixes in the last two commits

  • 471f927 build: compile the guest with the Ziren toolchain whatever toolchain builds the host. A plain cargo build --release --bin host --features execution-witness failed with could not find specification for target "mipsel-zkm-zkvm-elf": rustup exports the workspace toolchain (nightly, from rust-toolchain.toml) and cargo exports the host rustc path as RUSTC into build scripts, and zkm-build v1.2.7 sets neither for the nested guest build. bin/host/build.rs and bin/eth-proofs/build.rs now select the guest toolchain (ZKM_GUEST_TOOLCHAIN, default zkm) and drop RUSTC/RUSTC_WRAPPER before calling zkm-build (RUSTC_WORKSPACE_WRAPPER stays: zkm-build uses it to skip the guest under clippy). README gained the one-line requirement. The Ziren pin is unchanged.
  • 914fbfc report: drop the accrue_logs_bloom_cycles_count column the CSV record never carried. The first run into a fresh report.csv failed after a successful execution with CSV error: found record with 78 fields, but the previous record has 79 fields (persist_report_hook.rs): write_header emitted a column write_record never pushed (the guest has no such profile label; the bloom is precomputed in the client input, and the only reader does not use the column). Header and record are both 78 fields now.

Checked: clean-target build with no RUSTUP_TOOLCHAIN in the environment produces the MIPS guest; the CSV failure reproduced on the previous binary and is gone (fresh file plus appends, three GOAT blocks, exit 0); cargo fmt --check and clippy clean on the touched files. The host-executor integration tests need RPC_1/RPC_10/RPC_59144/RPC_11155111 archive endpoints and are unchanged.

Performance (GOAT testnet3, execution only, 914fbfc)

Of the last 3,000 testnet3 blocks (17238616-17241615), 2,749 are empty, 251 carry transactions (at most 2), 51 of those only zero-gas system transactions; the busiest block in the window used 271,265 gas. The busiest six plus two median blocks, executed stateless in the Ziren executor (instructions = the proving-relevant count; "tracked" = the host's cycle-tracker total; shares are of tracked cycles):

block txs gas instructions tracked instr/gas exec witness db state root deserialize senders
17241345 2 271,265 6,089,011 2,979,519 22.4 42% 21% 18% 14% 3%
17238627 2 255,111 5,853,329 2,729,091 22.9 39% 22% 19% 15% 3%
17238624 2 180,067 5,658,655 2,553,907 31.4 45% 19% 16% 13% 4%
17239616 2 165,445 5,584,998 2,471,274 33.8 41% 21% 17% 14% 4%
17241531 2 153,116 5,505,939 2,394,112 36.0 44% 20% 17% 13% 4%
17239399 1 122,609 4,859,777 1,727,603 39.6 40% 22% 20% 14% 3%
17240584 1 40,213 4,491,206 1,363,585 111.7 46% 19% 17% 13% 4%
17240594 1 40,213 4,487,433 1,359,860 111.6 46% 19% 17% 13% 4%

An empty block costs 3,851,300 instructions; a block with one zero-gas system transaction 4,333,336. Witnesses are 16-42 KB (44-99 trie nodes); the only precompile seen is sha256; sender recovery is ~47k cycles per transaction over the secp256k1 syscalls. The block-execution phase alone is 4-6 cycles per gas on the busiest blocks, in line with mainnet (~7.5 cycles/gas end to end on 30 M-gas blocks); the per-block figure of 22-112 instructions per gas is fixed cost, the blocks being 100-700x smaller than mainnet's.

The dominant fixed cost is ~3.1 M instructions per block (51-70% of every block, 81% of an empty one) outside every cycle-tracker span: verify_block builds the chain spec from input.genesis, and for Genesis::GoatTestnet that is serde_json::from_str::<alloy_genesis::Genesis> over the embedded 162 KB genesis JSON (15 contract accounts, 75 KB of code hex, 63 storage slots) on every block; mainnet uses the built-in ChainSpec and never pays it. A pre-decoded GOAT chain spec (parsed at build time, or a built-in spec) would roughly halve the busiest blocks and cut empty ones five-fold. Second tier: witness-db initialisation and state-root computation (keccak-bound, ~40% of tracked cycles together) and input deserialisation (~13%).

Proving on the current GPU prover (branch feat/goat-ziren-main, this branch moved to Ziren main)

The GOAT guest on Ziren main (af27773) executes the same blocks with 15-18% fewer cycles (17241345: 6,089,011 → 4,976,938; empty block 3,851,300 → 3,266,069) and proves on the production GPU prover, shadow instance, warm second run, compressed proof 280,657 bytes:

block gas cycles 1 card 2 cards 4 cards
17241345 271,265 4,976,938 5.07 s 3.66 s 3.71 s
17238627 255,111 4,778,228 5.20 s 3.44 s 3.55 s
17238624 180,067 4,664,290 5.10 s 3.53 s 3.43 s
17240584 40,213 3,756,574 4.90 s 3.35 s 3.64 s
17241325 (empty) 0 3,266,069 3.82 s 3.26 s 3.33 s

A GOAT block is one or two core shards, so the time is the fixed pipeline floor (shard, leaf, compose, root with the compress-schedule grinds); two cards take the root's grinds in parallel, a third and fourth card add nothing at this size.

Ubuntu and others added 12 commits December 25, 2025 07:35
* fix: enhance fallback mechanism for incomplete witness data

* fix: include bytecode retrieval in RPC fallback logic

* chore: clean up redundant lines in fallback address proof logic

* update Cargo.lock
…builds the host

zkm-build v1.2.7 runs the guest's cargo with the environment of the host's
build script, into which cargo exports the host compiler as RUSTC and
rustup exports the workspace toolchain (rust-toolchain.toml, nightly).
That toolchain has no mipsel-zkm-zkvm-elf target, so a plain cargo build
failed with "could not find specification for target" unless the whole
build was run with cargo +zkm. The two build scripts now select the zkm
toolchain for the nested build, overridable through ZKM_GUEST_TOOLCHAIN,
and drop the inherited RUSTC and RUSTC_WRAPPER, the way the current
zkm-build does itself. The README states the requirement.
The header listed accrue_logs_bloom_cycles_count, but the guest no longer
profiles that stage (the logs bloom comes precomputed in the client
input) and write_record never pushed a value for it, so the header had
79 fields and every record 78. The csv writer rejects that whenever the
report file is created fresh, and the host exited with a CSV error right
after a successful execution. The column goes; the only reader of the
report, the cycle-count diff test, never used it.
Both sides moved to Ziren v1.2.7 and touched the lock files; the locks are
rebuilt from the merged manifests, with main's tagged precompile forks.
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.

2 participants