Skip to content

feat/dma memcpy - #874

Open
jotabulacios wants to merge 18 commits into
mainfrom
feat/dma-memcpy
Open

feat/dma memcpy#874
jotabulacios wants to merge 18 commits into
mainfrom
feat/dma-memcpy

Conversation

@jotabulacios

@jotabulacios jotabulacios commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What

The guest's out-of-line memcpy becomes a DMA ecall: the executor performs the copy
natively and a dedicated AIR table proves it. Copies are chunked at 256 bytes.

Why

A copy ran as a RISC-V loop — per eight bytes a load, a store, two pointer increments and
a branch, each one a CPU row plus its memory operations. The DMA table replaces all of
that with one row per eight bytes: the source read at T+1 and the destination write at T+2
share the same value columns, so a copied byte cannot change without unbalancing the memory
bus. Both sides enforce the 256-byte bound — the executor rejects larger ecalls, the AIR
proves count < 257 on every first row — so one guest instruction cannot add an unbounded
number of rows to a continuation epoch.

memcpy and memcmp are ~37% of the guest's cycle budget, and memcpy's absolute cost
survived every earlier optimization untouched. The cheaper approach was tried first and
failed: overriding the compiler builtins with hand-written rv64 musl assembly regressed
13.9%
, because compiler_builtins already word-copies with shift-merge and a naive
word-copy-with-byte-fallback loses on mutually-misaligned buffers. Moving the copies off
the CPU trace is what is left.

Impact (ethrex 20-tx block, distinct senders, vs main at e0add1d5)

  • Guest cycles: 9,111,046 → 8,197,497 (−10.0%); per transfer, empty block subtracted,
    415,544 → 379,600 (−8.7%)
  • Prove: 25.309 s → 23.831 s (−5.8%); peak heap 50,856 → 48,584 MB (−4.5%)
  • Smaller blocks gain more (−22.5% on an empty block, −11.0% at 10 tx): the copy traffic
    sits mostly in per-block setup, so the ratio converges to ~8.7% rather than to zero
  • Keccak permutations (411) and ECSM calls (80) are identical on both sides: this moves
    memory traffic, it does not skip work
  • Cycles are deterministic — same figures on a laptop and on the bench machine

Breaking

Adds a fixed table: FIXED_TABLE_COUNT goes 10 → 11, so the set of tables in a proof
changes. Prover and verifier must be deployed together, and earlier binaries cannot verify
these proofs. Note that #876 (hint ecall) independently takes the same constant 10 → 11, so
whichever of the two lands second rebases onto 12.

Validation

Both DMA guests prove and verify. Forgeries are rejected: altered copied byte, source row
skipped forward, early end, wrong row width. Plus a 256-case differential fuzz over
overlap, alignment and page crossings, a guest walking lengths 0–256 and a multi-chunk
copy, the length-drift test with a non-empty DMA table, and make lint.

Changes (32 files)

  • executor/src/vm/instruction/execution.rs: the ecall — operand validation, 256-byte
    bound, copy through a fixed scratch (snapshot semantics on overlap).
  • prover/src/tables/dma.rs: the table — rows chained through DmaNext, Zero for end
    detection, LT for the 1-vs-8-byte width and the per-call bound.
  • prover/src/constraints/templates.rs: emit_add_pair_no_overflow, so address
    transitions cannot wrap modulo 2^64.
  • syscalls/src/syscalls.rs: strong assembly memcpy symbol that chunks into ecalls and
    preserves the C return value.
  • Tests and two guests (dma_memcpy_min, dma_memcpy_cases).

@jotabulacios

Copy link
Copy Markdown
Collaborator Author

/bench

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

Benchmark — real block (ethrex_mainnet_25368371.bin) (median of 3)

continuations · epoch 2^22 · 10 epochs

Metric main PR Δ
Peak heap 47024 MB 47757 MB +733 MB (+1.6%) ⚪
Prove time 157.521s 139.224s -18.297s (-11.6%) 🟢

🎉 Improvement on the real block — prove time down 11.6%.

Prove-time spread 0.9% (138.278s / 139.224s / 139.550s)

Commit: 6b6125e · Baseline: cached · Runner: self-hosted bench

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

Benchmark Results for modified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
head ecsm 3.5 ± 0.2 3.3 3.8 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head hashmap 116.7 ± 6.0 110.7 129.0 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head keccak 130.2 ± 3.4 125.7 135.5 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head syscall_commit 94.0 ± 1.2 92.2 95.2 1.00

@jotabulacios

Copy link
Copy Markdown
Collaborator Author

/bench

@github-actions

Copy link
Copy Markdown

Benchmark Results for unmodified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
base binary_search 53.6 ± 10.8 49.4 84.3 1.03 ± 0.22
head binary_search 51.8 ± 2.6 49.6 58.2 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base bitwise_ops 50.5 ± 0.6 50.0 51.5 1.00
head bitwise_ops 50.8 ± 0.6 50.2 51.8 1.01 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base fibonacci_26 52.3 ± 0.6 51.5 53.1 1.00 ± 0.02
head fibonacci_26 52.3 ± 0.6 51.6 53.3 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base matrix_multiply 52.5 ± 0.8 51.7 54.0 1.00
head matrix_multiply 52.5 ± 0.6 51.9 53.5 1.00 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base modular_exp 50.3 ± 0.8 49.5 51.6 1.00
head modular_exp 50.6 ± 0.7 49.6 51.4 1.01 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base quicksort 52.8 ± 0.4 52.5 53.7 1.00
head quicksort 53.3 ± 0.6 52.7 54.6 1.01 ± 0.01
Command Mean [ms] Min [ms] Max [ms] Relative
base sieve 54.0 ± 0.5 53.5 54.8 1.00
head sieve 56.8 ± 3.4 53.7 63.3 1.05 ± 0.06
Command Mean [ms] Min [ms] Max [ms] Relative
base sum_array 61.8 ± 0.5 61.2 62.4 1.00
head sum_array 62.2 ± 0.6 61.5 63.0 1.01 ± 0.01

@jotabulacios jotabulacios changed the title experimental/dma memcpy feat/dma memcpy Jul 29, 2026
@jotabulacios

Copy link
Copy Markdown
Collaborator Author

/ai-review

@github-actions

Copy link
Copy Markdown

Codex Code Review

No actionable issues found in the PR changes. Static review only; no builds or tests run per instructions.

@jotabulacios
jotabulacios marked this pull request as ready for review July 29, 2026 15:30
@github-actions

Copy link
Copy Markdown

AI Review

PR #874 · 32 changed files

Findings

Status Sev Location Finding Found by
confirmed medium executor/src/vm/instruction/execution.rs:479 DMA ecall address overflow check is off-by-one kimi
openrouter/moonshotai/kimi-k2.7-code
confirmed low prover/src/constraints/templates.rs:376 Misleading docstring in emit_add_pair_no_overflow kimi
openrouter/moonshotai/kimi-k2.7-code

Status column reflects the verdict from the verifier: deepseek-verifier (openrouter/deepseek/deepseek-v4-pro).

AI-001: DMA ecall address overflow check is off-by-one
  • Status: confirmed
  • Severity: medium
  • Location: executor/src/vm/instruction/execution.rs:479
  • Found by: kimi:openrouter/moonshotai/kimi-k2.7-code
  • Verified by: deepseek-verifier:openrouter/deepseek/deepseek-v4-pro
  • Rejected by: -

Claim

The DMA memcpy ecall rejects valid copies where the final accessed byte is exactly u64::MAX. It checks dst.checked_add(n) and src.checked_add(n), which require one-past-the-end to fit, but only bytes in [addr, addr+n) are accessed. The correct condition is that addr + (n-1) does not overflow.

Evidence

Lines 479-480 use dst.checked_add(n).ok_or(MemoryError::AddressOverflow)? and src.checked_add(n).ok_or(MemoryError::AddressOverflow)?. For n>0, the last byte accessed is at addr+n-1, so a copy with addr = u64::MAX - n + 1 is valid but is rejected because addr + n overflows. This matches the prover's per-row no-overflow constraints, so it is a completeness/consistency edge-case bug rather than a soundness issue.

Suggested fix

Change both checks to dst.checked_add(n.saturating_sub(1)).ok_or(MemoryError::AddressOverflow)? (and similarly for src), which is equivalent for n>0 and passes for n=0.

AI-003: Misleading docstring in emit_add_pair_no_overflow
  • Status: confirmed
  • Severity: low
  • Location: prover/src/constraints/templates.rs:376
  • Found by: kimi:openrouter/moonshotai/kimi-k2.7-code
  • Verified by: deepseek-verifier:openrouter/deepseek/deepseek-v4-pro
  • Rejected by: -

Claim

The docstring says the constraint fires "while active - end == 1", but the implementation computes active = main(active_column) - main(end_column) and callers pass active_column = MU, end_column = END. The phrase collides with the local variable name and does not describe the actual arithmetic condition (mu - end == 1).

Evidence

Lines 376-381 describe the condition as active - end == 1; lines 401-402 compute let active = b.main(0, active_column) - b.main(0, end_column); and in prover/src/tables/dma.rs the helper is invoked with active_column = cols::MU and end_column = cols::END.

Suggested fix

Reword the docstring to describe the actual condition, e.g. "while mu - end == 1 (i.e. active, non-terminal rows)", or rename the parameter/variable to avoid the collision.

Reviewer Lanes

Lane Model Prompt Status Findings
glm openrouter/z-ai/glm-5.2 general success 0
kimi openrouter/moonshotai/kimi-k2.7-code general success 2
minimax minimax/MiniMax-M3 general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0
moonmath zro/minimax-m3 general error: agentic lane timed out after 1800s 0
nemotron openrouter/nvidia/nemotron-3-ultra-550b-a55b general success 3

Verification Lanes

Lane Model Status Confirmed Rejected Uncertain
deepseek-verifier openrouter/deepseek/deepseek-v4-pro success 2 3 0

Native Codex and Claude reviews run separately and post their own comments. They are not included in this structured provenance report.

Discarded candidates (3) — rejected by the verifier
  • New emit_add_pair_no_overflow constraint critical for address wrap prevention (prover/src/constraints/templates.rs:330, found by nemotron:openrouter/nvidia/nemotron-3-ultra-550b-a55b) — This finding does not identify any actual bug or issue. It merely describes what the new emit_add_pair_no_overflow constraint does ('constrains high carry to zero on active non-terminal rows'), notes it is security-critical, and states it 'must be correct.' Both the claim and evidence acknowledge the logic 'appears correct.' This is an observation, not an actionable finding.
  • Register timestamp advancement in collect_dma_memcpy_ops may affect subsequent register accesses (prover/src/tables/trace_builder.rs:1080, found by nemotron:openrouter/nvidia/nemotron-3-ultra-550b-a55b) — The finding acknowledges the behavior is correct: the register_state.write calls update timestamps properly for the MEMW register read protocol. The evidence even states 'This is correct for MEMW register read protocol.' The CPU's collect_register_ops_from_cpu does not generate duplicate MEMW ops for ecall arguments because decode doesn't mark them as read_register — so DMA's explicit reads and timestamp updates are the sole source of these MEMW entries, making the design consistent. No bug is identified; this is a design remark, not an issue.
  • Assembly memcpy const interpolation relies on compile-time constant evaluation (syscalls/src/syscalls.rs:185, found by nemotron:openrouter/nvidia/nemotron-3-ultra-550b-a55b) — This is a statement about Rust language semantics, not an actual code issue. The DMA_MEMCPY_SYSCALL_NUMBER and DMA_MEMCPY_MAX_BYTES are both defined as const usize at lines 147-148, and the global_asm! macro at lines 235-236 correctly uses const interpolation to reference them. The claim that 'any change to make them non-const would break the assembly' is a tautology about Rust's const evaluation — it's not identifying a problem with the current code, which is correct as written.

Raw lane outputs, candidates, final issues, and model metrics are uploaded as workflow artifacts.

@nicole-graus

Copy link
Copy Markdown
Collaborator

/bench-verify

@github-actions

Copy link
Copy Markdown

Benchmark started on the bench server. The recursion-guest cycle comparison adds guest builds on top of the verifier bench, longer on a cold runner. The bench server is occupied until it finishes.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Verifier benchmark — c303daa759 vs main (20 pairs, monolithic + continuations)

ethrex 20-tx block · monolithic · blowup=2, 219 queries

Metric main PR Δ
Verify time (ABBA, 20 pairs, per-side) 2.976s 3.054s +2.60% 🔴
Proof size (exact, 1 reading) 155.38 MiB 155.72 MiB +0.22% 🔴

Per-side (⚠️ PR REJECTS the baseline's valid proof — likely a VERIFY REGRESSION, not a format change): A/B/B/A cancels machine drift but not proof-specific variance — read the Verify-time Δ as approximate.

  pairs: 20   mean A (PR): 3.054s   mean B (main): 2.976s
  [parametric] paired-t   mean +2.60%   sd 0.99%   se 0.22%
               95% CI: [+2.13%, +3.06%]   (t df=19 = 2.093)
  [robust]     median +2.81%   Wilcoxon W+=209 W-=1  p(exact)=3.8e-06  (z=+3.86)

  run-to-run jitter:    A CV 0.84%   B CV 0.51%        (lower = steadier)
  within-session drift: +0.24% over the run, 1st->2nd half +0.20%

🔴 REAL REGRESSION — PR verifies ~2.60% slower (paired-t and Wilcoxon agree).

ethrex 20-tx block · continuations, epoch 2^20 (9 epochs) · blowup=2, 219 queries

Metric main PR Δ
Verify time (ABBA, 8 pairs, per-side) 7.940s 8.176s +2.98% 🔴
Proof size (exact, 1 reading) 474.84 MiB 477.89 MiB +0.64% 🔴

Per-side (⚠️ PR REJECTS the baseline's valid proof — likely a VERIFY REGRESSION, not a format change): A/B/B/A cancels machine drift but not proof-specific variance — read the Verify-time Δ as approximate.

  pairs: 8   mean A (PR): 8.176s   mean B (main): 7.940s
  [parametric] paired-t   mean +2.98%   sd 0.29%   se 0.10%
               95% CI: [+2.73%, +3.23%]   (t df=7 = 2.365)
  [robust]     median +2.89%   Wilcoxon W+=36 W-=0  p(exact)=0.0078  (z=+2.45)

  run-to-run jitter:    A CV 0.20%   B CV 0.22%        (lower = steadier)
  within-session drift: -0.02% over the run, 1st->2nd half +0.04%

🔴 REAL REGRESSION — PR verifies ~2.98% slower (paired-t and Wilcoxon agree).

Verify-time rows only: drift-free interleaved A/B/B/A, with paired-t and exact Wilcoxon — trust the verdict when the two agree. Proof sizes are single exact readings (no averaging). - = PR faster.


Recursion guest cycles — verifier running INSIDE the VM (main vs PR)

empty program · monolithic · blowup=2, 1 query (diagnostic — NOT a real verifier cost)

Single exact reading per ref — no ABBA: guest cycles are deterministic for a fixed
(guest ELF, input blob), so there is no machine drift to cancel.

Metric main PR Δ
Guest cycles 468.4M 469.2M +0.8M (+0.18%)
Keccak calls 3025 3057 +32
  baseline  origin/main  74b6c4994d  guest=recursion-min.elf
  PR        c303daa7593010b58cfceecb616b410b51fe74c0  c303daa759  guest=recursion-min.elf
  note: cycles reproduce to ~±100k (build codegen + proof nondeterminism);
        treat sub-100k deltas as noise, not signal.
raw (exact integer counts)
ref_b_sha=74b6c4994d5ce6174ae244a488be8e710802b5d5 ref_b_elf=recursion-min.elf ref_b_cycles=468364224 ref_b_keccak=3025 ref_b_execute_wall_s=9
ref_a_sha=c303daa7593010b58cfceecb616b410b51fe74c0 ref_a_elf=recursion-min.elf ref_a_cycles=469211700 ref_a_keccak=3057 ref_a_execute_wall_s=9
delta_cycles=847476 delta_keccak=32

ethrex 20-tx block · continuations, epoch 2^21 (main 5 / PR 4 epochs) · blowup=2, 219 queries (128-bit)

Single exact reading per ref — no ABBA: guest cycles are deterministic for a fixed
(guest ELF, input blob), so there is no machine drift to cancel.

Metric main PR Δ
Guest cycles 4910.9M 4187.4M -723.5M (-14.73%)
Keccak calls 6789809 6094533 -695276
  baseline  origin/main  74b6c4994d  guest=recursion-cont-blowup2.elf
  PR        c303daa7593010b58cfceecb616b410b51fe74c0  c303daa759  guest=recursion-cont-blowup2.elf
  note: cycles reproduce to ~±100k (build codegen + proof nondeterminism);
        treat sub-100k deltas as noise, not signal.
raw (exact integer counts)
ref_b_sha=74b6c4994d5ce6174ae244a488be8e710802b5d5 ref_b_elf=recursion-cont-blowup2.elf ref_b_cycles=4910856929 ref_b_keccak=6789809 ref_b_execute_wall_s=78
ref_a_sha=c303daa7593010b58cfceecb616b410b51fe74c0 ref_a_elf=recursion-cont-blowup2.elf ref_a_cycles=4187394004 ref_a_keccak=6094533 ref_a_execute_wall_s=69
delta_cycles=-723462925 delta_keccak=-695276

@nicole-graus

Copy link
Copy Markdown
Collaborator

/bench-verify

@github-actions

Copy link
Copy Markdown

Benchmark started on the bench server. The recursion-guest cycle comparison adds guest builds on top of the verifier bench, longer on a cold runner. The bench server is occupied until it finishes.

@jotabulacios

Copy link
Copy Markdown
Collaborator Author

/bench-verify

@github-actions

Copy link
Copy Markdown

Benchmark started on the bench server. Two verifier arms (monolithic + continuations over an ethrex 20-tx block), then the recursion-guest cycle comparison, which adds guest builds on top — longer on a cold runner. The bench server is occupied until it finishes.

@MauroToscano

Copy link
Copy Markdown
Contributor

/bench

@MauroToscano

Copy link
Copy Markdown
Contributor

/bench-gpu

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

GPU Benchmark (ABBA) — 888f175a45 vs main (14 pairs)

RTX 5090 · Vast.ai datacenter @ $0.6832407407407408/hr · prover/cuda · ethrex real block, continuations · drift-free A/B/B/A

=== ABBA paired result  (improvement: - = PR faster) ===
  pairs: 14   mean A (PR): 76.026s   mean B (base): 75.546s

  [parametric] paired-t   mean +0.64%   sd 1.20%   se 0.32%
               95% CI: [-0.05%, +1.34%]   (t df=13 = 2.16)
  [robust]     median +0.93%   Wilcoxon W+=82 W-=23  p(exact)=0.0676  (z=+1.82)

  --- server stability (this run; compare across servers) ---
  run-to-run jitter:    A CV 0.67%   B CV 0.88%        (lower = steadier)
  within-session drift: +0.24% over the run, 1st->2nd half -0.05%
    (jitter -> Tier-1 cached gate floor; drift -> whether the cached baseline can be trusted)

  VERDICT: INCONCLUSIVE - effect not separable from 0 at n=14.
           Point estimate ~+0.93% (median). Need more pairs to resolve.

  raw pairs: /tmp/abba_run/pairs.csv

- = PR faster. Trust the verdict when paired-t and Wilcoxon agree.

@jotabulacios

Copy link
Copy Markdown
Collaborator Author

/bench-verify

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Benchmark started on the bench server. Two verifier arms (monolithic + continuations over an ethrex 20-tx block), then the recursion-guest cycle comparison, which adds guest builds on top — longer on a cold runner. The bench server is occupied until it finishes.

@Oppen

Oppen commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Automated review pass (high-effort, adversarially verified). Findings:

  • bug: executor/src/vm/instruction/execution.rs SyscallNumbers::accelerator() returns None for DmaMemcpy. Add Some(Accelerator::Dma) (or equivalent) — cli execute --cycles currently attributes every 256-byte DMA copy to one ordinary cycle, hiding 33 DMA + 67 MEMW rows/call.
  • bug: Makefiletest-prover/test-fast/test-prover-debug/test-prover-all depend on compile-recursion-elfs only, not $(RUST_ARTIFACTS). Clean checkout panics "elf not found" for all nine new DMA tests.
  • risk: prover/src/auto_storage.rs:182 DMA table has no max_rows entry, unlike CPU/MEMW/LOAD. Height is proportional to user data; at epoch_size_log2=23 a pathological table projects ~17GB before blowup.
  • risk: epoch sizing (syscalls/src/syscalls.rs:210) is cycle-count-only. One DMA ecall can blow an epoch's row budget with zero cycle-count change.
  • risk: syscalls/src/syscalls.rs:212 .text.memcpy has no .p2align 2 (sh_addralign=1 measured). Survives today only by luck; one unrelated change lands memcpy at an odd alignment and the guest dies with an undecodable-instruction error.
  • risk: executor/src/vm/instruction/execution.rs:479 bounds guard dst.checked_add(n) / src.checked_add(n) is off by one vs. highest touched byte n-1. Stricter than the load/store sequence it replaces (no bounds check, succeeds at u64::MAX).
  • risk: syscalls/src/syscalls.rs:38 guest re-declares DMA_MEMCPY_SYSCALL_NUMBER/_MAX_BYTES as literals with a "must match" comment instead of importing the executor's.
  • risk: prover/src/tables/trace_builder.rs:979 assert!(count <= DMA_MEMCPY_MAX_BYTES) panics where sibling callers use Result/Error::Execution for the same class of guard.

Cross-cutting with 876/896: accelerator()None for a new chip is now 2-for-2 (874, 876); consider making the CLI parity test enumerate variants so a new one fails by default.

@jotabulacios

Copy link
Copy Markdown
Collaborator Author

/bench

@jotabulacios

Copy link
Copy Markdown
Collaborator Author

bug: executor/src/vm/instruction/execution.rs SyscallNumbers::accelerator() returns None for DmaMemcpy. Add Some(Accelerator::Dma) (or equivalent) — cli execute --cycles currently attributes every 256-byte DMA copy to one ordinary cycle, hiding 33 DMA + 67 MEMW rows/call.

Fixed in 2f902226 — there's now a Dma calls line next to Keccak calls / Ecsm calls.

One thing about the diagnosis, since it matters for what the counter means: the DMA ecall really is one cycle, so nothing was being mis-attributed, and --cycles has never reported row counts for any chip. What was missing was just the report line, so a DMA-heavy guest looked like it made no accelerator calls at all.


risk: syscalls/src/syscalls.rs:212 .text.memcpy has no .p2align 2 (sh_addralign=1 measured). Survives today only by luck; one unrelated change lands memcpy at an odd alignment and the guest dies with an undecodable-instruction error.

Fixed in d94561ac, and you were right about the "only by luck" part. I checked it against a real build to be sure: assembling the section as it was gives sh_addralign = 1, with the directive it gives 4. What saved us so far is that the linker merges into .text (align 4) and memcpy happened to land aligned anyway.


Cross-cutting with 876/896: accelerator()None for a new chip is now 2-for-2 (874, 876); consider making the CLI parity test enumerate variants so a new one fails by default.

Agreed that the pattern is the actual problem, so I went after it structurally instead of adding the missing arm and moving on (2f902226): SyscallNumbers is declared through a macro_rules! that generates ALL from the same variant list, and the CLI tallies with an exhaustive match on Accelerator. A new syscall variant now produces 3 compile errors, and a new Accelerator variant stops the CLI from compiling — I checked both by actually adding a variant.

One trap worth flagging if you suggest this elsewhere: the first guard I wrote sized a covered array by ALL.len(), and it still passed when I deleted the last variant of ALL — the array shrinks along with the list, so no hole is left behind. Without strum in the workspace, generating the list from a macro seems to be the only version that can't be fooled that way.


risk: prover/src/auto_storage.rs:182 DMA table has no max_rows entry, unlike CPU/MEMW/LOAD. Height is proportional to user data; at epoch_size_log2=23 a pathological table projects ~17GB before blowup.

risk: epoch sizing (syscalls/src/syscalls.rs:210) is cycle-count-only. One DMA ecall can blow an epoch's row budget with zero cycle-count change.

Quoting these together because they're the same gap from two sides, and I think both are real — I just don't think DMA is where to fix them. The whole accelerator family behaves this way:

  • KECCAK (keccak.rs:99), ECSM (ecsm.rs:153), COMMIT (commit.rs:164) and DMA (dma.rs:120) all pad with the same n.next_power_of_two().max(4). No max_rows, no chunking. max_rows covers the 14 core chips only.
  • Cycle-only epoch sizing already breaks rows ≈ cycles for keccak and ecsm today, so DMA isn't introducing that either.
  • One more I ran into while checking the above: KECCAK and ECSM don't appear in auto_storage's projection at all, so it under-projects them today.

Your arithmetic holds, by the way — DMA_COLS = 32, ~33 rows/ecall, ~8 cycles/ecall → a saturated 2^23 epoch is ~2^26 rows ≈ 17 GB pre-blowup, and ~4× that adversarially. Two things I'd add to it: per-ecall area lands within ~1.6× of keccak's, because keccak packs 511 columns into its single row, so the 33 rows aren't as dramatic as they look; and chunking DMA isn't a one-liner, since the DmaNext bus chains row→row inside a single copy. What does make DMA the first reachable case is call frequency — any guest memcpy triggers it — which is why I'd rather it drive a fix that also covers keccak/ecsm/commit than get a DMA-shaped cap. Opening that as its own issue and linking it here.


bug: Makefiletest-prover/test-fast/test-prover-debug/test-prover-all depend on compile-recursion-elfs only, not $(RUST_ARTIFACTS). Clean checkout panics "elf not found" for all nine new DMA tests.

The behaviour you describe is real, but it isn't new here: main already has 6 prover tests reading program_artifacts/rust/*.elf under those same four targets (prove_elfs_tests.rs:1189 ecsm, plus allocator, pure_commit, ef_io_demo, commit_sum, ethrex), each with the same .expect("… run make compile-programs-rust"). So a clean checkout fails there without this PR; the DMA tests join an existing convention rather than breaking a working target.

The reason it's set up that way is cost: the .elf rules are FORCE (Makefile:217, and the comment at :184 explains that cargo owns the dep graph), so hanging $(RUST_ARTIFACTS) off those targets rebuilds all 35 Rust guests on every local test run. CI calls make compile-programs-rust explicitly instead.

That said, I do think the local target should work out of the box, and a shared test-elfs prerequisite would do it. I'd just rather do it once for all of those tests than only for the DMA ones — happy to open it, or to take it here if you'd prefer it not wait.


risk: executor/src/vm/instruction/execution.rs:479 bounds guard dst.checked_add(n) / src.checked_add(n) is off by one vs. highest touched byte n-1. Stricter than the load/store sequence it replaces (no bounds check, succeeds at u64::MAX).

The off-by-one is there, agreed. The only input it turns away is a copy whose last byte sits exactly at u64::MAX, so it errors instead of accepting, and it's still stricter than the unchecked load/store sequence it replaces. I'd keep it unless you can see a legitimate copy that ends there — loosening it to n-1 would buy that one address back, and I'd rather a new ecall err on the closed side.


risk: syscalls/src/syscalls.rs:38 guest re-declares DMA_MEMCPY_SYSCALL_NUMBER/_MAX_BYTES as literals with a "must match" comment instead of importing the executor's.

This one I'd push back on: importing them would make lambda-vm-syscalls depend on executor, and that dependency points the wrong way — the guest-side crate is meant to be buildable without the host VM. KECCAK and ECSM are declared the same way two lines above, for the same reason, and a mismatch shows up immediately in the end-to-end guest tests rather than silently.


risk: prover/src/tables/trace_builder.rs:979 assert!(count <= DMA_MEMCPY_MAX_BYTES) panics where sibling callers use Result/Error::Execution for the same class of guard.

I'd argue it isn't the same class. The sibling guards validate values that can genuinely arrive out of range; this one restates an invariant the executor already enforced via DmaMemcpyChunkTooLarge before the log the trace builder reads exists. So it's marking an unreachable state, not checking input, and an assert! says that more honestly than a Result the caller would have to pretend to handle. If you'd still rather it be uniform with the neighbours, it's a small change — I just didn't want to imply the trace builder is validating something it can't see.

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.

6 participants