diff --git a/.github/scripts/aggregate_recursion_histogram.py b/.github/scripts/aggregate_recursion_histogram.py
new file mode 100755
index 000000000..1ae34ff70
--- /dev/null
+++ b/.github/scripts/aggregate_recursion_histogram.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+"""Format the recursion-guest per-function profile as a Markdown PR comment.
+
+`test_recursion_pc_histogram` prints a per-function summary table: the cycles
+folded over each function's PCs, computed across the *full* histogram — the view
+that shows where the cycles actually go. We parse that table and render it as
+Markdown.
+
+ Top 25 functions by cycle count (aggregated over their PCs):
+ rank cycles % cum % PCs function
+ 1 5335072 24.95% 24.95% 72 <...>::visit_seq::<...>
+
+Reads the test's captured output from argv[1]; writes the Markdown body to
+argv[2] (or stdout).
+"""
+
+import re
+import sys
+
+# A per-function summary row: rank, cycles, pct%, cum%, pcs, function.
+FN_ROW = re.compile(
+ r"^\s*\d+\s+(\d+)\s+([\d.]+)%\s+([\d.]+)%\s+(\d+)\s+(.*\S)\s*$"
+)
+FN_TABLE_START = re.compile(r"Top \d+ functions by cycle count")
+# The "====" rule the test prints right after the (now sole) function table.
+TABLE_END = re.compile(r"^=+\s*$")
+TOTAL_CYCLES = re.compile(r"Total cycles\s*:\s*(\d+)")
+UNIQUE_PCS = re.compile(r"Unique PCs\s*:\s*(\d+)")
+EXEC_TIME = re.compile(r"Exec time\s*:\s*(\S+)")
+
+
+def parse(text):
+ total_cycles = unique_pcs = exec_time = None
+ rows = []
+ in_fn_table = False
+ for line in text.splitlines():
+ if total_cycles is None and (m := TOTAL_CYCLES.search(line)):
+ total_cycles = int(m.group(1))
+ if unique_pcs is None and (m := UNIQUE_PCS.search(line)):
+ unique_pcs = int(m.group(1))
+ if exec_time is None and (m := EXEC_TIME.search(line)):
+ exec_time = m.group(1)
+ if FN_TABLE_START.search(line):
+ in_fn_table = True
+ continue
+ if in_fn_table and TABLE_END.match(line):
+ in_fn_table = False
+ continue
+ if in_fn_table and (m := FN_ROW.match(line)):
+ rows.append(
+ {
+ "cycles": int(m.group(1)),
+ "pct": m.group(2),
+ "cum": m.group(3),
+ "pcs": int(m.group(4)),
+ "fn": m.group(5),
+ }
+ )
+ return total_cycles, unique_pcs, exec_time, rows
+
+
+def short(name, width=90):
+ return name if len(name) <= width else name[: width - 1] + "…"
+
+
+def render(total_cycles, unique_pcs, exec_time, rows, title="Recursion guest profile"):
+ if not rows:
+ return (
+ f"### {title}\n\n"
+ "> ⚠️ No per-function rows found in the test output — the run may "
+ "have failed before printing the table. Check the workflow logs.\n"
+ )
+
+ body = f"### {title}\n\n"
+ if total_cycles is not None:
+ body += f"**Total cycles:** {total_cycles:,}"
+ if unique_pcs is not None:
+ body += f" · **Unique PCs:** {unique_pcs:,}"
+ if exec_time:
+ body += f" · **Exec time:** {exec_time}"
+ body += "\n\n"
+
+ body += f"#### Top {len(rows)} functions by cycles (folded over their PCs)\n\n"
+ body += "| Rank | Cycles | % | Cum % | PCs | Function |\n"
+ body += "|-----:|-------:|--:|------:|----:|----------|\n"
+ for i, r in enumerate(rows, 1):
+ body += (
+ f"| {i} | {r['cycles']:,} | {r['pct']}% | {r['cum']}% | "
+ f"{r['pcs']} | `{short(r['fn'])}` |\n"
+ )
+
+ last_cum = rows[-1]["cum"]
+ body += (
+ f"\nEach function's cycles are summed over all its program counters "
+ f"across the full histogram; the top {len(rows)} cover {last_cum}% of total "
+ f"cycles. Percentages are of total cycles.\n"
+ )
+ return body
+
+
+def main():
+ import argparse
+
+ ap = argparse.ArgumentParser(description=__doc__)
+ ap.add_argument("log", help="captured test output to parse")
+ ap.add_argument("-o", "--out", help="write Markdown here instead of stdout")
+ ap.add_argument(
+ "-t",
+ "--title",
+ default="Recursion guest profile",
+ help="section heading (e.g. the test/config name)",
+ )
+ args = ap.parse_args()
+
+ with open(args.log, "r", errors="replace") as f:
+ text = f.read()
+ body = render(*parse(text), title=args.title)
+ if args.out:
+ with open(args.out, "w") as f:
+ f.write(body)
+ else:
+ sys.stdout.write(body)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/workflows/profile-recursion.yml b/.github/workflows/profile-recursion.yml
new file mode 100644
index 000000000..0e614fcd8
--- /dev/null
+++ b/.github/workflows/profile-recursion.yml
@@ -0,0 +1,178 @@
+name: Profile Recursion (PR)
+
+# Runs the recursion-guest PC histogram diagnostics (single-query and
+# multi-query, in parallel via a matrix) and posts a combined per-function
+# profile as a PR comment. Triggered by a `/profile_recursion` comment from a
+# repo member, or manually via workflow_dispatch.
+
+on:
+ workflow_dispatch:
+ issue_comment:
+ types: [created]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+concurrency:
+ group: profile-recursion-${{ github.event.issue.number || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ # One job per configuration; they run in parallel and each uploads a Markdown
+ # fragment artifact. The `comment` job stitches them into one PR comment.
+ profile:
+ # Skip unless: workflow_dispatch, or "/profile_recursion" comment on a PR by a member.
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ (github.event_name == 'issue_comment' &&
+ github.event.issue.pull_request &&
+ startsWith(github.event.comment.body, '/profile_recursion') &&
+ contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association))
+ runs-on: [self-hosted, bench]
+ timeout-minutes: 90
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: single-query
+ test: single
+ title: "Single query (blowup=2, 1 query)"
+ - name: multi-query
+ test: multi
+ title: "Multi query (blowup=8, 128-bit)"
+ steps:
+ - name: React to comment
+ if: github.event_name == 'issue_comment' && matrix.name == 'single-query'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'eyes'
+ });
+
+ - name: Get PR head ref
+ id: pr-ref
+ if: github.event_name == 'issue_comment'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUM: ${{ github.event.issue.number }}
+ run: |
+ SHA=$(gh pr view "$PR_NUM" --repo "$GITHUB_REPOSITORY" --json headRefOid -q .headRefOid)
+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
+
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ steps.pr-ref.outputs.sha || github.sha }}
+
+ - name: Setup Rust Environment
+ uses: ./.github/actions/setup-rust
+
+ - name: Add cargo to PATH
+ run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
+
+ - name: Run recursion PC histogram (${{ matrix.name }})
+ env:
+ TEST: ${{ matrix.test }}
+ run: |
+ # Self-provision the RISC-V sysroot in a user-writable dir (the default
+ # /opt path on the bench runner is root-owned); the guest ELF build the
+ # test triggers picks this up via the Makefile's `SYSROOT_DIR ?=`.
+ export SYSROOT_DIR="$HOME/.lambda-vm-sysroot"
+ set -o pipefail
+ make test-profile-recursion-$TEST 2>&1 | tee /tmp/hist.log
+
+ - name: Aggregate into a per-function fragment
+ if: always()
+ env:
+ TITLE: ${{ matrix.title }}
+ run: |
+ python3 .github/scripts/aggregate_recursion_histogram.py \
+ /tmp/hist.log --title "$TITLE" --out "/tmp/fragment-${{ matrix.name }}.md"
+ cat "/tmp/fragment-${{ matrix.name }}.md" >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Upload fragment
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: profile-fragment-${{ matrix.name }}
+ path: /tmp/fragment-${{ matrix.name }}.md
+ retention-days: 7
+
+ # Stitch the matrix fragments into a single PR comment.
+ comment:
+ needs: profile
+ # always() so partial-matrix failures still post; skip when `profile` was
+ # skipped (non-/profile_recursion or non-member comment) so this job — and
+ # the self-hosted bench runner it spins up — doesn't fire on every comment.
+ if: always() && github.event_name == 'issue_comment' && needs.profile.result != 'skipped'
+ runs-on: [self-hosted, bench]
+ steps:
+ - name: Get PR head ref
+ id: pr-ref
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUM: ${{ github.event.issue.number }}
+ run: |
+ SHA=$(gh pr view "$PR_NUM" --repo "$GITHUB_REPOSITORY" --json headRefOid -q .headRefOid)
+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
+
+ - name: Download fragments
+ uses: actions/download-artifact@v4
+ with:
+ path: fragments
+ pattern: profile-fragment-*
+ merge-multiple: true
+
+ - name: Assemble comment body
+ env:
+ COMMIT_SHA: ${{ steps.pr-ref.outputs.sha }}
+ run: |
+ {
+ echo "## Recursion guest profile"
+ echo
+ # Single-query first, then multi-query, then any others.
+ for frag in fragments/fragment-single-query.md \
+ fragments/fragment-multi-query.md; do
+ [ -f "$frag" ] && { cat "$frag"; echo; }
+ done
+ echo "Commit: ${COMMIT_SHA:0:8} · Runner: self-hosted bench"
+ } > /tmp/profile_comment.md
+ cat /tmp/profile_comment.md
+
+ - name: Comment on PR
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const body = fs.readFileSync('/tmp/profile_comment.md', 'utf8');
+
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+ // Reuse our own marker comment so repeated /profile_recursion runs update in place.
+ const existing = comments.find(c =>
+ c.user.type === 'Bot' &&
+ c.body.includes('Recursion guest profile')
+ );
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body,
+ });
+ }
diff --git a/Makefile b/Makefile
index 454eff098..801845534 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
.PHONY: deps deps-linux deps-macos compile-programs-asm compile-programs-rust compile-bench \
compile-programs compile-recursion-elfs clean-asm clean-rust clean-bench clean-shared \
clean-recursion-elfs clean test test-asm \
-test-rust test-executor test-flamegraph flamegraph-prover \
+test-rust test-executor test-flamegraph flamegraph-prover test-profile-recursion test-profile-recursion-single test-profile-recursion-multi \
test-fast test-prover test-prover-all test-disk-spill test-math-cuda test-cuda-integration \
bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \
update-ethrex-fixture-checksums check-ethrex-fixture-checksums
@@ -51,7 +51,7 @@ BENCH_ARTIFACTS := $(addprefix $(BENCH_ARTIFACTS_DIR)/, $(addsuffix .elf, $(BENC
# rather than executor/programs/. The recursion guest is the in-VM STARK verifier.
RECURSION_GUESTS_DIR=./bench_vs/lambda
RECURSION_ARTIFACTS_DIR=./executor/program_artifacts/recursion
-RECURSION_GUESTS := empty fibonacci recursion
+RECURSION_GUESTS := empty fibonacci recursion deserialize-only
RECURSION_ARTIFACTS := $(addprefix $(RECURSION_ARTIFACTS_DIR)/, $(addsuffix .elf, $(RECURSION_GUESTS)))
# Override with: make ... SYSROOT_DIR=$HOME/.lambda-vm-sysroot
@@ -232,6 +232,14 @@ test-rust: compile-programs-rust
test-flamegraph:
cargo test -p executor --test flamegraph
+test-profile-recursion: test-profile-recursion-single test-profile-recursion-multi
+
+test-profile-recursion-single: compile-recursion-elfs
+ cargo test --package lambda-vm-prover --lib test_recursion_profile_1query -- --ignored --nocapture
+
+test-profile-recursion-multi: compile-recursion-elfs
+ cargo test --package lambda-vm-prover --lib test_recursion_profile_multiquery -- --ignored --nocapture
+
# Regenerate the committed ethrex block fixtures (see tooling/ethrex-fixtures).
# Run after bumping the ethrex rev; README checksums are refreshed automatically.
regen-ethrex-fixtures:
diff --git a/bench_vs/lambda/deserialize-only/.cargo/config.toml b/bench_vs/lambda/deserialize-only/.cargo/config.toml
new file mode 100644
index 000000000..f5ea686ff
--- /dev/null
+++ b/bench_vs/lambda/deserialize-only/.cargo/config.toml
@@ -0,0 +1,7 @@
+[target.riscv64im-lambda-vm-elf]
+rustflags = [
+ "-C", "link-arg=-e",
+ "-C", "link-arg=main",
+ "--cfg", "getrandom_backend=\"custom\"",
+ "-C", "passes=lower-atomic"
+]
diff --git a/bench_vs/lambda/deserialize-only/Cargo.lock b/bench_vs/lambda/deserialize-only/Cargo.lock
new file mode 100644
index 000000000..9433fadb3
--- /dev/null
+++ b/bench_vs/lambda/deserialize-only/Cargo.lock
@@ -0,0 +1,1199 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "atomic-polyfill"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
+dependencies = [
+ "critical-section",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "const-default"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa"
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "critical-section"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto"
+version = "0.1.0"
+dependencies = [
+ "digest",
+ "math",
+ "rand 0.8.6",
+ "rand_chacha 0.3.1",
+ "serde",
+ "sha3",
+]
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "deserialize-only-bench"
+version = "0.1.0"
+dependencies = [
+ "lambda-vm-prover",
+ "lambda-vm-syscalls",
+ "postcard",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "ecsm"
+version = "0.1.0"
+dependencies = [
+ "k256",
+ "num-bigint",
+ "num-traits",
+]
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "ff",
+ "generic-array",
+ "group",
+ "rand_core 0.6.4",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "embedded-alloc"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd"
+dependencies = [
+ "const-default",
+ "critical-section",
+ "linked_list_allocator",
+ "rlsf",
+]
+
+[[package]]
+name = "embedded-hal"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "executor"
+version = "0.1.0"
+dependencies = [
+ "ecsm",
+ "rustc-demangle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "half"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
+
+[[package]]
+name = "hash32"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "heapless"
+version = "0.7.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
+dependencies = [
+ "atomic-polyfill",
+ "hash32",
+ "rustc_version",
+ "serde",
+ "spin",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "k256"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
+dependencies = [
+ "cfg-if",
+ "elliptic-curve",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "lambda-vm-prover"
+version = "0.1.0"
+dependencies = [
+ "crypto",
+ "ecsm",
+ "executor",
+ "log",
+ "math",
+ "serde",
+ "sha3",
+ "stark",
+ "sysinfo",
+]
+
+[[package]]
+name = "lambda-vm-syscalls"
+version = "0.1.0"
+dependencies = [
+ "embedded-alloc",
+ "getrandom 0.2.17",
+ "getrandom 0.3.4",
+ "lazy_static",
+ "rand 0.9.4",
+ "riscv",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "linked_list_allocator"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "math"
+version = "0.1.0"
+dependencies = [
+ "getrandom 0.2.17",
+ "num-bigint",
+ "num-traits",
+ "rand 0.8.6",
+ "rayon",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "ntapi"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "heapless",
+ "serde",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "riscv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25"
+dependencies = [
+ "critical-section",
+ "embedded-hal",
+ "paste",
+ "riscv-macros",
+ "riscv-pac",
+]
+
+[[package]]
+name = "riscv-macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "riscv-pac"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436"
+
+[[package]]
+name = "rlsf"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1646a59a9734b8b7a0ac51689388a60fe1625d4b956348e9de07591a1478457a"
+dependencies = [
+ "cfg-if",
+ "const-default",
+ "libc",
+ "rustversion",
+ "svgbobdoc",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_cbor"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
+dependencies = [
+ "half",
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.143"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha3"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "stark"
+version = "0.1.0"
+dependencies = [
+ "crypto",
+ "itertools",
+ "log",
+ "math",
+ "serde",
+ "serde_cbor",
+ "sha3",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "svgbobdoc"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50"
+dependencies = [
+ "base64",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "unicode-width",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sysinfo"
+version = "0.31.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+ "memchr",
+ "ntapi",
+ "windows",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
+dependencies = [
+ "windows-core",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-result",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
diff --git a/bench_vs/lambda/deserialize-only/Cargo.toml b/bench_vs/lambda/deserialize-only/Cargo.toml
new file mode 100644
index 000000000..fac6a7628
--- /dev/null
+++ b/bench_vs/lambda/deserialize-only/Cargo.toml
@@ -0,0 +1,11 @@
+[workspace]
+
+[package]
+name = "deserialize-only-bench"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+lambda-vm-prover = { path = "../../../prover", default-features = false }
+lambda-vm-syscalls = { path = "../../../syscalls" }
+postcard = { version = "1.0", features = ["alloc"] }
diff --git a/bench_vs/lambda/deserialize-only/src/main.rs b/bench_vs/lambda/deserialize-only/src/main.rs
new file mode 100644
index 000000000..7ba9a9d93
--- /dev/null
+++ b/bench_vs/lambda/deserialize-only/src/main.rs
@@ -0,0 +1,32 @@
+//! Deserialize-only counterpart to the recursion guest.
+//!
+//! Reads the same private-input blob as `recursion-bench`, postcard-decodes
+//! `(VmProof, Vec, ProofOptions)`, then commits and halts — without ever
+//! calling `verify_with_options`. The cycle delta between this guest and
+//! `recursion-bench` is the actual cost of the STARK verifier inside the VM.
+//!
+//! Mirrors the recursion guest's std setup (build-std + `lambda_vm_syscalls`)
+//! so the two differ only in the verify call.
+
+#![no_main]
+
+use lambda_vm_prover::{ProofOptions, VmProof};
+
+#[unsafe(export_name = "main")]
+pub fn main() -> ! {
+ lambda_vm_syscalls::allocator::init_allocator();
+
+ const PANIC_MSG: &str = "PANICKED";
+ std::panic::set_hook(Box::new(|_| unsafe {
+ lambda_vm_syscalls::syscalls::sys_panic(PANIC_MSG.as_ptr(), PANIC_MSG.len())
+ }));
+
+ let blob = lambda_vm_syscalls::syscalls::get_private_input();
+ let decoded: (VmProof, Vec, ProofOptions) =
+ postcard::from_bytes(&blob).expect("failed to deserialize recursion input");
+
+ // Tie the committed byte to the decoded value so LLVM can't elide the decode.
+ let marker = decoded.2.blowup_factor ^ *decoded.1.first().unwrap_or(&0);
+ lambda_vm_syscalls::syscalls::commit(&[marker]);
+ lambda_vm_syscalls::syscalls::sys_halt();
+}
diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs
index 03119f617..d4186e563 100644
--- a/crypto/stark/src/verifier.rs
+++ b/crypto/stark/src/verifier.rs
@@ -97,6 +97,7 @@ pub trait IsStarkVerifier<
/// Checks whether the purported evaluations of the composition polynomial parts and the trace
/// polynomials at the out-of-domain challenge are consistent.
/// See https://lambdaclass.github.io/lambdaworks/starks/protocol.html#step-2-verify-claimed-composition-polynomial
+ #[inline(never)]
fn step_2_verify_claimed_composition_polynomial(
air: &dyn AIR,
proof: &StarkProof,
@@ -241,6 +242,7 @@ pub trait IsStarkVerifier<
/// Reconstructs the Deep composition polynomial evaluations at the challenge indices values using the provided
/// openings of the trace polynomials and the composition polynomial parts. It then uses these to verify that the
/// FRI decommitments are valid and correspond to the Deep composition polynomial.
+ #[inline(never)]
fn step_3_verify_fri(
proof: &StarkProof,
domain: &VerifierDomain,
@@ -396,6 +398,7 @@ pub trait IsStarkVerifier<
/// Verifies the validity of the purported values of the trace polynomials and the composition polynomial
/// parts at the domain elements and their symmetric counterparts corresponding to all the FRI query
/// index challenges.
+ #[inline(never)]
fn step_4_verify_trace_and_composition_openings(
proof: &StarkProof,
challenges: &Challenges,
@@ -903,6 +906,7 @@ pub trait IsStarkVerifier<
/// Replays rounds 2, 3 and 4 of the protocol for a given proof, assuming round 1 has
/// already been replayed and the RAP challenges are known.
+ #[inline(never)]
fn replay_rounds_after_round_1(
air: &dyn AIR,
proof: &StarkProof,
diff --git a/executor/src/elf.rs b/executor/src/elf.rs
index ed79fb983..da38cbbf1 100644
--- a/executor/src/elf.rs
+++ b/executor/src/elf.rs
@@ -557,4 +557,9 @@ impl SymbolTable {
pub fn len(&self) -> usize {
self.functions.len()
}
+
+ /// Borrow the full function list (sorted by address).
+ pub fn functions(&self) -> &[FunctionSymbol] {
+ &self.functions
+ }
}
diff --git a/executor/src/flamegraph.rs b/executor/src/flamegraph.rs
index f9b447d19..4764d71a2 100644
--- a/executor/src/flamegraph.rs
+++ b/executor/src/flamegraph.rs
@@ -154,7 +154,7 @@ impl FlamegraphGenerator {
/// Demangle a Rust symbol name using the official rustc-demangle crate.
///
/// Uses the alternate format (`{:#}`) to omit the hash suffix for cleaner output.
-pub(crate) fn demangle(name: &str) -> String {
+pub fn demangle(name: &str) -> String {
// Use rustc-demangle with alternate format to omit hash
format!("{:#}", rustc_demangle(name))
}
diff --git a/executor/src/vm/memory.rs b/executor/src/vm/memory.rs
index f349eeae6..f3a3e622c 100644
--- a/executor/src/vm/memory.rs
+++ b/executor/src/vm/memory.rs
@@ -218,6 +218,13 @@ impl Memory {
Ok(self.public_output.clone())
}
+ /// Read-only access to the underlying 4-byte cell map. Exposed for
+ /// diagnostic tooling (e.g. counting the distinct 4 KB memory pages a
+ /// program touches) — not part of the normal execution interface.
+ pub fn cells(&self) -> &U64HashMap<[u8; 4]> {
+ &self.cells
+ }
+
/// Pre-loads private input bytes at `PRIVATE_INPUT_START_INDEX` as a
/// 4-byte LE length prefix followed by the raw data. The guest reads these
/// bytes directly via normal RISC-V loads (ZisK-style memory-mapped input).
diff --git a/prover/src/tests/recursion_smoke_test.rs b/prover/src/tests/recursion_smoke_test.rs
index 7bcd4bd3d..d34e7cf4d 100644
--- a/prover/src/tests/recursion_smoke_test.rs
+++ b/prover/src/tests/recursion_smoke_test.rs
@@ -1,17 +1,13 @@
-//! End-to-end naive recursion pipeline smoke tests.
+//! End-to-end naive recursion pipeline smoke tests: prove an inner program,
+//! hand `(VmProof, elf, opts)` to the in-VM verifier guest, then either prove
+//! the guest's execution (`OuterMode::Prove`) or just execute it
+//! (`OuterMode::ExecuteOnly`). Guest ELFs come from `make compile-recursion-elfs`.
//!
-//! Each test:
-//! 1. Proves an inner program on the host.
-//! 2. Serializes `(VmProof, inner_elf, opts)` with postcard.
-//! 3. Hands that as private input to the recursion guest.
-//! 4. Either **proves** the recursion guest's execution (memory-bounded via
-//! continuations) and verifies the outer proof (`OuterMode::Prove`), or
-//! merely **executes** the guest in-VM and reads the committed marker
-//! straight off the executor's memory (`OuterMode::ExecuteOnly`) — a cheaper
-//! tier that skips the LDE/FRI that dominate the full pipeline.
-//!
-//! The guest ELFs are assumed built by `make compile-recursion-elfs`.
+//! Every pipeline host-verifies the inner proof, so building with
+//! `--features stark/instruments` makes any of these tests print the verifier's
+//! per-step `Time spent:` timings.
+use std::ops::ControlFlow;
use std::path::PathBuf;
fn workspace_root() -> PathBuf {
@@ -32,11 +28,8 @@ fn read_guest_elf(root: &std::path::Path, name: &str) -> Vec {
})
}
-/// Minimum-security FRI parameters: blowup=2, a single FRI query. Security is
-/// intentionally terrible — used by the capacity-probing test, where the goal
-/// is the smallest possible inner proof, not a sound one.
-/// (`GoldilocksCubicProofOptions::with_blowup` derives a query count from a
-/// 128-bit target, far more than we want here.)
+/// Smallest possible inner proof (blowup=2, 1 query). Intentionally insecure —
+/// for the cheap diagnostics, not soundness.
const MIN_PROOF_OPTIONS: stark::proof::options::ProofOptions =
stark::proof::options::ProofOptions {
blowup_factor: 2,
@@ -45,11 +38,8 @@ const MIN_PROOF_OPTIONS: stark::proof::options::ProofOptions =
grinding_factor: 1,
};
-/// Prove `inner_elf` (fed `inner_input`) under `opts`, then package
-/// `(proof, elf, opts)` into the postcard blob the recursion guest consumes as
-/// its private input. `tag` prefixes the progress lines. Returns the inner
-/// proof — callers that re-verify it on the host need it — next to the encoded
-/// blob.
+/// Prove `inner_elf` under `opts` and postcard-encode `(proof, elf, opts)` into
+/// the guest's private-input blob. Returns the proof and the blob.
fn prove_inner_and_encode_blob(
tag: &str,
inner_elf: &[u8],
@@ -74,26 +64,17 @@ fn prove_inner_and_encode_blob(
(inner_proof, blob)
}
-/// How far to take the recursion guest after it has been handed the inner
-/// proof. The guest under test is the verifier either way — this only chooses
-/// whether we also prove the guest's own execution.
+/// Whether to also prove the guest's own execution after handing it the proof.
#[derive(Clone, Copy, Debug)]
enum OuterMode {
- /// Execute the guest in-VM and read the committed marker straight off the
- /// executor's memory. Streams logs via `Executor::resume()` and never
- /// builds a `Traces`, so footprint stays bounded to the VM's touched
- /// memory + instruction cache. Skips the LDE/FRI of the full pipeline entirely.
+ /// Execute in-VM, read the committed marker off memory; no LDE/FRI.
ExecuteOnly,
- /// Prove the guest's execution memory-bounded via continuations, then
- /// verify the outer proof on the host. Peak RAM is a single epoch's proof.
+ /// Prove the execution (memory-bounded via continuations) and verify on host.
Prove,
}
-/// Execute the recursion guest in-VM on `blob` and return the bytes it
-/// committed (the success marker the in-VM verifier emits).
-///
-/// Streams execution via `Executor::resume()`. The committed marker is
-/// read directly off the executor's memory. This avoids OOMs.
+/// Execute the recursion guest in-VM on `blob` and return its committed bytes,
+/// read straight off the executor's memory after a streamed run.
fn execute_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8]) -> Vec {
use executor::elf::Elf;
use executor::vm::execution::Executor;
@@ -102,12 +83,11 @@ fn execute_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8]
let program = Elf::load(recursion_elf_bytes).expect("load recursion elf");
let mut executor = Executor::new(&program, blob.to_vec()).expect("executor new");
- // Drain chunks to completion without retaining logs or building a trace.
- while executor
- .resume()
- .expect("recursion guest execution failed (verify panicked in-VM?)")
- .is_some()
- {}
+ let (total_cycles, exec_time) = drive_executor(
+ &mut executor,
+ |_log| ControlFlow::Continue(()),
+ |_, _, _| {},
+ );
let committed = executor
.finish()
@@ -115,7 +95,7 @@ fn execute_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8]
.memory_values;
eprintln!(
- "[{label}] committed {} bytes: {:?} (as str: {:?})",
+ "[{label}] {total_cycles} cycles in {exec_time:?}; committed {} bytes: {:?} (as str: {:?})",
committed.len(),
committed,
String::from_utf8_lossy(&committed),
@@ -126,9 +106,8 @@ fn execute_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8]
/// Epoch size for the outer prove: 2^20 ≈ 1M cycles per epoch.
const OUTER_EPOCH_SIZE_LOG2: u32 = 20;
-/// Prove the recursion guest's execution on `blob` memory-bounded via
-/// continuations and verify the bundle on the host, returning the bytes the
-/// guest committed.
+/// Prove the guest's execution via continuations, verify on host, return the
+/// committed bytes.
fn prove_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8]) -> Vec {
let opts =
crate::GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid");
@@ -148,10 +127,292 @@ fn prove_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8])
committed
}
-/// Core pipeline: prove an inner program with the given options, hand the
-/// proof+ELF+options to the recursion guest, then take the guest to `mode`
-/// (execute-only or full prove) and assert it committed the `[1]` success
-/// marker — i.e. the in-VM verifier accepted the inner proof.
+/// Stream a guest's execution via `Executor::resume()` without buffering the log
+/// stream. `on_log` returns `Break` to stop early; `on_progress` fires per chunk.
+/// Returns `(total_cycles, wall_time)`, exact even on an early break.
+fn drive_executor(
+ executor: &mut executor::vm::execution::Executor,
+ mut on_log: impl FnMut(&executor::vm::logs::Log) -> ControlFlow<()>,
+ mut on_progress: impl FnMut(usize, u64, std::time::Duration),
+) -> (u64, std::time::Duration) {
+ let start = std::time::Instant::now();
+ let mut total_cycles: u64 = 0;
+ let mut chunks: usize = 0;
+ while let Some(logs) = executor
+ .resume()
+ .expect("executor resume failed (guest panicked in-VM?)")
+ {
+ let mut stop = false;
+ for log in logs {
+ total_cycles += 1;
+ if on_log(log).is_break() {
+ stop = true;
+ break;
+ }
+ }
+ chunks += 1;
+ on_progress(chunks, total_cycles, start.elapsed());
+ if stop {
+ break;
+ }
+ }
+ (total_cycles, start.elapsed())
+}
+
+/// Shared preamble: build the blob (an `empty` inner proof under `opts`), load
+/// `guest_name`, and stand up an executor. Returns `(elf_bytes, program, executor)`.
+fn setup_guest_run(
+ label: &str,
+ guest_name: &str,
+ opts: &stark::proof::options::ProofOptions,
+) -> (
+ Vec,
+ executor::elf::Elf,
+ executor::vm::execution::Executor,
+) {
+ let root = workspace_root();
+ let empty_elf_bytes = read_guest_elf(&root, "empty");
+ let guest_elf_bytes = read_guest_elf(&root, guest_name);
+
+ let (_inner_proof, blob) = prove_inner_and_encode_blob(label, &empty_elf_bytes, &[], opts);
+
+ let program = executor::elf::Elf::load(&guest_elf_bytes).expect("ELF load failed");
+ assert_ne!(
+ program.entry_point, 0,
+ "{guest_name} ELF has entry_point=0 — build artifact is malformed"
+ );
+ let executor =
+ executor::vm::execution::Executor::new(&program, blob).expect("Executor::new failed");
+ (guest_elf_bytes, program, executor)
+}
+
+/// A `drive_executor` progress callback printing one line every `stride` chunks.
+fn log_progress(
+ label: impl Into,
+ stride: usize,
+) -> impl FnMut(usize, u64, std::time::Duration) {
+ let label = label.into();
+ move |chunks, cycles, elapsed| {
+ if chunks.is_multiple_of(stride) {
+ eprintln!("[{label}] ... {chunks} chunks, {cycles} cycles, {elapsed:?} elapsed");
+ }
+ }
+}
+
+/// Demangled enclosing-function name for a PC via the ELF symbol table;
+/// `` if none covers it. No file:line (symtab has no DWARF).
+fn resolve_pc(symbols: &executor::elf::SymbolTable, pc: u64) -> String {
+ symbols.lookup(pc).map_or_else(
+ || "".to_string(),
+ |s| executor::flamegraph::demangle(&s.name),
+ )
+}
+
+/// Verifier sub-routines in execution order; `run_profile` buckets cycles by
+/// substring-matching the enclosing symbol (a missing step merges into the prior).
+const VERIFIER_STEP_KEYWORDS: [&str; 4] = [
+ "replay_rounds_after_round_1",
+ "step_2_verify_claimed_composition_polynomial",
+ "step_3_verify_fri",
+ "step_4_verify_trace_and_composition_openings",
+];
+
+/// `blowup=8` (128-bit, multi-query) options for the `multiquery` variants.
+fn blowup8() -> stark::proof::options::ProofOptions {
+ crate::GoldilocksCubicProofOptions::with_blowup(8).expect("blowup=8 is always valid")
+}
+
+/// Print the top-25 functions by cycles, folding the PC histogram by symbol.
+fn print_function_table(
+ symbols: &executor::elf::SymbolTable,
+ pc_hist: std::collections::HashMap,
+ total_cycles: u64,
+) {
+ let mut by_function: std::collections::HashMap =
+ std::collections::HashMap::new();
+ for (pc, count) in &pc_hist {
+ let entry = by_function
+ .entry(resolve_pc(symbols, *pc))
+ .or_insert((0, 0));
+ entry.0 += *count; // cycles
+ entry.1 += 1; // distinct PCs folded into this function
+ }
+ let mut fn_entries: Vec<(String, (u64, u64))> = by_function.into_iter().collect();
+ fn_entries.sort_unstable_by_key(|(_name, (cycles, _pcs))| std::cmp::Reverse(*cycles));
+
+ let pct = |n: u64| 100.0 * (n as f64) / (total_cycles as f64);
+ eprintln!(" Unique PCs : {}", pc_hist.len());
+ eprintln!();
+ eprintln!(" Top 25 functions by cycle count (aggregated over their PCs):");
+ eprintln!(" rank cycles % cum % PCs function");
+ let mut fn_cumulative: u64 = 0;
+ for (rank, (name, (cycles, pcs))) in fn_entries.iter().take(25).enumerate() {
+ fn_cumulative += cycles;
+ eprintln!(
+ " {:>4} {:>14} {:>6.2}% {:>6.2}% {:>5} {}",
+ rank + 1,
+ cycles,
+ pct(*cycles),
+ pct(fn_cumulative),
+ pcs,
+ name,
+ );
+ }
+}
+
+/// Print the monotonic per-verifier-step cycle bucketing (`buckets[0]` = setup).
+fn print_step_breakdown(buckets: &[u64; 5], total_cycles: u64) {
+ let labels = [
+ "0. setup (alloc + postcard decode + VmAirs::new + pre-step-1)",
+ "1. step 1: replay_rounds_after_round_1",
+ "2. step 2: verify_claimed_composition_polynomial",
+ "3. step 3: verify_fri",
+ "4. step 4: verify_trace_and_composition_openings (+ wrap-up)",
+ ];
+ eprintln!();
+ eprintln!(" Per-step cycle breakdown (monotonic state machine):");
+ eprintln!(" {:<60} {:>14} {:>7}", "bucket", "cycles", "%");
+ for (label, cycles) in labels.iter().zip(buckets.iter()) {
+ let pct = if total_cycles > 0 {
+ 100.0 * (*cycles as f64) / (total_cycles as f64)
+ } else {
+ 0.0
+ };
+ eprintln!(" {:<60} {:>14} {:>6.2}%", label, cycles, pct);
+ }
+}
+
+/// Single-pass execute-only profiler. Always prints total cycles + a rough
+/// trace/LDE estimate; with `detailed`, also the top-25 functions + per-step
+/// breakdown (one streamed pass). `!detailed` does no per-log work.
+fn run_profile(
+ guest_name: &str,
+ progress_stride: usize,
+ opts: stark::proof::options::ProofOptions,
+ detailed: bool,
+) {
+ use std::collections::HashMap;
+
+ let (guest_elf_bytes, _program, mut executor) = setup_guest_run("profile", guest_name, &opts);
+ let symbols = executor::elf::SymbolTable::parse(&guest_elf_bytes);
+
+ let mut pc_hist: HashMap = HashMap::new();
+ let mut buckets = [0u64; 5];
+ let mut last_range: Option<(u64, u64)> = None;
+ let mut last_advance: u8 = 0;
+ let bucket = std::cell::Cell::new(0u8);
+ let unique = std::cell::Cell::new(0usize);
+
+ if detailed {
+ assert!(
+ !symbols.is_empty(),
+ "{guest_name} ELF has no symbol table — was it stripped?"
+ );
+ for (i, kw) in VERIFIER_STEP_KEYWORDS.iter().enumerate() {
+ let n = symbols
+ .functions()
+ .iter()
+ .filter(|f| f.name.contains(kw))
+ .count();
+ eprintln!(
+ "[profile] step {}: keyword={kw:?} -> {n} symbol(s) {}",
+ i + 1,
+ if n > 0 {
+ ""
+ } else {
+ "(no match; merges into previous bucket)"
+ },
+ );
+ }
+ }
+
+ eprintln!(
+ "[profile] executing {guest_name} guest ({}) ...",
+ if detailed {
+ "histogram + steps"
+ } else {
+ "cycle counter"
+ }
+ );
+ let (total_cycles, exec_time) = drive_executor(
+ &mut executor,
+ |log| {
+ if detailed {
+ let pc = log.current_pc;
+ *pc_hist.entry(pc).or_insert(0) += 1;
+ unique.set(pc_hist.len());
+
+ let in_cached = matches!(last_range, Some((s, e)) if pc >= s && pc < e);
+ if !in_cached {
+ if let Some(sym) = symbols.lookup(pc) {
+ last_range = Some((sym.address, sym.address + sym.size.max(1)));
+ last_advance = 0;
+ for (i, kw) in VERIFIER_STEP_KEYWORDS.iter().enumerate() {
+ if sym.name.contains(kw) {
+ last_advance = (i + 1) as u8;
+ }
+ }
+ } else {
+ last_range = None;
+ last_advance = 0;
+ }
+ }
+ if bucket.get() < last_advance {
+ bucket.set(last_advance);
+ }
+ buckets[bucket.get() as usize] += 1;
+ }
+ ControlFlow::Continue(())
+ },
+ |chunks, cycles, elapsed| {
+ if chunks.is_multiple_of(progress_stride) {
+ if detailed {
+ eprintln!(
+ "[profile] ... {chunks} chunks, {cycles} cycles, {} unique PCs, bucket={}, {elapsed:?}",
+ unique.get(),
+ bucket.get(),
+ );
+ } else {
+ eprintln!("[profile] ... {chunks} chunks, {cycles} cycles, {elapsed:?}");
+ }
+ }
+ },
+ );
+
+ eprintln!();
+ eprintln!("============================================================");
+ eprintln!(
+ " {} GUEST PROFILE (blowup={}, {} queries)",
+ guest_name.to_uppercase(),
+ opts.blowup_factor,
+ opts.fri_number_of_queries,
+ );
+ eprintln!("============================================================");
+ eprintln!(" Total cycles : {total_cycles}");
+ eprintln!(" Exec time : {exec_time:?}");
+ eprintln!();
+ eprintln!(" Rough trace/LDE size if this guest were proven:");
+ let approx_columns = 250u64;
+ let main_trace_bytes = total_cycles * approx_columns * 8;
+ eprintln!(
+ " main trace : ~{:.2} GB ({total_cycles} cycles × ~{approx_columns} cols × 8 B)",
+ main_trace_bytes as f64 / 1e9,
+ );
+ eprintln!(
+ " main LDE (blowup=2) : ~{:.2} GB (+aux ≈ 50% more → peak ≈ 2-3× LDE)",
+ (main_trace_bytes * 2) as f64 / 1e9,
+ );
+
+ if detailed {
+ eprintln!();
+ print_function_table(&symbols, pc_hist, total_cycles);
+ print_step_breakdown(&buckets, total_cycles);
+ }
+ eprintln!("============================================================");
+}
+
+/// Core pipeline: prove the inner program, run the guest to `mode`, assert it
+/// committed `[1]` (the in-VM verifier accepted the proof).
fn run_recursion_pipeline_with_options(
label: &str,
inner_elf_bytes: &[u8],
@@ -198,8 +459,7 @@ fn run_recursion_pipeline_with_options(
eprintln!("[{label}] guest committed [1]: in-VM verify accepted ✓");
}
-/// Convenience wrapper using `blowup=8` for the inner proof — the default for
-/// the `empty` and `fibonacci` cases, chosen to keep outer-prove memory tractable.
+/// `run_recursion_pipeline_with_options` with `blowup=8` (the `empty`/`fibonacci` default).
fn run_recursion_pipeline(
label: &str,
inner_elf_bytes: &[u8],
@@ -217,9 +477,8 @@ fn run_recursion_pipeline(
);
}
-/// Reproduce the recursion guest's EXACT path on the host — decode the postcard
-/// blob into `(VmProof, Vec, ProofOptions)` and call `verify_with_options`.
-/// Cheap regression guard.
+/// Decode the blob on the host and verify — a cheap guard on the encode/decode
+/// contract without running the VM.
#[test]
#[ignore = "needs prebuilt guest ELF (make compile-recursion-elfs)"]
fn test_recursion_blob_decodes_and_verifies_on_host() {
@@ -252,8 +511,7 @@ fn test_recursion_blob_decodes_and_verifies_on_host() {
// === Execute-only tier ========================================================
-/// Execute-only mirror of `test_recursion_prove_empty`: verify a `blowup=8`
-/// proof of the empty program in-VM.
+/// Execute-only: verify a `blowup=8` proof of the empty program in-VM.
#[test]
#[ignore = "slow: runs the in-VM STARK verifier (minutes on CI)"]
fn test_recursion_execute_empty() {
@@ -267,8 +525,7 @@ fn test_recursion_execute_empty() {
);
}
-/// Execute-only mirror of `test_recursion_prove_1query`: smallest possible
-/// inner proof (blowup=2, 1 query) → least guest work.
+/// Execute-only: smallest inner proof (blowup=2, 1 query) → least guest work.
#[test]
#[ignore = "slow: runs the in-VM STARK verifier (minutes on CI)"]
fn test_recursion_execute_1query() {
@@ -283,8 +540,7 @@ fn test_recursion_execute_1query() {
);
}
-/// Execute-only mirror of `test_recursion_prove`: verify a `blowup=8` proof of
-/// fibonacci(10) in-VM.
+/// Execute-only: verify a `blowup=8` proof of fibonacci(10) in-VM.
#[test]
#[ignore = "slow: runs the in-VM STARK verifier (minutes on CI)"]
fn test_recursion_execute() {
@@ -304,8 +560,7 @@ fn test_recursion_execute() {
// === Full-prove tier ==========================================================
-/// Inner program: empty (halt immediately). Useful for measuring the
-/// verifier's intrinsic recursion overhead.
+/// Inner program: empty — the verifier's intrinsic recursion overhead.
#[test]
#[ignore = "slow: memory-bounded continuation prove of the verifier-in-VM"]
fn test_recursion_prove_empty() {
@@ -319,8 +574,7 @@ fn test_recursion_prove_empty() {
);
}
-/// Inner program: empty, but with the absolute-minimum FRI parameters
-/// (blowup=2, **fri_number_of_queries=1**). For quick profiling only.
+/// Inner program: empty, blowup=2/1-query. Quick profiling only.
#[test]
#[ignore = "slow: memory-bounded continuation prove of the verifier-in-VM"]
fn test_recursion_prove_1query() {
@@ -336,6 +590,289 @@ fn test_recursion_prove_1query() {
);
}
+/// Dump the guest's private-input blob to `/tmp/recursion_input.bin` for the
+/// CLI's `execute --flamegraph`.
+#[test]
+#[ignore = "diagnostic: writes recursion private input to /tmp/recursion_input.bin"]
+fn test_dump_recursion_input() {
+ let root = workspace_root();
+ let empty_elf_bytes = read_guest_elf(&root, "empty");
+
+ let (_inner_proof, blob) =
+ prove_inner_and_encode_blob("dump-input", &empty_elf_bytes, &[], &MIN_PROOF_OPTIONS);
+
+ let path = "/tmp/recursion_input.bin";
+ std::fs::write(path, &blob).expect("write blob");
+ eprintln!("[dump-input] wrote {} bytes to {path}", blob.len());
+}
+
+/// Cycle count only of the recursion guest verifying a 1-query inner proof.
+#[test]
+#[ignore = "diagnostic: fast; recursion guest cycle count (1 query)"]
+fn test_recursion_cycles_1query() {
+ run_profile("recursion", 500, MIN_PROOF_OPTIONS, false);
+}
+
+/// Cycle count only at 128-bit security: more FRI queries → more verifier cycles.
+#[test]
+#[ignore = "diagnostic: fast; recursion guest cycle count (multi-query)"]
+fn test_recursion_cycles_multiquery() {
+ run_profile("recursion", 500, blowup8(), false);
+}
+
+/// Full profile (top-25 + per-step) of the 1-query run.
+#[test]
+#[ignore = "diagnostic: ~8 min; recursion guest histogram + steps (1 query)"]
+fn test_recursion_profile_1query() {
+ run_profile("recursion", 500, MIN_PROOF_OPTIONS, true);
+}
+
+/// Full profile at 128-bit security: weight shifts toward per-query FRI/Merkle.
+#[test]
+#[ignore = "diagnostic: heavy; recursion guest histogram + steps (multi-query)"]
+fn test_recursion_profile_multiquery() {
+ run_profile("recursion", 500, blowup8(), true);
+}
+
+/// Count the distinct 4 KB pages the guest touches (code/heap/input/stack) — a
+/// proxy for the prover's per-page PAGE-table overhead, without running it.
+#[test]
+#[ignore = "diagnostic: counts distinct 4 KB memory pages touched by the recursion guest"]
+fn test_recursion_page_count() {
+ use executor::vm::memory::PRIVATE_INPUT_START_INDEX;
+ use std::collections::HashSet;
+
+ let (_bytes, program, mut executor) =
+ setup_guest_run("page-count", "recursion", &MIN_PROOF_OPTIONS);
+
+ // Precompute the recursion ELF's PT_LOAD ranges so we can bucket code/
+ // static pages separately from heap. `Elf::load` already expands BSS
+ // (memsz > filesz) into zero-valued words, so these ranges cover
+ // .text + .rodata + .data + .bss.
+ let segment_ranges: Vec<(u64, u64)> = program
+ .data
+ .iter()
+ .map(|seg| (seg.base_addr, seg.base_addr + (seg.values.len() as u64 * 4)))
+ .collect();
+ eprintln!(
+ "[page-count] recursion ELF: {} PT_LOAD segment(s)",
+ segment_ranges.len(),
+ );
+ for (i, (lo, hi)) in segment_ranges.iter().enumerate() {
+ eprintln!(
+ "[page-count] segment[{i}]: 0x{lo:016x} .. 0x{hi:016x} ({} bytes)",
+ hi - lo,
+ );
+ }
+
+ // Stream through execution — running to completion via `Executor::run`
+ // would accumulate ~67 M `Log` records (~2.7 GB) we don't need. We only
+ // care about the *final* memory state.
+ eprintln!("[page-count] executing recursion guest (streaming) ...");
+ let (total_cycles, exec_time) = drive_executor(
+ &mut executor,
+ |_log| ControlFlow::Continue(()),
+ log_progress("page-count", 50),
+ );
+
+ // Collect the set of distinct 4 KB pages from every cell touched during
+ // (a) program loading, (b) private-input loading, (c) execution.
+ const PAGE_MASK: u64 = !0xFFFu64;
+ let cells = executor.memory().cells();
+ let total_cells = cells.len();
+ let pages: HashSet = cells.keys().map(|&a| a & PAGE_MASK).collect();
+
+ // Bucket by region. A "code/static" page is any page that overlaps a
+ // PT_LOAD segment. Stack lives near the top of the 64-bit address
+ // space; private input lives in the [0xFF000000, ...) window above the
+ // 3 GB heap ceiling.
+ const HEAP_CEILING: u64 = 0xC000_0000;
+ const STACK_FLOOR: u64 = 0xFFFF_FFFF_0000_0000;
+
+ let mut code_pages = 0usize;
+ let mut heap_pages = 0usize;
+ let mut private_input_pages = 0usize;
+ let mut stack_pages = 0usize;
+ let mut other_pages = 0usize;
+
+ for &page in &pages {
+ let page_end = page.saturating_add(0x1000);
+ let in_code = segment_ranges
+ .iter()
+ .any(|&(lo, hi)| page < hi && lo < page_end);
+ if in_code {
+ code_pages += 1;
+ } else if page >= STACK_FLOOR {
+ stack_pages += 1;
+ } else if page >= PRIVATE_INPUT_START_INDEX {
+ private_input_pages += 1;
+ } else if page < HEAP_CEILING {
+ heap_pages += 1;
+ } else {
+ other_pages += 1;
+ }
+ }
+
+ eprintln!();
+ eprintln!("============================================================");
+ eprintln!(" RECURSION GUEST PAGE-COUNT SUMMARY");
+ eprintln!("============================================================");
+ eprintln!(" Total cycles : {total_cycles}");
+ eprintln!(" Executor wall time : {exec_time:?}");
+ eprintln!(" Memory cells touched (4 B ea) : {total_cells}");
+ eprintln!(" Distinct 4 KB pages touched : {}", pages.len());
+ eprintln!();
+ eprintln!(" Pages per region:");
+ eprintln!(" code/static (ELF segments) : {code_pages}");
+ eprintln!(" heap (0..0xC000_0000) : {heap_pages}");
+ eprintln!(" private input (0xFF000000..) : {private_input_pages}");
+ eprintln!(" stack (>= 0xFFFFFFFF_00000000) : {stack_pages}");
+ if other_pages > 0 {
+ eprintln!(" other (unclassified) : {other_pages}");
+ }
+ eprintln!();
+ eprintln!(" Interpretation (PAGE-table overhead):");
+ eprintln!(" <1k pages → PAGE overhead is not the bottleneck.");
+ eprintln!(" 10k-100k → TLSF heap fragmentation; try a bump alloc.");
+ eprintln!(" >100k → postcard decode dominates; stream-decode?");
+ eprintln!("============================================================");
+}
+
+/// Sampled call-stack flamegraph of the recursion guest, written to
+/// `/tmp/recursion_folded_sampled.txt` (inferno "folded stacks" format).
+#[test]
+#[ignore = "diagnostic: sampled flamegraph for the verifier-in-VM"]
+fn test_recursion_sampled_flamegraph() {
+ use executor::flamegraph::FlamegraphGenerator;
+ use std::io::BufWriter;
+
+ /// 1-in-N logs sampled. >1 desyncs the call stack on skipped CALL/RETURNs,
+ /// so keep at 1 unless stack accuracy is expendable.
+ const SAMPLE_RATE: usize = 1;
+
+ /// Stop after this many cycles (0 = run to completion).
+ const CYCLE_BUDGET: u64 = 5_000_000;
+
+ let (recursion_elf_bytes, program, mut executor) =
+ setup_guest_run("sampled-fg", "recursion", &MIN_PROOF_OPTIONS);
+
+ eprintln!("[sampled-fg] executing recursion guest (sampling 1-in-{SAMPLE_RATE}) ...",);
+ let symbols = executor::elf::SymbolTable::parse(&recursion_elf_bytes);
+ let entry_point = program.entry_point;
+
+ // Build our own instruction cache from the same segments `Executor::new`
+ // decodes internally. Owning it (rather than reading `executor.instructions`
+ // mid-loop) is what lets the per-log closure call `process_logs` without
+ // borrowing `executor`, which `drive_executor` holds mutably for `resume()`.
+ let instructions = executor::vm::execution::InstructionCache::new(&program.data)
+ .expect("instruction cache build failed");
+
+ // RefCell so the per-log closure (`process_logs`, &mut self) and the
+ // progress closure (`write_folded`, &self) can both reach the generator —
+ // their calls never overlap, so the runtime borrow check never trips.
+ let generator = std::cell::RefCell::new(FlamegraphGenerator::new(symbols, entry_point));
+
+ // Path is defined here (not after the loop) so the periodic checkpoint
+ // writes below can target it. The final write at the end still happens.
+ let path = "/tmp/recursion_folded_sampled.txt";
+
+ let mut i = 0usize;
+ let (total_cycles, exec_time) = drive_executor(
+ &mut executor,
+ |log| {
+ // 1-in-SAMPLE_RATE logs are fed to `process_logs`. At SAMPLE_RATE==1
+ // this is the identity filter (`_ % 1 == 0`); the `#[allow]` keeps
+ // the general form so SAMPLE_RATE can be bumped without touching the
+ // body. Skipped logs lose stack accuracy — acceptable diagnostic
+ // quality at higher rates.
+ #[allow(clippy::modulo_one)]
+ let take = i.is_multiple_of(SAMPLE_RATE);
+ if take {
+ generator
+ .borrow_mut()
+ .process_logs(std::slice::from_ref(log), &instructions)
+ .expect("flamegraph process_logs");
+ }
+ i += 1;
+
+ // Early exit once we've covered the cycle budget. The dominant hot
+ // kernels are ~uniform across the verifier's runtime, so a partial
+ // run still surfaces them. `#[allow]` lets CYCLE_BUDGET be const-0
+ // (full run) without tripping clippy.
+ #[allow(clippy::absurd_extreme_comparisons)]
+ if CYCLE_BUDGET > 0 && i as u64 >= CYCLE_BUDGET {
+ eprintln!("[sampled-fg] hit cycle budget ({CYCLE_BUDGET} cycles), stopping early");
+ ControlFlow::Break(())
+ } else {
+ ControlFlow::Continue(())
+ }
+ },
+ |chunks, cycles, elapsed| {
+ if chunks.is_multiple_of(500) {
+ eprintln!(
+ "[sampled-fg] ... {chunks} chunks, {cycles} cycles, {elapsed:?} elapsed"
+ );
+ // Checkpoint: re-write the folded file in place so a killed run
+ // still leaves a usable (if partial) flamegraph on disk.
+ let file = std::fs::File::create(path).expect("create output file");
+ let mut writer = BufWriter::new(file);
+ generator
+ .borrow()
+ .write_folded(&mut writer)
+ .expect("write folded output");
+ }
+ },
+ );
+
+ let file = std::fs::File::create(path).expect("create output file");
+ let mut writer = BufWriter::new(file);
+ generator
+ .borrow()
+ .write_folded(&mut writer)
+ .expect("write folded output");
+
+ eprintln!();
+ eprintln!("============================================================");
+ eprintln!(" SAMPLED FLAMEGRAPH SUMMARY");
+ eprintln!("============================================================");
+ eprintln!(" Total cycles : {total_cycles}");
+ eprintln!(" Sample rate : 1 in {SAMPLE_RATE}");
+ eprintln!(" Exec time : {exec_time:?}");
+ eprintln!(" Output file : {path}");
+ eprintln!("============================================================");
+ eprintln!();
+ eprintln!(" To render SVG (requires inferno):");
+ eprintln!(" cat {path} | inferno-flamegraph > /tmp/recursion_flamegraph_sampled.svg");
+ eprintln!("============================================================");
+}
+
+// Control guest: decodes the blob and halts. Its cycle count subtracted from
+// the matching recursion run isolates the in-VM verifier cost.
+
+#[test]
+#[ignore = "diagnostic: fast; deserialize-only guest cycle count (1 query)"]
+fn test_deserialize_only_cycles_1query() {
+ run_profile("deserialize-only", 50, MIN_PROOF_OPTIONS, false);
+}
+
+#[test]
+#[ignore = "diagnostic: fast; deserialize-only guest cycle count (multi-query)"]
+fn test_deserialize_only_cycles_multiquery() {
+ run_profile("deserialize-only", 50, blowup8(), false);
+}
+
+#[test]
+#[ignore = "diagnostic: ~1 min; deserialize-only guest histogram (1 query)"]
+fn test_deserialize_only_profile_1query() {
+ run_profile("deserialize-only", 50, MIN_PROOF_OPTIONS, true);
+}
+
+#[test]
+#[ignore = "diagnostic: deserialize-only guest histogram (multi-query)"]
+fn test_deserialize_only_profile_multiquery() {
+ run_profile("deserialize-only", 50, blowup8(), true);
+}
+
/// Inner program: fibonacci(10).
#[test]
#[ignore = "slow: memory-bounded continuation prove of the verifier-in-VM"]