From 1166d0ae545c91c637a9876d8e103963eba3dd2d Mon Sep 17 00:00:00 2001 From: diegokingston Date: Tue, 15 Sep 2026 14:37:26 -0300 Subject: [PATCH 1/8] feat(vm): add constrained SHA-256 compression precompile --- bin/cli/src/main.rs | 14 +- docs/precompiles/sha256.md | 59 ++++ executor/programs/asm/test_sha256.s | 21 ++ executor/programs/asm/test_sha256_overlap.s | 32 ++ .../programs/rust/sha256/.cargo/config.toml | 5 + executor/programs/rust/sha256/Cargo.lock | 294 ++++++++++++++++++ executor/programs/rust/sha256/Cargo.toml | 9 + executor/programs/rust/sha256/src/main.rs | 12 + executor/src/lib.rs | 2 + executor/src/sha256.rs | 59 ++++ executor/src/tests/mod.rs | 2 + executor/src/tests/sha256_tests.rs | 79 +++++ executor/src/vm/instruction/execution.rs | 40 ++- executor/tests/rust.rs | 12 + executor/tests/sha256_vectors.bin | Bin 0 -> 3328 bytes prover/src/auto_storage.rs | 30 ++ prover/src/continuation.rs | 36 +++ prover/src/lib.rs | 36 ++- prover/src/tables/cpu.rs | 3 + prover/src/tables/mod.rs | 7 + prover/src/tables/sha256.rs | 273 ++++++++++++++++ prover/src/tables/sha256_common.rs | 69 ++++ prover/src/tables/sha256_k.rs | 49 +++ prover/src/tables/sha256_rotxor.rs | 101 ++++++ prover/src/tables/sha256_round.rs | 118 +++++++ prover/src/tables/sha256_schedule.rs | 92 ++++++ prover/src/tables/trace_builder.rs | 114 +++++++ prover/src/tables/types.rs | 12 + prover/src/test_utils.rs | 69 ++++ prover/src/tests/auto_storage_tests.rs | 8 +- .../tests/constraint_program_device_tests.rs | 15 + prover/src/tests/constraint_program_tests.rs | 15 + .../tests/count_table_lengths_drift_tests.rs | 33 ++ prover/src/tests/mod.rs | 2 + prover/src/tests/ood_window_ir_tests.rs | 22 ++ prover/src/tests/prove_elfs_tests.rs | 41 +++ prover/src/tests/sha256_tests.rs | 67 ++++ syscalls/src/lib.rs | 2 + syscalls/src/sha256.rs | 40 +++ 39 files changed, 1881 insertions(+), 13 deletions(-) create mode 100644 docs/precompiles/sha256.md create mode 100644 executor/programs/asm/test_sha256.s create mode 100644 executor/programs/asm/test_sha256_overlap.s create mode 100644 executor/programs/rust/sha256/.cargo/config.toml create mode 100644 executor/programs/rust/sha256/Cargo.lock create mode 100644 executor/programs/rust/sha256/Cargo.toml create mode 100644 executor/programs/rust/sha256/src/main.rs create mode 100644 executor/src/sha256.rs create mode 100644 executor/src/tests/sha256_tests.rs create mode 100644 executor/tests/sha256_vectors.bin create mode 100644 prover/src/tables/sha256.rs create mode 100644 prover/src/tables/sha256_common.rs create mode 100644 prover/src/tables/sha256_k.rs create mode 100644 prover/src/tables/sha256_rotxor.rs create mode 100644 prover/src/tables/sha256_round.rs create mode 100644 prover/src/tables/sha256_schedule.rs create mode 100644 prover/src/tests/sha256_tests.rs create mode 100644 syscalls/src/sha256.rs diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index a04e920db..c4b893ef3 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -412,7 +412,7 @@ fn cmd_execute( // below (the flamegraph path drives execution inside the executor and does // not expose per-log data). `None` means "not counted", so the accel lines // are omitted rather than printed as misleading zeros. - let mut accel_counts: Option<(u64, u64)> = None; + let mut accel_counts: Option<(u64, u64, u64)> = None; let cycle_count = if let Some(ref output_path) = flamegraph.path { // Shared execute+flamegraph path (executor::flamegraph) instead of @@ -480,6 +480,7 @@ fn cmd_execute( let mut cycle_count: u64 = 0; let mut keccak_calls: u64 = 0; let mut ecsm_calls: u64 = 0; + let mut sha256_calls: u64 = 0; // Reused per chunk: `(current_pc, a7)` for logs whose a7 matches an // accelerator syscall number. This is a cheap superset — a non-ECALL // instruction can hold the same value in src1 — that `accelerator_of` @@ -512,6 +513,7 @@ fn cmd_execute( match accelerator_of(executor.instructions.get(pc), a7) { Some(Accelerator::Keccak) => keccak_calls += 1, Some(Accelerator::Ecsm) => ecsm_calls += 1, + Some(Accelerator::Sha256) => sha256_calls += 1, None => {} } } @@ -526,16 +528,17 @@ fn cmd_execute( } if cycles { - accel_counts = Some((keccak_calls, ecsm_calls)); + accel_counts = Some((keccak_calls, ecsm_calls, sha256_calls)); } cycle_count }; if cycles { println!("Cycles: {}", cycle_count); - if let Some((keccak_calls, ecsm_calls)) = accel_counts { + if let Some((keccak_calls, ecsm_calls, sha256_calls)) = accel_counts { println!("Keccak calls: {}", keccak_calls); println!("Ecsm calls: {}", ecsm_calls); + println!("Sha256 compression calls: {}", sha256_calls); } } @@ -1104,13 +1107,16 @@ mod tests { // `accelerator_of` must match the prover's `CpuOperation::from_log`: count an // invocation only when the instruction is an ECALL AND a7 is the accelerator - // syscall number. Covers both accelerators, the non-accelerator syscalls, a + // syscall number. Covers all accelerators, the non-accelerator syscalls, a // non-ECALL whose src1 collides with an accelerator number, and a cache miss. #[test] fn accelerator_of_mirrors_prover_classification() { use executor::vm::instruction::execution::{ECSM_SYSCALL_NUMBER, KECCAK_SYSCALL_NUMBER}; let ecall = Instruction::EcallEbreak; + let sha = executor::vm::instruction::execution::SHA256_SYSCALL_NUMBER; + assert_eq!(accelerator_of(Some(&ecall), sha), Some(Accelerator::Sha256)); + assert_eq!(accelerator_of(Some(&Instruction::Fence), sha), None); assert_eq!( accelerator_of(Some(&ecall), KECCAK_SYSCALL_NUMBER), diff --git a/docs/precompiles/sha256.md b/docs/precompiles/sha256.md new file mode 100644 index 000000000..aeb6fa2f8 --- /dev/null +++ b/docs/precompiles/sha256.md @@ -0,0 +1,59 @@ +# SHA-256 compression precompile + +The VM implements the compression syscall specified in `spec/sha256.typ`: + +| Register | Meaning | +|---|---| +| a7 | -1 (`u64::MAX`) | +| a0 | Pointer to 32 bytes of SHA-256 state, eight big-endian words | +| a1 | Pointer to one 64-byte message block, sixteen big-endian words | + +The syscall reads both complete inputs before replacing the state with the +compression result. Arbitrary byte alignment and overlapping operands are +supported. Overflowing address ranges are rejected before writing. The syscall +performs compression only; `lambda_vm_syscalls::sha256::sha256` supplies the +standard IV, length encoding, padding and multiblock processing. + +## Proof integration + +Five fixed tables constrain the compression core, 64 rounds, 48 expanded +schedule words, rotations/XORs, and verifier-committed round constants. They +connect to the existing ECALL, memory and range-check arguments. State writes +use the existing combined read/write memory convention. Continuation collection +and disk-spill estimates account for the new tables and memory accesses. + +This implementation uses bit constraints for Ch/Maj and rotations rather than +the BYTE_ALU/HWSL layout proposed in the spec. The rotation table accepts the +four tuples needed by SHA-256. Constraint degree remains at most three. This +is a new proof implementation requiring review, not an audited equivalence +claim about the spec's table layout. + +The fixed-table count increases from 11 to 16. This changes the proof format; +proofs from older versions are not compatible with this verifier. + +## Validation + +The dedicated guest checks 104 hashes against a Python hashlib oracle: thirteen +lengths (0, 1, 31, 32, 55, 56, 63, 64, 65, 127, 128, 129, 1024), each at eight +byte alignments. Executor tests exercise compression, overlapping operands, +32-bit address-limb boundaries and rejected overflowing ranges. Prover tests +cover complete proofs, continuation boundaries and rejection of corrupted +outputs, pointers, round constants, schedule dependencies and rotation results. + +```sh +make compile-programs-asm +make executor/program_artifacts/rust/sha256.elf +cargo test --release -p executor sha256 +cargo test --release -p lambda-vm-prover sha256 --lib +``` + +## Earlier application experiment + +An isolated Amsterdam ethrex guest at revision +`89e160231d80b5d9bdfa8a7074e470298641d24d`, with its SHA provider routed through +this wrapper, used 26,654,728 instructions versus 37,145,872 before acceleration +(28.24% fewer) on a mainnet-derived local replay. The smaller fixtures saved +31–35%. Both sides used the same executor with verified arithmetic hints. +These are earlier integration measurements, not a benchmark of this PR branch, +and the replay is not a canonical mainnet block. Guest wiring and other guest +optimizations are outside this VM-only change. diff --git a/executor/programs/asm/test_sha256.s b/executor/programs/asm/test_sha256.s new file mode 100644 index 000000000..54cc70539 --- /dev/null +++ b/executor/programs/asm/test_sha256.s @@ -0,0 +1,21 @@ +.globl main +main: + la a0, state + la a1, message + li a7, -1 + ecall + li a0, 1 + la a1, state + li a2, 32 + li a7, 64 + ecall + li a0, 0 + li a7, 93 + ecall +.data +.byte 0 +state: +.byte 106,9,230,103,187,103,174,133,60,110,243,114,165,79,245,58,81,14,82,127,155,5,104,140,31,131,217,171,91,224,205,25 +.byte 0,0 +message: +.byte 97,98,99,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24 diff --git a/executor/programs/asm/test_sha256_overlap.s b/executor/programs/asm/test_sha256_overlap.s new file mode 100644 index 000000000..9bab836b7 --- /dev/null +++ b/executor/programs/asm/test_sha256_overlap.s @@ -0,0 +1,32 @@ +.globl main +main: + la a0, state + la a1, message + li a7, -1 + ecall + la a0, state + la a1, state + li a7, -1 + ecall + la a0, state + la a1, state + addi a1, a1, 3 + li a7, -1 + ecall + li a0, 1 + la a1, state + li a2, 32 + li a7, 64 + ecall + li a0, 0 + li a7, 93 + ecall +.data +.byte 0 +state: +.byte 106,9,230,103,187,103,174,133,60,110,243,114,165,79,245,58,81,14,82,127,155,5,104,140,31,131,217,171,91,224,205,25 +.byte 0,0 +message: +.byte 97,98,99,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24 + +.zero 64 diff --git a/executor/programs/rust/sha256/.cargo/config.toml b/executor/programs/rust/sha256/.cargo/config.toml new file mode 100644 index 000000000..ca99a3f45 --- /dev/null +++ b/executor/programs/rust/sha256/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.riscv64im-lambda-vm-elf] +rustflags = [ + "--cfg", "getrandom_backend=\"custom\"", + "-C", "passes=lower-atomic" +] diff --git a/executor/programs/rust/sha256/Cargo.lock b/executor/programs/rust/sha256/Cargo.lock new file mode 100644 index 000000000..a50594dc1 --- /dev/null +++ b/executor/programs/rust/sha256/Cargo.lock @@ -0,0 +1,294 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-default" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[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 = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[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 = "lambda-vm-syscalls" +version = "0.1.0" +dependencies = [ + "embedded-alloc", + "getrandom 0.2.17", + "getrandom 0.3.4", + "lazy_static", + "rand", + "riscv", + "thiserror", +] + +[[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.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linked_list_allocator" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[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.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[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 = "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", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07393724337be2ee43a9d86164df4505746874a3fa65913374bc6d6a92314362" +dependencies = [ + "cfg-if", + "const-default", + "libc", + "rustversion", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "sha256" +version = "0.1.0" +dependencies = [ + "lambda-vm-syscalls", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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 = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/executor/programs/rust/sha256/Cargo.toml b/executor/programs/rust/sha256/Cargo.toml new file mode 100644 index 000000000..0ffb75391 --- /dev/null +++ b/executor/programs/rust/sha256/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +[package] +name = "sha256" +version = "0.1.0" +edition = "2024" +[profile.release] +lto = "thin" +[dependencies] +lambda-vm-syscalls = { path = "../../../../syscalls" } diff --git a/executor/programs/rust/sha256/src/main.rs b/executor/programs/rust/sha256/src/main.rs new file mode 100644 index 000000000..640ac0f98 --- /dev/null +++ b/executor/programs/rust/sha256/src/main.rs @@ -0,0 +1,12 @@ +fn main() { + let mut data = [0u8; 1032]; + for (i, b) in data.iter_mut().enumerate() { + *b = (i * 17 + 3) as u8; + } + for len in [0, 1, 31, 32, 55, 56, 63, 64, 65, 127, 128, 129, 1024] { + for offset in 0..8 { + let hash = lambda_vm_syscalls::sha256::sha256(&data[offset..offset + len]); + lambda_vm_syscalls::syscalls::commit(&hash); + } + } +} diff --git a/executor/src/lib.rs b/executor/src/lib.rs index d626ca1f4..827dbb2c4 100644 --- a/executor/src/lib.rs +++ b/executor/src/lib.rs @@ -3,3 +3,5 @@ pub mod flamegraph; #[cfg(test)] pub mod tests; pub mod vm; + +pub mod sha256; diff --git a/executor/src/sha256.rs b/executor/src/sha256.rs new file mode 100644 index 000000000..9f11b9fc6 --- /dev/null +++ b/executor/src/sha256.rs @@ -0,0 +1,59 @@ +//! SHA-256 compression only. Padding and the IV belong to guest code. +//! The syscall ABI uses big-endian bytes for both the state and message. +pub const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +]; +pub const IV: [u32; 8] = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, +]; +pub fn sigma(x: u32, kind: usize) -> u32 { + match kind { + 0 => x.rotate_right(7) ^ x.rotate_right(18) ^ (x >> 3), + 1 => x.rotate_right(17) ^ x.rotate_right(19) ^ (x >> 10), + 2 => x.rotate_right(2) ^ x.rotate_right(13) ^ x.rotate_right(22), + 3 => x.rotate_right(6) ^ x.rotate_right(11) ^ x.rotate_right(25), + _ => unreachable!(), + } +} +pub fn schedule(m: &[u8; 64]) -> [u32; 64] { + let mut w = [0u32; 64]; + for i in 0..16 { + w[i] = u32::from_be_bytes(m[4 * i..4 * i + 4].try_into().unwrap()); + } + for i in 16..64 { + w[i] = w[i - 16] + .wrapping_add(sigma(w[i - 15], 0)) + .wrapping_add(w[i - 7]) + .wrapping_add(sigma(w[i - 2], 1)); + } + w +} +pub fn round(s: [u32; 8], w: u32, k: u32) -> [u32; 8] { + let [a, b, c, d, e, f, g, h] = s; + let t1 = h + .wrapping_add(sigma(e, 3)) + .wrapping_add((e & f) ^ (!e & g)) + .wrapping_add(k) + .wrapping_add(w); + let t2 = sigma(a, 2).wrapping_add((a & b) ^ (a & c) ^ (b & c)); + [t1.wrapping_add(t2), a, b, c, d.wrapping_add(t1), e, f, g] +} +pub fn compress(h: &mut [u8; 32], m: &[u8; 64]) { + let initial = + std::array::from_fn(|i| u32::from_be_bytes(h[i * 4..i * 4 + 4].try_into().unwrap())); + let mut s = initial; + let w = schedule(m); + for i in 0..64 { + s = round(s, w[i], K[i]); + } + for i in 0..8 { + h[4 * i..4 * i + 4].copy_from_slice(&s[i].wrapping_add(initial[i]).to_be_bytes()); + } +} diff --git a/executor/src/tests/mod.rs b/executor/src/tests/mod.rs index 244447b22..408b72a2b 100644 --- a/executor/src/tests/mod.rs +++ b/executor/src/tests/mod.rs @@ -3,3 +3,5 @@ pub mod flamegraph_tests; pub mod hint_tests; pub mod keccak_tests; pub mod memory_tests; + +mod sha256_tests; diff --git a/executor/src/tests/sha256_tests.rs b/executor/src/tests/sha256_tests.rs new file mode 100644 index 000000000..bbf76e5a7 --- /dev/null +++ b/executor/src/tests/sha256_tests.rs @@ -0,0 +1,79 @@ +use crate::{ + sha256, + vm::{ + instruction::{ + decoding::Instruction, + execution::{ExecutionError, SHA256_SYSCALL_NUMBER}, + }, + memory::Memory, + registers::Registers, + }, +}; +#[test] +fn sha256_abc_vector() { + let mut h = [0u8; 32]; + for (i, x) in sha256::IV.iter().enumerate() { + h[4 * i..4 * i + 4].copy_from_slice(&x.to_be_bytes()); + } + let mut m = [0; 64]; + m[..4].copy_from_slice(b"abc\x80"); + m[63] = 24; + sha256::compress(&mut h, &m); + assert_eq!( + h, + [ + 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, + 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, + 0xf2, 0x00, 0x15, 0xad + ] + ); +} +#[test] +fn sha256_syscall_alignment_overlap_and_boundary() { + for (h, m) in [ + (0x1000, 0x2000), + (0x1003, 0x2007), + (0x1000, 0x1000), + (0x1007, 0x1000), + (0x1000, 0x1007), + (0xfffffff0, 0x2003), + ] { + let mut memory = Memory::default(); + for i in 0..64 { + memory.store_byte(m + i, (i * 17) as u8); + } + for i in 0..32 { + memory.store_byte(h + i, (i * 23) as u8); + } + let mut expected = std::array::from_fn(|i| memory.load_byte(h + i as u64)); + let message = std::array::from_fn(|i| memory.load_byte(m + i as u64)); + sha256::compress(&mut expected, &message); + let mut registers = Registers::default(); + registers.write(17, SHA256_SYSCALL_NUMBER).unwrap(); + registers.write(10, h).unwrap(); + registers.write(11, m).unwrap(); + Instruction::EcallEbreak + .run(&mut 0, &mut registers, &mut memory) + .unwrap(); + assert_eq!( + std::array::from_fn::<_, 32, _>(|i| memory.load_byte(h + i as u64)), + expected + ); + } +} +#[test] +fn sha256_rejects_overflow_without_writes() { + for (h, m) in [(u64::MAX - 30, 0x1000), (0x1000, u64::MAX - 62)] { + let mut memory = Memory::default(); + memory.store_byte(0x1000, 123); + let mut registers = Registers::default(); + registers.write(17, SHA256_SYSCALL_NUMBER).unwrap(); + registers.write(10, h).unwrap(); + registers.write(11, m).unwrap(); + assert!(matches!( + Instruction::EcallEbreak.run(&mut 0, &mut registers, &mut memory), + Err(ExecutionError::Sha256AddressOverflow) + )); + assert_eq!(memory.load_byte(0x1000), 123); + } +} diff --git a/executor/src/vm/instruction/execution.rs b/executor/src/vm/instruction/execution.rs index 592af95e8..25615e8fe 100644 --- a/executor/src/vm/instruction/execution.rs +++ b/executor/src/vm/instruction/execution.rs @@ -19,11 +19,15 @@ pub enum SyscallNumbers { // Placeholder discriminant. The actual syscall value is HINT_SYSCALL_NUMBER. // Non-constraining hint (host computes modular inverse/sqrt, guest verifies). Hint = 95, + // Placeholder discriminant. The wire value is SHA256_SYSCALL_NUMBER (-1). + Sha256 = 96, } -/// Syscall number for KeccakPermute (u64::MAX - 1 = 0xFFFF_FFFF_FFFF_FFFE). -/// -/// Cannot be an enum discriminant because it exceeds isize::MAX. +/// SHA256 compression syscall (-1). a0: 32-byte state, a1: 64-byte message. +/// Both use big-endian bytes; alignment is unrestricted and overlap is allowed. +pub const SHA256_SYSCALL_NUMBER: u64 = u64::MAX; + +/// KeccakPermute syscall (-2); cannot be an enum discriminant above isize::MAX. pub const KECCAK_SYSCALL_NUMBER: u64 = u64::MAX - 1; const KECCAK_STATE_BYTES: u64 = 25 * 8; @@ -86,6 +90,7 @@ impl TryFrom for SyscallNumbers { 2 => Ok(SyscallNumbers::Panic), 64 => Ok(SyscallNumbers::Commit), 93 => Ok(SyscallNumbers::Halt), + v if v == SHA256_SYSCALL_NUMBER => Ok(SyscallNumbers::Sha256), v if v == KECCAK_SYSCALL_NUMBER => Ok(SyscallNumbers::KeccakPermute), v if v == ECSM_SYSCALL_NUMBER => Ok(SyscallNumbers::Ecsm), v if v == HINT_SYSCALL_NUMBER => Ok(SyscallNumbers::Hint), @@ -97,6 +102,7 @@ impl TryFrom for SyscallNumbers { /// A syscall that drives a specialized in-circuit accelerator chip. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Accelerator { + Sha256, Keccak, Ecsm, } @@ -107,6 +113,7 @@ impl SyscallNumbers { /// accelerator can't be silently missed by counters that consume this. pub fn accelerator(self) -> Option { match self { + SyscallNumbers::Sha256 => Some(Accelerator::Sha256), SyscallNumbers::KeccakPermute => Some(Accelerator::Keccak), SyscallNumbers::Ecsm => Some(Accelerator::Ecsm), SyscallNumbers::Print @@ -491,6 +498,31 @@ impl Instruction { src2_val = buf_addr; dst_val = count; } + SyscallNumbers::Sha256 => { + let h_addr = registers.read(10)?; + let m_addr = registers.read(11)?; + h_addr + .checked_add(31) + .ok_or(ExecutionError::Sha256AddressOverflow)?; + m_addr + .checked_add(63) + .ok_or(ExecutionError::Sha256AddressOverflow)?; + // Read both operands completely before writing: overlap is allowed. + let mut m = [0u8; 64]; + let mut h = [0u8; 32]; + for (i, byte) in m.iter_mut().enumerate() { + *byte = memory.load_byte(m_addr + i as u64); + } + for (i, byte) in h.iter_mut().enumerate() { + *byte = memory.load_byte(h_addr + i as u64); + } + crate::sha256::compress(&mut h, &m); + for (i, byte) in h.iter().enumerate() { + memory.store_byte(h_addr + i as u64, *byte); + } + src2_val = h_addr; + dst_val = m_addr; + } SyscallNumbers::KeccakPermute => { // keccak-f[1600] permutation on 200 bytes (25 × u64) at address in x10 let state_addr = registers.read(10)?; @@ -736,6 +768,8 @@ impl Comparison { #[derive(thiserror::Error, Debug)] pub enum ExecutionError { + #[error("SHA256 operand address overflow")] + Sha256AddressOverflow, #[error("Sub immediate instruction is not supported")] SubImmNotSupported, #[error("Store bytes unsigned instruction is not supported")] diff --git a/executor/tests/rust.rs b/executor/tests/rust.rs index 1c13ad1a5..3973adbf2 100644 --- a/executor/tests/rust.rs +++ b/executor/tests/rust.rs @@ -348,3 +348,15 @@ fn test_args_panics() { fn test_ckzg() { run_program_and_check_public_output("./program_artifacts/rust/ckzg.elf", vec![1, 1], vec![]); } + +#[test] +fn test_sha256_precompile() { + // Independent Python hashlib oracle: concatenate sha256(data[o:o+n]) for + // n in [0,1,31,32,55,56,63,64,65,127,128,129,1024], o in range(8), + // where data[i] = (17*i+3) mod 256. Covers both padding blocks and alignment. + run_program_and_check_public_output( + "./program_artifacts/rust/sha256.elf", + include_bytes!("sha256_vectors.bin").to_vec(), + vec![], + ); +} diff --git a/executor/tests/sha256_vectors.bin b/executor/tests/sha256_vectors.bin new file mode 100644 index 0000000000000000000000000000000000000000..81cf111f7ab6e952168a82da094c0ff3932412dd GIT binary patch literal 3328 zcmdtXOiWC(IUUnAb<<~uah#e{$wHVc`J2rac?ZrU0dkZ&TwaTU)Dplu8dN!*cq#3&LfBvLzj!0YlYIK8` z-+OC*b2i1xd*^J!US~^#Hb+)H2=s;{*h6+LB)5b=>zUFIilU2KG`+H)6U%*VZT;2& zne}fwKV@rdQ1&4BP#RCW4Baf*1n zn-6la?YSL3?kcvd_+W5k>36YIwGX=lhdx~nV)bI-AdzSqCp!8|D4Z~k%SV!Gb3$5p zPfJR7?6GEg2FT59aUlUUg`nB8kQ2w{yVgu7dU}E?qL+Inki>5IQ@fl1RNri8XJWY9 zqfM5gcLuL~+Mk&+Qg=@-?7IfQS1!C<~Kp$)HiNd+5h8j@qE4IR`t5@P~+W7Jz zv>jCrbVl!lO0HV)1Ng~7cU&U)ag=;o=*h&Y@?o4dA?^WKs2<4R-cr*F4ZCM^soZ~b ztwmp75UkFTDA6_ey>G-Yuo@vB0z7*5>Oy6|^wEG>R>3bvGRsn{^;sS2k0k5lwEml| z`Bua?3xWxv(PA8cYzNIze63D~`6#YQaWSbzPDhc)ajh=DipJdYq#G24*w535B-s@e zI>*K?=_W+SD-UBSk5iR=d=zbN#esFTGco=hIjOzywP~oW=|M(T;;*rj-KziAO$wi2 z%8UZUuA_7&{aKXSqV-pcpKb!8Se6KyFR8e?KVP0fdzZNjV0x>jKiD{Gb*6&v|AP0}xrP~F6Ez-uo z`3m?C5UQEpW3Ap9YM?t&88_L5ju3x-7yhufPftgC{ikz+Or$P0)OxTj6d8_?6!XM0 zRw1U4iYZOA8O+4b-?0tW7`iXj(W%pdr=|b&=jTg1m;yY$=I}tzGcC0>skF(0UPmF= zTcwyil z9{J>XcIK7e4#7!h;x@~}xb8{&k&*wb6{vK6F88+vLpWi=SL0!?Fl>Gu0y9y0mX&m} zjU6BILwZ2geFT%Tq_yw(u4d0dGQ)G2!@*s@l`ORi((hWE zG1e0)oRWAihPgO5%=SKH>b8~>gt>$Db&GUGjIQ<_8qyUc&ME=7-;NeFY4%iIuV%)L zS*$lmWbe*;MEJ{y%AfQP)Er|*qXI~7iX0zI!TJ~l9YWW#FEB47ldXuNeDPpwO@F1Fx0z^M0lvVZ!*lzQK_BhLYXu()1ltFG&gQU*6c3 z!PC1E_iGWx8#)P6bYfP!h4i?>-HA)a;gc=%~+KQ=Y?Ha(*NlE%(I@7D<_Woha1=&OLxYOEiip^RYg~$ddi&QEX!RzHIo-)zTi%HCQ zUcZ-BqW($SOAYLa<~=7dVyi+0l!upqS`r1!j#p15YJlk_?>&czIKM0MX**LQ55A>U z10sV(M(=`niSf}Y{P{a8L1{{tu+;ufXXl0MU$%4H;Yu;ry|5e6I4-D3`a$vt`laC% z^PKHn)}J2@&#mhr=6w;6E!lO{#<;Xo_r3tn+}T76_5BAk_Vg#h-E6a!BUZuO!?WWm zFJG4TyCn9w)4$%#@s9fNxXYt*5?=h8IbP*{45l~i(~Nml>yB9{k)da&JJ`*YGE<@k zck-C?*BBPv<<=_VyaEcAPQdt%GRb2yIgw=MT@+&T#1f4L@XjLp+dsj5Xosn34}v1} zNB!ca$C2sJ7SI$i)nwA{=e2VXivN7);yrg3>kBOGujF=VbFt6_T^jYJJzyv4D5>E( zjd^rzMOIl%uNDf_O_?C{Co3+^wGU(p{1?!hg>|l+zC(NKtNT$&ovZ|?kS!Wih2bTc z0ThF)?JJFj7ZU4|R-LDFAw+a#-d~fch>oeHIMe8s*of;)F;s1y z?wjd#5(-T3G%rJtQfp)N8Li|}^5XWK2l~hXJCy&0`D^GGVu6Mae!(%3YrUmIdJMpo z$^;xu!c`&il3X4ff&9C-)~{nH!>X*t52Id`Iv1B!ZDe|)i4ezeyO%IbGZ1cgJ(!J& zi2w@d^CoY*nDb&qRo_4w>@(CCsW}qaf*l5Bv^y2l8(F=E$RsYN8lV+URXYxh5q+09%qcQHoS7J$S@;(NzLt%rTem#q_CHFy|Gl>gm1>d*f?2 z-TVrk`v~*0T3_`AH~cb@Y@MA=$q=rm9B%VfHFvaB%)X<#W4kbM-nPiI(Ir|`rm&H* zTxyunQpLABOA4YuV0sHbf*-C(T`9AvR`*opCy*P>OB%;xoP`XW=eywclJU>&DKeI; zT_CL0sF%{9m=pVfsmn$OIKFZE1A5eU?8E`+O5CTUhK`sJ)Daa3N}QgG8FTM_og@CL z)O*O&xg)RwS@^i~Ak5?GI!8yi`!(A^C0fRf39#GiemZym!qa_y2}0&KTZueL{v9q_ zs;(1qz?#oC2}i~_=|z%u>CA;OocXZ zqDAt48Me@&WHpCNKP)lj%@U|Au;AjOIopDR*07WLhE$z(etd&p``Y|#sn$J;vZ3%* qy%ztsG6Gud0pS|iUb;?px2p2DacAxeoOawF<;0Zuft{h>AS` literal 0 HcmV?d00001 diff --git a/prover/src/auto_storage.rs b/prover/src/auto_storage.rs index 6b5ed8a5d..a9aac1e1d 100644 --- a/prover/src/auto_storage.rs +++ b/prover/src/auto_storage.rs @@ -202,6 +202,36 @@ fn table_specs(lengths: &TableLengths) -> Vec { 2, ), ]; + // SHA256 is a fixed set of tables, expanded per compression call. Include + // their real widths and lookup columns in the spill decision. + use crate::tables::{sha256, sha256_k, sha256_rotxor, sha256_round, sha256_schedule}; + for (factor, width, buses) in [ + (1, sha256::WIDTH, sha256::bus_interactions().len()), + ( + 64, + sha256_round::WIDTH, + sha256_round::bus_interactions().len(), + ), + ( + 48, + sha256_schedule::WIDTH, + sha256_schedule::bus_interactions().len(), + ), + ( + 224, + sha256_rotxor::WIDTH, + sha256_rotxor::bus_interactions().len(), + ), + ] { + let rows = (lengths.sha256_calls * factor).next_power_of_two().max(4); + specs.push((rows, width as u64, aux_cols(buses), 1)); + } + specs.push(( + 64, + sha256_k::WIDTH as u64, + aux_cols(sha256_k::bus_interactions().len()), + 2, + )); // Each unique 256 KB page → its own PAGE table at PAGE_SIZE rows. for _ in 0..lengths.unique_page_count { specs.push(( diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index 85f2d6223..d8e3aa126 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -490,6 +490,26 @@ impl ContinuationProof { pub fn num_epochs(&self) -> usize { self.epochs.len() } + + /// SCRATCH ANALYSIS HOOK (safe to delete): per-epoch `(proof, table_counts)` + /// plus the cross-epoch global proof, so out-of-crate tooling can measure + /// proof shape (widths, Merkle depths, FRI layers) without re-deriving it. + /// Read-only borrows; nothing here is used by prove/verify. + #[allow(clippy::type_complexity)] + pub fn shape_parts( + &self, + ) -> ( + Vec<(&MultiProof, &TableCounts)>, + &MultiProof, + ) { + ( + self.epochs + .iter() + .map(|e| (&e.proof, &e.table_counts)) + .collect(), + &self.global, + ) + } } /// Borrowed view over an [`EpochProof`] (owned or archived-in-place). Lets @@ -2031,6 +2051,22 @@ mod tests { ); } + #[test] + fn test_sha256_across_epochs_verifies() { + let elf_bytes = asm_elf_bytes("test_sha256_overlap"); + let out = prove_and_verify_continuation( + &elf_bytes, + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); + assert!( + out.is_some(), + "SHA state and pointer registers must survive epoch boundaries" + ); + } + // Guards that the continuation API takes `epoch_size_log2` directly. A log2 of // 4 produces 16-cycle epochs over the 33-cycle `test_commit_split`, putting its // two commits in different epochs and exercising the cross-epoch x254 carry. diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 79ef4c715..49d7635bb 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -82,8 +82,8 @@ pub struct RuntimePageRange { /// Number of tables that always contribute exactly one sub-proof, regardless /// of `TableCounts`: bitwise, decode, halt, commit, keccak, keccak_rnd, -/// keccak_rc, register, ecsm, ecdas, hint. -pub const FIXED_TABLE_COUNT: usize = 11; +/// keccak_rc, register, ecsm, ecdas, hint, and five SHA256 tables. +pub const FIXED_TABLE_COUNT: usize = 16; /// Number of chunks for each split table. /// The verifier needs this to reconstruct matching AIRs. @@ -518,6 +518,12 @@ pub(crate) struct VmAirs { pub halt: VmAir, pub commit: VmAir, pub keccak: VmAir, + pub sha256: VmAir, + pub sha256_round: VmAir, + pub sha256_schedule: VmAir, + pub sha256_rotxor: VmAir, + pub sha256_k: VmAir, + pub keccak_rnd: VmAir, pub keccak_rc: VmAir, pub ecsm: VmAir, @@ -544,6 +550,15 @@ impl VmAirs { (self.decode.as_ref(), &mut traces.decode, &()), (self.commit.as_ref(), &mut traces.commit, &()), (self.keccak.as_ref(), &mut traces.keccak, &()), + (self.sha256.as_ref(), &mut traces.sha256, &()), + (self.sha256_round.as_ref(), &mut traces.sha256_round, &()), + ( + self.sha256_schedule.as_ref(), + &mut traces.sha256_schedule, + &(), + ), + (self.sha256_rotxor.as_ref(), &mut traces.sha256_rotxor, &()), + (self.sha256_k.as_ref(), &mut traces.sha256_k, &()), (self.keccak_rnd.as_ref(), &mut traces.keccak_rnd, &()), (self.keccak_rc.as_ref(), &mut traces.keccak_rc, &()), (self.ecsm.as_ref(), &mut traces.ecsm, &()), @@ -619,6 +634,11 @@ impl VmAirs { self.decode.as_ref(), self.commit.as_ref(), self.keccak.as_ref(), + self.sha256.as_ref(), + self.sha256_round.as_ref(), + self.sha256_schedule.as_ref(), + self.sha256_rotxor.as_ref(), + self.sha256_k.as_ref(), self.keccak_rnd.as_ref(), self.keccak_rc.as_ref(), self.ecsm.as_ref(), @@ -787,6 +807,12 @@ impl VmAirs { .collect(); let halt: VmAir = Box::new(create_halt_air(proof_options)); let commit: VmAir = Box::new(create_commit_air(proof_options)); + let sha256: VmAir = Box::new(test_utils::create_sha256_air(proof_options)); + let sha256_round: VmAir = Box::new(test_utils::create_sha256_round_air(proof_options)); + let sha256_schedule: VmAir = + Box::new(test_utils::create_sha256_schedule_air(proof_options)); + let sha256_rotxor: VmAir = Box::new(test_utils::create_sha256_rotxor_air(proof_options)); + let sha256_k: VmAir = Box::new(test_utils::create_sha256_k_air(proof_options)); let keccak: VmAir = Box::new(create_keccak_air(proof_options)); let keccak_rnd: VmAir = Box::new(create_keccak_rnd_air(proof_options)); let keccak_rc: VmAir = Box::new(create_keccak_rc_air(proof_options).with_preprocessed( @@ -912,6 +938,12 @@ impl VmAirs { halt, commit, keccak, + sha256, + sha256_round, + sha256_schedule, + sha256_rotxor, + sha256_k, + keccak_rnd, keccak_rc, ecsm, diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index fc4c2f976..a366c31f4 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -193,6 +193,7 @@ pub struct CpuOperation { /// addresses (x10/x11/x12) are recovered from the register state in the trace /// builder, exactly like ECSM. pub ecall_hint: bool, + pub ecall_sha256: bool, } impl CpuOperation { @@ -361,6 +362,8 @@ impl CpuOperation { keccak_state_addr, ecall_ecsm, ecall_hint, + ecall_sha256: f.ecall + && log.src1_val == executor::vm::instruction::execution::SHA256_SYSCALL_NUMBER, } } diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index f1a899f56..5777d0ba4 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -163,3 +163,10 @@ impl MaxRowsConfig { } } } + +pub mod sha256; +pub mod sha256_common; +pub mod sha256_k; +pub mod sha256_rotxor; +pub mod sha256_round; +pub mod sha256_schedule; diff --git a/prover/src/tables/sha256.rs b/prover/src/tables/sha256.rs new file mode 100644 index 000000000..055f2a805 --- /dev/null +++ b/prover/src/tables/sha256.rs @@ -0,0 +1,273 @@ +//! SHA256 compression syscall (-1), following spec/sha256.typ. +//! One core row binds memory to the schedule, round chain, and feed-forward. +use super::{ + sha256_common::*, + sha256_schedule, + types::{BusId, GoldilocksExtension as E, GoldilocksField as F}, +}; +use crate::constraints::templates::{AddOperand, INV_SHIFT_32, emit_add_pair}; +use stark::{ + constraints::builder::{ConstraintBuilder, ConstraintSet}, + lookup::{BusInteraction, BusValue}, + trace::TraceTable, +}; +pub const PTR: usize = 4; +pub const H: usize = 60; +pub const M: usize = 92; +pub const OUT: usize = 156; +pub const LAST: usize = 188; +pub const CARRY: usize = 196; +pub const MU: usize = 204; +pub const WIDTH: usize = 205; +// 14 pointers: h[0..4], m[0..8], inclusive h end, inclusive m end. +#[derive(Clone, Debug)] +pub struct Operation { + pub timestamp: u64, + pub state_addr: u64, + pub message_addr: u64, + pub state: [u8; 32], + pub message: [u8; 64], +} +impl Operation { + pub fn state_words(&self) -> [u32; 8] { + std::array::from_fn(|i| { + u32::from_be_bytes(self.state[4 * i..4 * i + 4].try_into().unwrap()) + }) + } + pub fn pointers(&self) -> [u64; 14] { + std::array::from_fn(|i| match i { + 0..4 => self.state_addr + 8 * i as u64, + 4..12 => self.message_addr + 8 * (i - 4) as u64, + 12 => self.state_addr + 31, + _ => self.message_addr + 63, + }) + } +} +pub fn generate(ops: &[Operation]) -> TraceTable { + trace( + ops.iter() + .map(|op| { + let mut r = vec![0; WIDTH]; + r[0] = op.timestamp & 0xffffffff; + r[1] = op.timestamp >> 32; + r[2] = (op.timestamp + 1) & 0xffffffff; + r[3] = (op.timestamp + 1) >> 32; + for (i, p) in op.pointers().iter().enumerate() { + for j in 0..4 { + r[PTR + 4 * i + j] = (p >> (16 * j)) & 65535; + } + } + for i in 0..32 { + r[H + i] = op.state[i] as u64; + } + for i in 0..64 { + r[M + i] = op.message[i] as u64; + } + let mut out = op.state; + executor::sha256::compress(&mut out, &op.message); + for i in 0..32 { + r[OUT + i] = out[i] as u64; + } + let w = executor::sha256::schedule(&op.message); + let init = op.state_words(); + let mut s = init; + for (&word, &constant) in w.iter().zip(&executor::sha256::K) { + s = executor::sha256::round(s, word, constant); + } + for i in 0..8 { + r[LAST + i] = s[i] as u64; + r[CARRY + i] = (s[i] as u64 + init[i] as u64) >> 32; + } + r[MU] = 1; + r + }) + .collect(), + WIDTH, + ) +} +fn mem( + ptr: usize, + old: Vec, + new: Vec, + ts: usize, + reg: Option, +) -> BusInteraction { + let mut v = old; + v.push(BusValue::constant(reg.is_some() as u64)); + if let Some(reg) = reg { + v.extend([BusValue::constant(2 * reg), BusValue::constant(0)]); + } else { + v.extend([half(ptr), half(ptr + 2)]); + } + v.extend(new); + v.extend([ + col(ts), + col(ts + 1), + BusValue::constant(reg.is_some() as u64), + BusValue::constant(0), + BusValue::constant(reg.is_none() as u64), + ]); + send(BusId::Memw, MU, v) +} +pub fn bus_interactions() -> Vec { + let mut v = vec![recv( + BusId::Ecall, + MU, + vec![ + col(0), + col(1), + BusValue::constant(0xffffffff), + BusValue::constant(0xffffffff), + ], + )]; + for (reg, p) in [(10, PTR), (11, PTR + 16)] { + let mut bytes = vec![half(p), half(p + 2)]; + bytes.extend((0..6).map(|_| BusValue::constant(0))); + v.push(mem(p, bytes.clone(), bytes, 0, Some(reg))); + } + for i in 0..8 { + let bytes = (0..8).map(|j| col(M + 8 * i + j)).collect::>(); + v.push(mem(PTR + 16 + 4 * i, bytes.clone(), bytes, 0, None)); + } + for i in 0..4 { + v.push(mem( + PTR + 4 * i, + (0..8).map(|j| col(H + 8 * i + j)).collect(), + (0..8).map(|j| col(OUT + 8 * i + j)).collect(), + 2, + None, + )); + } + for i in 0..56 { + v.push(send(BusId::IsHalfword, MU, vec![col(PTR + i)])); + } + for i in 0..16 { + v.push(send( + BusId::AreBytes, + MU, + vec![col(OUT + 2 * i), col(OUT + 2 * i + 1)], + )); + } + for i in 0..16 { + let mut interaction = recv( + BusId::ShaM, + MU, + vec![col(0), col(1), BusValue::constant(i as u64), be(M + 4 * i)], + ); + interaction.multiplicity = + stark::lookup::Multiplicity::Linear(vec![stark::lookup::LinearTerm::Column { + column: MU, + coefficient: sha256_schedule::amount(i) as i64, + }]); + v.push(interaction); + } + let mut start = vec![col(0), col(1), BusValue::constant(0)]; + start.extend((0..8).map(|i| be(H + 4 * i))); + v.push(send(BusId::ShaRound, MU, start)); + let mut end = vec![col(0), col(1), BusValue::constant(64)]; + end.extend((0..8).map(|i| col(LAST + i))); + v.push(recv(BusId::ShaRound, MU, end)); + v +} +#[derive(Clone, Copy)] +pub struct Constraints; +impl ConstraintSet for Constraints { + fn max_degree(&self) -> usize { + 3 + } + fn eval>(&self, b: &mut B) { + let mut id = 0; + check_bits(b, &mut id, CARRY, 9); + emit_add_pair( + b, + id, + &[MU], + &AddOperand::dword(0), + &AddOperand::constant(1), + &AddOperand::dword(2), + ); + id += 2; + for i in 0..14 { + let (base, offset) = match i { + 0..4 => (PTR, 8 * i), + 4..12 => (PTR + 16, 8 * (i - 4)), + 12 => (PTR, 31), + _ => (PTR + 16, 63), + }; + let p = PTR + 4 * i; + emit_add_pair( + b, + id, + &[MU], + &AddOperand::from_dword_hl(base), + &AddOperand::constant(offset as i64), + &AddOperand::from_dword_hl(p), + ); + id += 2; + if i >= 12 { + let lo = b.main(0, base) + b.const_base(65536) * b.main(0, base + 1); + let hi = b.main(0, base + 2) + b.const_base(65536) * b.main(0, base + 3); + let outlo = b.main(0, p) + b.const_base(65536) * b.main(0, p + 1); + let outhi = b.main(0, p + 2) + b.const_base(65536) * b.main(0, p + 3); + let carry = (lo + b.const_base(offset as u64) - outlo) * b.const_base(INV_SHIFT_32); + b.emit_base(id, b.main(0, MU) * (hi + carry - outhi)); + id += 1; + } + } + for i in 0..8 { + let mut h = b.const_base(0); + let mut out = b.const_base(0); + for j in 0..4 { + h = h + b.const_base(1 << (8 * (3 - j))) * b.main(0, H + 4 * i + j); + out = out + b.const_base(1 << (8 * (3 - j))) * b.main(0, OUT + 4 * i + j); + } + b.emit_base( + id, + b.main(0, MU) + * (h + b.main(0, LAST + i) + - out + - b.const_base(1 << 32) * b.main(0, CARRY + i)), + ); + id += 1; + } + } +} + +pub fn rot_ops(ops: &[Operation]) -> Vec<(u32, usize)> { + let mut v = vec![]; + for op in ops { + let w = executor::sha256::schedule(&op.message); + for i in 16..64 { + v.push((w[i - 15], 0)); + v.push((w[i - 2], 1)); + } + let mut s = op.state_words(); + for (&word, &constant) in w.iter().zip(&executor::sha256::K) { + v.push((s[0], 2)); + v.push((s[4], 3)); + s = executor::sha256::round(s, word, constant); + } + } + v +} +pub fn bitwise_ops(ops: &[Operation]) -> Vec { + use super::bitwise::{BitwiseOperation as Op, BitwiseOperationType as Ty}; + let mut v = vec![]; + for op in ops { + for p in op.pointers() { + for j in 0..4 { + let h = (p >> (16 * j)) as u16; + v.push(Op::halfword(Ty::IsHalf, h as u8, (h >> 8) as u8)); + } + } + let mut out = op.state; + executor::sha256::compress(&mut out, &op.message); + for i in 0..16 { + v.push(Op::halfword(Ty::AreBytes, out[2 * i], out[2 * i + 1])); + } + for i in 0..48 { + v.push(Op::halfword(Ty::AreBytes, i, 0)); + } + } + v +} diff --git a/prover/src/tables/sha256_common.rs b/prover/src/tables/sha256_common.rs new file mode 100644 index 000000000..417afb383 --- /dev/null +++ b/prover/src/tables/sha256_common.rs @@ -0,0 +1,69 @@ +//! Shared encodings for the SHA-256 chips. Words use least-significant bit first +//! internally; the core chip binds these words to big-endian memory bytes. +use super::types::{BusId, GoldilocksExtension as E, GoldilocksField as F, VmTable}; +use stark::constraints::builder::ConstraintBuilder; +use stark::{ + lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}, + trace::TraceTable, +}; +pub fn col(c: usize) -> BusValue { + BusValue::Packed { + start_column: c, + packing: Packing::Direct, + } +} +pub fn lin(v: Vec<(usize, i64)>, k: i64) -> BusValue { + BusValue::linear( + v.into_iter() + .map(|(column, coefficient)| LinearTerm::Column { + column, + coefficient, + }) + .chain(std::iter::once(LinearTerm::Constant(k))) + .collect(), + ) +} +pub fn bits(c: usize, n: usize) -> BusValue { + lin((0..n).map(|i| (c + i, 1i64 << i)).collect(), 0) +} +pub fn be(c: usize) -> BusValue { + lin((0..4).map(|i| (c + i, 1i64 << (8 * (3 - i)))).collect(), 0) +} +pub fn half(c: usize) -> BusValue { + lin(vec![(c, 1), (c + 1, 65536)], 0) +} +pub fn send(bus: BusId, mu: usize, v: Vec) -> BusInteraction { + BusInteraction::sender(bus, Multiplicity::Column(mu), v) +} +pub fn recv(bus: BusId, mu: usize, v: Vec) -> BusInteraction { + BusInteraction::receiver(bus, Multiplicity::Column(mu), v) +} +pub fn trace(rows: Vec>, width: usize) -> TraceTable { + let n = rows.len().next_power_of_two().max(4); + let mut t = TraceTable::new_main(super::types::zeroed_fe_vec(n * width), width, 1); + for (r, row) in rows.iter().enumerate() { + for (c, x) in row.iter().enumerate() { + t.main_table.set_u64(r, c, *x); + } + } + t +} +pub fn put_bits(row: &mut [u64], c: usize, x: u64, n: usize) { + for i in 0..n { + row[c + i] = (x >> i) & 1; + } +} +pub fn word>(b: &B, c: usize, n: usize) -> B::Expr { + let mut x = b.const_base(0); + for i in 0..n { + x = x + b.main(0, c + i) * b.const_base(1u64 << i); + } + x +} +pub fn check_bits>(b: &mut B, idx: &mut usize, c: usize, n: usize) { + for i in 0..n { + let x = b.main(0, c + i); + b.emit_base(*idx, x.clone() * (x - b.const_base(1))); + *idx += 1; + } +} diff --git a/prover/src/tables/sha256_k.rs b/prover/src/tables/sha256_k.rs new file mode 100644 index 000000000..896a13609 --- /dev/null +++ b/prover/src/tables/sha256_k.rs @@ -0,0 +1,49 @@ +//! SHA256_K: verifier-committed 64-round constant table. +use super::{ + sha256_common::*, + types::{BusId, FE, GoldilocksExtension, GoldilocksField}, +}; +use math::polynomial::Polynomial; +use stark::commitment::{ROWS_PER_LEAF, commit_bit_reversed}; +use stark::prover::evaluate_polynomial_on_lde_domain; +use stark::{ + config::Commitment, lookup::BusInteraction, proof::options::ProofOptions, trace::TraceTable, +}; +pub const WIDTH: usize = 3; +pub fn generate(n: usize) -> TraceTable { + trace( + (0..64) + .map(|i| vec![i as u64, executor::sha256::K[i] as u64, n as u64]) + .collect(), + WIDTH, + ) +} +pub fn bus_interactions() -> Vec { + vec![recv(BusId::ShaK, 2, vec![col(0), col(1)])] +} +pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { + let columns: Vec> = vec![ + (0..64).map(|i| FE::from(i as u64)).collect(), + executor::sha256::K + .iter() + .map(|k| FE::from(*k as u64)) + .collect(), + ]; + let polys: Vec<_> = columns + .iter() + .map(|c| Polynomial::interpolate_fft::(c).unwrap()) + .collect(); + let lde: Vec<_> = polys + .iter() + .map(|p| { + evaluate_polynomial_on_lde_domain( + p, + options.blowup_factor as usize, + 64, + &FE::from(options.coset_offset), + ) + .unwrap() + }) + .collect(); + commit_bit_reversed(&lde, ROWS_PER_LEAF).unwrap().1 +} diff --git a/prover/src/tables/sha256_rotxor.rs b/prover/src/tables/sha256_rotxor.rs new file mode 100644 index 000000000..69586f0d4 --- /dev/null +++ b/prover/src/tables/sha256_rotxor.rs @@ -0,0 +1,101 @@ +//! ROTXOR specialized to the four parameter tuples used by SHA256. +//! Bit constraints replace the spec's HWSL/byte lookups, preserving its bus. +use super::{ + sha256_common::*, + types::{BusId, GoldilocksExtension as E, GoldilocksField as F}, +}; +use stark::{ + constraints::builder::{ConstraintBuilder, ConstraintSet}, + lookup::{BusInteraction, BusValue}, + trace::TraceTable, +}; +pub const WIDTH: usize = 197; +pub const MU: usize = 68; +pub const PARAMS: [[u64; 4]; 4] = [[2, 11, 3, 0], [3, 2, 10, 0], [6, 9, 2, 1], [9, 14, 6, 1]]; +pub fn generate(ops: &[(u32, usize)]) -> TraceTable { + trace( + ops.iter() + .map(|&(x, k)| { + let mut r = vec![0; WIDTH]; + put_bits(&mut r, 0, x as u64, 32); + put_bits(&mut r, 32, executor::sha256::sigma(x, k) as u64, 32); + r[64 + k] = 1; + r[MU] = 1; + for (j, (a, b)) in [(7, 18), (17, 19), (2, 13), (6, 11)] + .into_iter() + .enumerate() + { + put_bits( + &mut r, + 69 + 32 * j, + (x.rotate_right(a) ^ x.rotate_right(b)) as u64, + 32, + ); + } + r + }) + .collect(), + WIDTH, + ) +} +pub fn bus_interactions() -> Vec { + let mut v = vec![bits(0, 32)]; + for (j, _) in PARAMS[0].iter().enumerate() { + v.push(lin( + (0..4).map(|k| (64 + k, PARAMS[k][j] as i64)).collect(), + 0, + )); + } + v.push(bits(32, 32)); + vec![recv(BusId::ShaRot, MU, v)] +} +pub fn request(mu: usize, input: BusValue, kind: usize, out: BusValue) -> BusInteraction { + let mut v = vec![input]; + v.extend(PARAMS[kind].map(BusValue::constant)); + v.push(out); + send(BusId::ShaRot, mu, v) +} +#[derive(Clone, Copy)] +pub struct Constraints; +impl ConstraintSet for Constraints { + fn max_degree(&self) -> usize { + 3 + } + fn eval>(&self, b: &mut B) { + let mut id = 0; + check_bits(b, &mut id, 0, WIDTH); + let sum = (0..4).fold(b.const_base(0), |s, k| s + b.main(0, 64 + k)); + b.emit_base(id, sum - b.main(0, MU)); + id += 1; + for i in 0..32 { + let mut expected = b.const_base(0); + for (k, (r0, r1, r2, rot)) in [ + (7, 18, 3, false), + (17, 19, 10, false), + (2, 13, 22, true), + (6, 11, 25, true), + ] + .into_iter() + .enumerate() + { + let x = b.main(0, (i + r0) % 32); + let y = b.main(0, (i + r1) % 32); + let z = if rot || i + r2 < 32 { + b.main(0, (i + r2) % 32) + } else { + b.const_base(0) + }; + let xy = b.main(0, 69 + 32 * k + i); + b.emit_base( + id, + xy.clone() - (x.clone() + y.clone() - b.const_base(2) * x * y), + ); + id += 1; + let xor = xy.clone() + z.clone() - b.const_base(2) * xy * z; + expected = expected + b.main(0, 64 + k) * xor; + } + b.emit_base(id, b.main(0, 32 + i) - expected); + id += 1; + } + } +} diff --git a/prover/src/tables/sha256_round.rs b/prover/src/tables/sha256_round.rs new file mode 100644 index 000000000..c9c7d4e37 --- /dev/null +++ b/prover/src/tables/sha256_round.rs @@ -0,0 +1,118 @@ +//! SHA256ROUND: the spec's round chain, with bit-decomposed state to constrain +//! Ch/Maj directly. This substitutes local bit gates for BYTE_ALU lookups. +use super::{ + sha256_common::*, + sha256_rotxor as rot, + types::{BusId, GoldilocksExtension as E, GoldilocksField as F}, +}; +use stark::{ + constraints::builder::{ConstraintBuilder, ConstraintSet}, + lookup::BusInteraction, + trace::TraceTable, +}; +pub const STATE: usize = 3; +pub const OUT: usize = 259; +pub const S0: usize = 323; +pub const S1: usize = 324; +pub const W: usize = 325; +pub const K: usize = 326; +pub const CARRY: usize = 327; +pub const MU: usize = 333; +pub const WIDTH: usize = 334; +pub fn generate(ops: &[super::sha256::Operation]) -> TraceTable { + let mut rows = vec![]; + for op in ops { + let w = executor::sha256::schedule(&op.message); + let mut s = op.state_words(); + for (i, &word) in w.iter().enumerate() { + let mut r = vec![0; WIDTH]; + r[0] = op.timestamp & 0xffffffff; + r[1] = op.timestamp >> 32; + r[2] = i as u64; + for (j, &state_word) in s.iter().enumerate() { + put_bits(&mut r, STATE + j * 32, state_word as u64, 32); + } + let out = executor::sha256::round(s, word, executor::sha256::K[i]); + put_bits(&mut r, OUT, out[0] as u64, 32); + put_bits(&mut r, OUT + 32, out[4] as u64, 32); + r[S0] = executor::sha256::sigma(s[0], 2) as u64; + r[S1] = executor::sha256::sigma(s[4], 3) as u64; + r[W] = word as u64; + r[K] = executor::sha256::K[i] as u64; + let t1 = s[7] as u64 + r[S1] + ((s[4] & s[5]) ^ (!s[4] & s[6])) as u64 + r[W] + r[K]; + let t2 = r[S0] + ((s[0] & s[1]) ^ (s[0] & s[2]) ^ (s[1] & s[2])) as u64; + put_bits(&mut r, CARRY, (t1 + t2) >> 32, 3); + put_bits(&mut r, CARRY + 3, (s[3] as u64 + t1) >> 32, 3); + r[MU] = 1; + rows.push(r); + s = out; + } + } + trace(rows, WIDTH) +} +pub fn bus_interactions() -> Vec { + let mut input = vec![col(0), col(1), col(2)]; + input.extend((0..8).map(|j| bits(STATE + 32 * j, 32))); + let mut output = vec![col(0), col(1), lin(vec![(2, 1)], 1)]; + output.extend( + [ + OUT, + STATE, + STATE + 32, + STATE + 64, + OUT + 32, + STATE + 128, + STATE + 160, + STATE + 192, + ] + .map(|c| bits(c, 32)), + ); + vec![ + recv(BusId::ShaRound, MU, input), + send(BusId::ShaRound, MU, output), + send(BusId::ShaM, MU, vec![col(0), col(1), col(2), col(W)]), + send(BusId::ShaK, MU, vec![col(2), col(K)]), + rot::request(MU, bits(STATE, 32), 2, col(S0)), + rot::request(MU, bits(STATE + 128, 32), 3, col(S1)), + ] +} +#[derive(Clone, Copy)] +pub struct Constraints; +impl ConstraintSet for Constraints { + fn max_degree(&self) -> usize { + 3 + } + fn eval>(&self, b: &mut B) { + let mut id = 0; + check_bits(b, &mut id, STATE, 320); + check_bits(b, &mut id, CARRY, 6); + check_bits(b, &mut id, MU, 1); + let mut ch = b.const_base(0); + let mut maj = b.const_base(0); + for i in 0..32 { + let a = b.main(0, STATE + i); + let bb = b.main(0, STATE + 32 + i); + let c = b.main(0, STATE + 64 + i); + let e = b.main(0, STATE + 128 + i); + let f = b.main(0, STATE + 160 + i); + let g = b.main(0, STATE + 192 + i); + let m = + a.clone() * bb.clone() + c * (a.clone() + bb.clone() - b.const_base(2) * a * bb); + maj = maj + b.const_base(1u64 << i) * m; + ch = ch + b.const_base(1u64 << i) * (e.clone() * f + (b.const_base(1) - e) * g); + } + let t1 = word(b, STATE + 224, 32) + b.main(0, S1) + ch + b.main(0, W) + b.main(0, K); + let t2 = b.main(0, S0) + maj; + b.emit_base( + id, + t1.clone() + t2 - word(b, OUT, 32) - b.const_base(1 << 32) * word(b, CARRY, 3), + ); + id += 1; + b.emit_base( + id, + word(b, STATE + 96, 32) + t1 + - word(b, OUT + 32, 32) + - b.const_base(1 << 32) * word(b, CARRY + 3, 3), + ); + } +} diff --git a/prover/src/tables/sha256_schedule.rs b/prover/src/tables/sha256_schedule.rs new file mode 100644 index 000000000..7a5d1c1a1 --- /dev/null +++ b/prover/src/tables/sha256_schedule.rs @@ -0,0 +1,92 @@ +//! SHA256_M lookback schedule, words 16..63. Dependency multiplicities are +//! derived from the fixed schedule DAG; every word is bound to this invocation. +use super::{ + sha256_common::*, + sha256_rotxor as rot, + types::{BusId, GoldilocksExtension as E, GoldilocksField as F}, +}; +use stark::{ + constraints::builder::{ConstraintBuilder, ConstraintSet}, + lookup::BusInteraction, + trace::TraceTable, +}; +pub const WIDTH: usize = 46; +pub const MU: usize = 45; +// timestamp 0..2, index 2, back2/back7/back15/back16 3..7, s0/s1 7..9, +// out bits 9..41, carry bits 41..43, amount 43, index-16 44, mu 45. +pub fn amount(i: usize) -> u64 { + 1 + [2, 7, 15, 16] + .into_iter() + .filter(|d| i + d >= 16 && i + d < 64) + .count() as u64 +} +pub fn generate(ops: &[super::sha256::Operation]) -> TraceTable { + let mut rows = vec![]; + for op in ops { + let w = executor::sha256::schedule(&op.message); + for i in 16..64 { + let mut r = vec![0; WIDTH]; + r[0] = op.timestamp & 0xffffffff; + r[1] = op.timestamp >> 32; + r[2] = i as u64; + for (j, d) in [2, 7, 15, 16].into_iter().enumerate() { + r[3 + j] = w[i - d] as u64; + } + r[7] = executor::sha256::sigma(w[i - 15], 0) as u64; + r[8] = executor::sha256::sigma(w[i - 2], 1) as u64; + put_bits(&mut r, 9, w[i] as u64, 32); + let sum = r[6] + r[7] + r[4] + r[8]; + put_bits(&mut r, 41, sum >> 32, 2); + r[43] = amount(i); + r[44] = (i - 16) as u64; + r[MU] = 1; + rows.push(r); + } + } + trace(rows, WIDTH) +} +pub fn bus_interactions() -> Vec { + let mut v = vec![]; + for (j, d) in [2, 7, 15, 16].into_iter().enumerate() { + v.push(send( + BusId::ShaM, + MU, + vec![col(0), col(1), lin(vec![(2, 1)], -d), col(3 + j)], + )); + } + v.push(rot::request(MU, col(5), 0, col(7))); + v.push(rot::request(MU, col(3), 1, col(8))); + v.push(recv( + BusId::ShaM, + 43, + vec![col(0), col(1), col(2), bits(9, 32)], + )); + v.push(send( + BusId::AreBytes, + MU, + vec![col(44), stark::lookup::BusValue::constant(0)], + )); + v +} +#[derive(Clone, Copy)] +pub struct Constraints; +impl ConstraintSet for Constraints { + fn eval>(&self, b: &mut B) { + let mut id = 0; + check_bits(b, &mut id, 9, 34); + check_bits(b, &mut id, MU, 1); + b.emit_base( + id, + b.main(0, 6) + b.main(0, 7) + b.main(0, 4) + b.main(0, 8) + - word(b, 9, 32) + - b.const_base(1 << 32) * word(b, 41, 2), + ); + id += 1; + b.emit_base( + id, + b.main(0, MU) * (b.main(0, 2) - b.const_base(16) - b.main(0, 44)), + ); + id += 1; + b.emit_base(id, (b.const_base(1) - b.main(0, MU)) * b.main(0, 43)); + } +} diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 29874caef..c2188c8f8 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -67,6 +67,7 @@ use super::register::{self, FinalRegisterStateMap, FinalRegisterWordState}; use super::shift::{self, ShiftOperation}; use super::store; use super::types::{GoldilocksExtension, GoldilocksField}; +use super::{sha256, sha256_k, sha256_rotxor, sha256_round, sha256_schedule}; use crate::Error; use crate::paged_mem::{ImageSource, PagedMem}; @@ -547,6 +548,7 @@ fn collect_ops_from_cpu( Vec, Vec, Vec, + Vec, Vec, Vec, Vec, @@ -559,6 +561,7 @@ fn collect_ops_from_cpu( let mut bitwise_ops = Vec::with_capacity(cpu_ops.len() * 4); let mut commit_ops = Vec::new(); let mut keccak_ops = Vec::new(); + let mut sha256_ops = Vec::new(); let mut cpu32_ops = Vec::new(); let mut ecsm_ops = Vec::new(); let mut ecdas_ops = Vec::new(); @@ -649,6 +652,11 @@ fn collect_ops_from_cpu( } // Collect ECSM ecall operations (memory I/O + the two table row sets) + if op.ecall_sha256 { + let (mem, sha) = collect_sha256_ops(op, memory_state, register_state); + memw.extend_ops(mem); + sha256_ops.push(sha); + } if op.ecall_ecsm { let (ecsm_memw, ecsm_op, ecdas_rows) = collect_ecsm_ops(op, memory_state, register_state); @@ -716,6 +724,7 @@ fn collect_ops_from_cpu( bitwise_ops, commit_ops, keccak_ops, + sha256_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -2857,6 +2866,11 @@ pub struct Traces { /// KECCAK core table (one row per keccak permutation call) pub keccak: TraceTable, + pub sha256: TraceTable, + pub sha256_round: TraceTable, + pub sha256_schedule: TraceTable, + pub sha256_rotxor: TraceTable, + pub sha256_k: TraceTable, /// KECCAK_RND round table (24 rows per keccak call) pub keccak_rnd: TraceTable, @@ -2907,6 +2921,7 @@ struct CollectedOps { dvrm_ops: Vec<(DvrmOperation, bool)>, commit_ops: Vec, keccak_ops: Vec, + sha256_ops: Vec, // Auxiliary ALU / memory / CPU32 dispatch chips (driven by the CPU ALU/MEMORY dispatch). eq_ops: Vec, bytewise_ops: Vec, @@ -2968,6 +2983,7 @@ fn collect_all_ops( mut bitwise_ops: Vec, commit_ops: Vec, keccak_ops: Vec, + sha256_ops: Vec, cpu32_ops: Vec, ecsm_ops: Vec, ecdas_ops: Vec, @@ -3108,6 +3124,7 @@ fn collect_all_ops( dvrm_ops, commit_ops, keccak_ops, + sha256_ops, eq_ops, bytewise_ops, store_ops, @@ -3152,6 +3169,7 @@ fn build_traces( dvrm_ops, commit_ops, keccak_ops, + sha256_ops, eq_ops, bytewise_ops, store_ops, @@ -3243,6 +3261,7 @@ fn build_traces( Box::new(|h| h.add_ops(&collect_bitwise_from_memw_aligned(&memw_aligned_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_commit(&commit_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_keccak(&keccak_ops))), + Box::new(|h| h.add_ops(&sha256::bitwise_ops(&sha256_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_ecsm(&ecsm_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_ecdas(&ecdas_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_hint(&hint_ops))), @@ -3715,6 +3734,11 @@ fn build_traces( halt: halt_trace, commit: commit_trace, keccak: keccak_trace, + sha256: sha256::generate(&sha256_ops), + sha256_round: sha256_round::generate(&sha256_ops), + sha256_schedule: sha256_schedule::generate(&sha256_ops), + sha256_rotxor: sha256_rotxor::generate(&sha256::rot_ops(&sha256_ops)), + sha256_k: sha256_k::generate(sha256_ops.len()), keccak_rnd: keccak_rnd_trace, keccak_rc: keccak_rc_trace, ecsm: ecsm_trace, @@ -3752,6 +3776,7 @@ fn padded_chunked_rows(ops_count: usize, max_rows: usize) -> u64 { #[cfg(feature = "disk-spill")] #[derive(Debug, Default, Clone)] pub struct TableLengths { + pub sha256_calls: u64, pub cpu_padded_rows: u64, pub memw_padded_rows: u64, pub memw_aligned_padded_rows: u64, @@ -3802,6 +3827,7 @@ pub fn count_table_lengths( let mut dvrm_count = 0usize; let mut branch_count = 0usize; let mut commit_count = 0usize; + let mut sha256_count = 0u64; let mut current_commit_index = 0u32; let partition_memw = |op: &MemwOperation, @@ -3869,6 +3895,19 @@ pub fn count_table_lengths( ); } + if cpu_op.ecall_sha256 { + sha256_count += 1; + let (ops, _) = collect_sha256_ops(&cpu_op, &mut memory_state, &mut register_state); + for memw_op in &ops { + partition_memw( + memw_op, + &mut memw_by_width, + &mut memw_aligned_count, + &mut memw_register_count, + ); + } + } + // ECALL Commit if cpu_op.ecall_commit { // Match `expand_commit_operations_for_ecall`'s `0..=count` loop @@ -3957,6 +3996,7 @@ pub fn count_table_lengths( let cycle_count = logs.len() as u64; Ok(TableLengths { + sha256_calls: sha256_count, cpu_padded_rows: padded_chunked_rows(cpu_count, max_rows.cpu), memw_padded_rows: padded_chunked_rows(memw_count, max_rows.memw), memw_aligned_padded_rows: padded_chunked_rows(memw_aligned_count, max_rows.memw_aligned), @@ -4036,6 +4076,11 @@ impl Traces { halt, commit, keccak, + sha256, + sha256_round, + sha256_schedule, + sha256_rotxor, + sha256_k, keccak_rnd, keccak_rc, ecsm, @@ -4092,6 +4137,11 @@ impl Traces { total += (t.num_rows() * MEMW_R_COLS) as u64; } total += (keccak.num_rows() * KECCAK_COLS) as u64; + total += (sha256.num_rows() * sha256::WIDTH) as u64; + total += (sha256_round.num_rows() * sha256_round::WIDTH) as u64; + total += (sha256_schedule.num_rows() * sha256_schedule::WIDTH) as u64; + total += (sha256_rotxor.num_rows() * sha256_rotxor::WIDTH) as u64; + total += (sha256_k.num_rows() * (sha256_k::WIDTH - 2)) as u64; total += (keccak_rnd.num_rows() * KECCAK_RND_COLS) as u64; total += (keccak_rc.num_rows() * (KECCAK_RC_COLS - KECCAK_RC_PRECOMPUTED)) as u64; for t in eqs { @@ -4169,6 +4219,11 @@ impl Traces { halt, commit, keccak, + sha256, + sha256_round, + sha256_schedule, + sha256_rotxor, + sha256_k, keccak_rnd, keccak_rc, ecsm, @@ -4225,6 +4280,15 @@ impl Traces { total += (t.num_rows() * n_memw_r) as u64; } total += (keccak.num_rows() * n_keccak) as u64; + total += (sha256.num_rows() * aux_cols(sha256::bus_interactions().len())) as u64; + total += + (sha256_round.num_rows() * aux_cols(sha256_round::bus_interactions().len())) as u64; + total += (sha256_schedule.num_rows() * aux_cols(sha256_schedule::bus_interactions().len())) + as u64; + total += + (sha256_rotxor.num_rows() * aux_cols(sha256_rotxor::bus_interactions().len())) as u64; + total += (sha256_k.num_rows() * aux_cols(sha256_k::bus_interactions().len())) as u64; + total += (keccak_rnd.num_rows() * n_keccak_rnd) as u64; total += (keccak_rc.num_rows() * n_keccak_rc) as u64; for t in eqs { @@ -4592,6 +4656,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + sha256_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4611,6 +4676,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + sha256_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4705,6 +4771,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + sha256_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4720,6 +4787,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + sha256_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4749,3 +4817,49 @@ impl Traces { ) } } + +/// SHA compression memory accesses: registers at T, message at T, then state +/// read/write at T+1. Snapshot before writes permits arbitrary operand overlap. +fn collect_sha256_ops( + op: &CpuOperation, + memory: &mut MemoryState, + registers: &mut RegisterState, +) -> (Vec, sha256::Operation) { + let t = op.timestamp; + let h_addr = registers.read(10).0; + let m_addr = registers.read(11).0; + let state = std::array::from_fn(|i| memory.read_byte(h_addr + i as u64).0); + let message = std::array::from_fn(|i| memory.read_byte(m_addr + i as u64).0); + let sha = sha256::Operation { + timestamp: t, + state_addr: h_addr, + message_addr: m_addr, + state, + message, + }; + let mut output = state; + executor::sha256::compress(&mut output, &message); + let mut ops = vec![]; + for reg in [10, 11] { + let (val, old) = registers.read(reg); + let value = pack_register_value(val); + ops.push( + MemwOperation::new(true, 2 * reg as u64, value, t, 2, true) + .with_old(value, [old, old, 0, 0, 0, 0, 0, 0]), + ); + registers.write(reg, val, t); + } + for (addr, bytes, time, read) in [ + (m_addr, message.as_slice(), t, true), + (h_addr, output.as_slice(), t + 1, true), + ] { + for (i, chunk) in bytes.chunks_exact(8).enumerate() { + let addr = addr + 8 * i as u64; + let (old, old_ts) = memory.read_bytes(addr, 8); + let value = std::array::from_fn(|j| chunk[j] as u32); + ops.push(MemwOperation::new(false, addr, value, time, 8, read).with_old(old, old_ts)); + memory.write_bytes(addr, u64::from_le_bytes(chunk.try_into().unwrap()), 8, time); + } + } + (ops, sha) +} diff --git a/prover/src/tables/types.rs b/prover/src/tables/types.rs index fab4aabff..1c2e03bf2 100644 --- a/prover/src/tables/types.rs +++ b/prover/src/tables/types.rs @@ -359,6 +359,10 @@ pub enum BusId { /// Cross-epoch memory bus: the local-to-global table's per-cell init/fini /// boundary claims, matched across epochs by the final aggregation LogUp. GlobalMemory = 31, + ShaRound = 32, + ShaM = 33, + ShaRot = 34, + ShaK = 35, } impl BusId { @@ -388,6 +392,10 @@ impl BusId { BusId::Ecdas => "Ecdas", BusId::Bit => "Bit", BusId::GlobalMemory => "GlobalMemory", + BusId::ShaRound => "ShaRound", + BusId::ShaM => "ShaM", + BusId::ShaRot => "ShaRot", + BusId::ShaK => "ShaK", } } } @@ -420,6 +428,10 @@ impl TryFrom for BusId { 28 => Ok(BusId::Ecdas), 30 => Ok(BusId::Bit), 31 => Ok(BusId::GlobalMemory), + 32 => Ok(BusId::ShaRound), + 33 => Ok(BusId::ShaM), + 34 => Ok(BusId::ShaRot), + 35 => Ok(BusId::ShaK), other => Err(other), } } diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index d6a8b8608..7df37dce7 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -1019,3 +1019,72 @@ pub fn create_ecdas_air(proof_options: &ProofOptions) -> ConcreteVmAir ConcreteVmAir { + use crate::tables::sha256 as chip; + build_air( + chip::WIDTH, + chip::bus_interactions(), + options, + 1, + chip::Constraints, + "SHA256", + ) +} + +pub fn create_sha256_round_air( + options: &ProofOptions, +) -> ConcreteVmAir { + use crate::tables::sha256_round as chip; + build_air( + chip::WIDTH, + chip::bus_interactions(), + options, + 1, + chip::Constraints, + "SHA256ROUND", + ) +} + +pub fn create_sha256_schedule_air( + options: &ProofOptions, +) -> ConcreteVmAir { + use crate::tables::sha256_schedule as chip; + build_air( + chip::WIDTH, + chip::bus_interactions(), + options, + 1, + chip::Constraints, + "SHA256MSGSCHED", + ) +} + +pub fn create_sha256_rotxor_air( + options: &ProofOptions, +) -> ConcreteVmAir { + use crate::tables::sha256_rotxor as chip; + build_air( + chip::WIDTH, + chip::bus_interactions(), + options, + 1, + chip::Constraints, + "ROTXOR", + ) +} + +pub fn create_sha256_k_air(options: &ProofOptions) -> ConcreteVmAir { + use crate::tables::sha256_k as chip; + build_air( + chip::WIDTH, + chip::bus_interactions(), + options, + 1, + EmptyConstraints, + "SHA256_K", + ) + .with_preprocessed(chip::preprocessed_commitment(options), 2) +} diff --git a/prover/src/tests/auto_storage_tests.rs b/prover/src/tests/auto_storage_tests.rs index 5d976f81b..79aa7dc2d 100644 --- a/prover/src/tests/auto_storage_tests.rs +++ b/prover/src/tests/auto_storage_tests.rs @@ -29,7 +29,7 @@ fn peak_bytes_per_table_increment_is_exact() { let baseline = peak_bytes(&empty_lengths(), blowup, ALL_TABLES); let mut lengths = empty_lengths(); - lengths.cpu_padded_rows = 4; + lengths.cpu_padded_rows = 8; let bumped = peak_bytes(&lengths, blowup, ALL_TABLES); let cpu_main = CPU_COLS as u64; @@ -45,11 +45,11 @@ fn peak_bytes_per_table_increment_is_exact() { + b * KECCAK_NODE_BYTES; // FRI Merkle (geometric ≈ 1) let per_row_domain = (3 + 2 * b) * GOLDILOCKS_BYTES; - // CPU adds 4 rows of persistent + transient (top-k by ALL_TABLES) + - // its 4-row Domain entry (a fresh unique key not previously present). + // CPU adds 8 rows of persistent + transient (top-k by ALL_TABLES) + + // its 8-row Domain entry (a fresh unique key not previously present). assert_eq!( bumped - baseline, - 4 * (per_row_persistent + per_row_transient + per_row_domain) + 8 * (per_row_persistent + per_row_transient + per_row_domain) ); } diff --git a/prover/src/tests/constraint_program_device_tests.rs b/prover/src/tests/constraint_program_device_tests.rs index a29a7cb49..a26a14e0b 100644 --- a/prover/src/tests/constraint_program_device_tests.rs +++ b/prover/src/tests/constraint_program_device_tests.rs @@ -180,6 +180,21 @@ fn all_table_programs_lower_and_match_folders() { check_air_device(&create_keccak_rnd_air(&opts), "KECCAK_RND"); check_air_device(&create_keccak_rc_air(&opts), "KECCAK_RC"); check_air_device(&create_ecsm_air(&opts), "ECSM"); + check_air_device(&crate::test_utils::create_sha256_air(&opts), "sha256"); + check_air_device( + &crate::test_utils::create_sha256_round_air(&opts), + "sha256_round", + ); + check_air_device( + &crate::test_utils::create_sha256_schedule_air(&opts), + "sha256_schedule", + ); + check_air_device( + &crate::test_utils::create_sha256_rotxor_air(&opts), + "sha256_rotxor", + ); + check_air_device(&crate::test_utils::create_sha256_k_air(&opts), "sha256_k"); + check_air_device(&create_ecdas_air(&opts), "ECDAS"); check_air_device(&create_hint_air(&opts), "HINT"); } diff --git a/prover/src/tests/constraint_program_tests.rs b/prover/src/tests/constraint_program_tests.rs index e227da53d..0d655ed10 100644 --- a/prover/src/tests/constraint_program_tests.rs +++ b/prover/src/tests/constraint_program_tests.rs @@ -178,6 +178,21 @@ fn all_table_programs_match_folders() { check_air(&create_keccak_rnd_air(&opts), "KECCAK_RND"); check_air(&create_keccak_rc_air(&opts), "KECCAK_RC"); check_air(&create_ecsm_air(&opts), "ECSM"); + check_air(&crate::test_utils::create_sha256_air(&opts), "sha256"); + check_air( + &crate::test_utils::create_sha256_round_air(&opts), + "sha256_round", + ); + check_air( + &crate::test_utils::create_sha256_schedule_air(&opts), + "sha256_schedule", + ); + check_air( + &crate::test_utils::create_sha256_rotxor_air(&opts), + "sha256_rotxor", + ); + check_air(&crate::test_utils::create_sha256_k_air(&opts), "sha256_k"); + check_air(&create_ecdas_air(&opts), "ECDAS"); check_air(&create_hint_air(&opts), "HINT"); } diff --git a/prover/src/tests/count_table_lengths_drift_tests.rs b/prover/src/tests/count_table_lengths_drift_tests.rs index 7337f0790..5c5504af6 100644 --- a/prover/src/tests/count_table_lengths_drift_tests.rs +++ b/prover/src/tests/count_table_lengths_drift_tests.rs @@ -126,3 +126,36 @@ fn count_table_lengths_matches_nonempty_hint_trace() { ); assert_count_table_lengths_matches(&elf, &result.logs); } + +#[test] +fn count_table_lengths_sha256_memory() { + let (elf, logs, _) = run_asm_elf("test_sha256_overlap"); + let predicted = count_table_lengths(&elf, &logs, &MaxRowsConfig::default(), &[]).unwrap(); + let traces = + Traces::from_elf_and_logs_minimal(&elf, &logs, &MaxRowsConfig::default(), &[]).unwrap(); + assert_eq!(predicted.sha256_calls, 3); + assert_eq!( + predicted.memw_padded_rows, + traces + .memws + .iter() + .map(|t| t.num_rows() as u64) + .sum::() + ); + assert_eq!( + predicted.memw_aligned_padded_rows, + traces + .memw_aligneds + .iter() + .map(|t| t.num_rows() as u64) + .sum::() + ); + assert_eq!( + predicted.memw_register_padded_rows, + traces + .memw_registers + .iter() + .map(|t| t.num_rows() as u64) + .sum::() + ); +} diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 9288cf2ac..6cfe46304 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -98,3 +98,5 @@ pub mod templates_tests; pub mod trace_builder_tests; #[cfg(test)] pub mod trace_test_helpers; + +mod sha256_tests; diff --git a/prover/src/tests/ood_window_ir_tests.rs b/prover/src/tests/ood_window_ir_tests.rs index 29d224627..195ab8869 100644 --- a/prover/src/tests/ood_window_ir_tests.rs +++ b/prover/src/tests/ood_window_ir_tests.rs @@ -113,6 +113,28 @@ fn all_table_windows_match_captured_ir() { assert_ood_window_matches_ir(&create_keccak_rnd_air(&opts), true, "KECCAK_RND"); assert_ood_window_matches_ir(&create_keccak_rc_air(&opts), true, "KECCAK_RC"); assert_ood_window_matches_ir(&create_ecsm_air(&opts), true, "ECSM"); + assert_ood_window_matches_ir(&crate::test_utils::create_sha256_air(&opts), true, "sha256"); + assert_ood_window_matches_ir( + &crate::test_utils::create_sha256_round_air(&opts), + true, + "sha256_round", + ); + assert_ood_window_matches_ir( + &crate::test_utils::create_sha256_schedule_air(&opts), + true, + "sha256_schedule", + ); + assert_ood_window_matches_ir( + &crate::test_utils::create_sha256_rotxor_air(&opts), + true, + "sha256_rotxor", + ); + assert_ood_window_matches_ir( + &crate::test_utils::create_sha256_k_air(&opts), + true, + "sha256_k", + ); + assert_ood_window_matches_ir(&create_ecdas_air(&opts), true, "ECDAS"); assert_ood_window_matches_ir(&create_hint_air(&opts), true, "HINT"); } diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index bbc8d2c63..bf1275bb5 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -3862,3 +3862,44 @@ fn test_epoch_memory_bus_with_l2g_bookend() { "epoch Memory bus must balance with L2G bookend + PAGE excluding touched cells" ); } + +#[test] +fn test_prove_elfs_sha256() { + let (elf, logs, _) = run_asm_elf("test_sha256"); + let mut traces = + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + let expected = [ + 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, + 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, + 0x15, 0xad, + ]; + assert_eq!(traces.public_output_bytes, expected); + assert!(prove_and_verify_vm_minimal(&elf, &mut traces)); +} + +#[test] +fn test_prove_elfs_sha256_overlap_and_tampering() { + let (elf, logs, _) = run_asm_elf("test_sha256_overlap"); + let mut traces = + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + assert!(prove_and_verify_vm_minimal(&elf, &mut traces)); + // Each mutation preserves the original memory/output claims. It must fail + // either local arithmetic or the cross-table lookup that authenticates it. + for target in 0..5 { + let mut traces = + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + let (table, col) = match target { + 0 => (&mut traces.sha256, crate::tables::sha256::OUT), + 1 => (&mut traces.sha256, crate::tables::sha256::PTR), + 2 => (&mut traces.sha256_round, crate::tables::sha256_round::K), + 3 => (&mut traces.sha256_schedule, 3), + _ => (&mut traces.sha256_rotxor, 32), + }; + let old = *table.main_table.get(0, col); + table.main_table.set(0, col, old + FieldElement::::one()); + assert!( + !prove_and_verify_vm_minimal(&elf, &mut traces), + "accepted tamper {target}" + ); + } +} diff --git a/prover/src/tests/sha256_tests.rs b/prover/src/tests/sha256_tests.rs new file mode 100644 index 000000000..d4f71a140 --- /dev/null +++ b/prover/src/tests/sha256_tests.rs @@ -0,0 +1,67 @@ +//! Algebraic and trace mutation tests for the SHA compression chips. +use crate::tables::types::{FE, GoldilocksExtension as E, GoldilocksField as F}; +use crate::tables::{sha256, sha256_rotxor, sha256_round, sha256_schedule}; +use stark::{ + constraints::builder::{ConstraintSet, ProverEvalFolder}, + frame::Frame, + table::TableView, + trace::TraceTable, + traits::TransitionEvaluationContext, +}; +fn holds>(c: C, t: &TraceTable) -> bool { + for row in 0..t.num_rows() { + let main = (0..t.main_table.width) + .map(|i| *t.main_table.get(row, i)) + .collect(); + let frame = Frame::::new(vec![TableView::new(vec![main], vec![vec![]])]); + let empty = vec![]; + let zero = math::field::element::FieldElement::::zero(); + let ctx = + TransitionEvaluationContext::new_prover(frame.as_row_frame(), &empty, &empty, &zero); + let mut base = vec![FE::zero(); c.meta().len()]; + let mut ext = vec![zero; c.meta().len()]; + c.eval(&mut ProverEvalFolder::new(&ctx, &mut base, &mut ext)); + if base.iter().any(|x| *x != FE::zero()) { + eprintln!( + "row {row} failures {:?}", + base.iter() + .enumerate() + .filter(|(_, v)| **v != FE::zero()) + .collect::>() + ); + return false; + } + } + true +} +fn ops() -> Vec { + (0..3) + .map(|i| sha256::Operation { + timestamp: 400 + 4 * i, + state_addr: 0x1003, + message_addr: 0x2007, + state: std::array::from_fn(|j| (j * 17 + i as usize) as u8), + message: std::array::from_fn(|j| (j * 23 + i as usize) as u8), + }) + .collect() +} +#[test] +fn sha256_constraints_and_mutations() { + let ops = ops(); + let mut core = sha256::generate(&ops); + assert!(holds(sha256::Constraints, &core)); + core.main_table.set(0, sha256::OUT, FE::from(256)); + assert!(!holds(sha256::Constraints, &core)); + let mut rounds = sha256_round::generate(&ops); + assert!(holds(sha256_round::Constraints, &rounds)); + rounds.main_table.set(0, sha256_round::OUT, FE::from(2)); + assert!(!holds(sha256_round::Constraints, &rounds)); + let mut schedule = sha256_schedule::generate(&ops); + assert!(holds(sha256_schedule::Constraints, &schedule)); + schedule.main_table.set(0, 9, FE::from(2)); + assert!(!holds(sha256_schedule::Constraints, &schedule)); + let mut rot = sha256_rotxor::generate(&sha256::rot_ops(&ops)); + assert!(holds(sha256_rotxor::Constraints, &rot)); + rot.main_table.set(0, 32, FE::from(2)); + assert!(!holds(sha256_rotxor::Constraints, &rot)); +} diff --git a/syscalls/src/lib.rs b/syscalls/src/lib.rs index 767f0ff71..22260c1fe 100644 --- a/syscalls/src/lib.rs +++ b/syscalls/src/lib.rs @@ -9,3 +9,5 @@ pub mod entrypoint; pub mod keccak; pub mod random; pub mod syscalls; + +pub mod sha256; diff --git a/syscalls/src/sha256.rs b/syscalls/src/sha256.rs new file mode 100644 index 000000000..4bcfb7197 --- /dev/null +++ b/syscalls/src/sha256.rs @@ -0,0 +1,40 @@ +//! SHA-256 using the compression accelerator specified in spec/sha256.typ. +//! State and chunk are big-endian bytes; the ecall permits arbitrary alignment. +#[inline] +pub fn compress(state: &mut [u8; 32], chunk: &[u8; 64]) { + #[cfg(target_arch = "riscv64")] + unsafe { + core::arch::asm!("ecall", in("a7") u64::MAX, in("a0") state.as_mut_ptr(), in("a1") chunk.as_ptr(), options(nostack)); + } + #[cfg(not(target_arch = "riscv64"))] + { + let _ = (state, chunk); + panic!("SHA256 accelerator requires LambdaVM"); + } +} +/// Standard SHA-256, including IV, length encoding, and one or two padding blocks. +pub fn sha256(input: &[u8]) -> [u8; 32] { + let mut state = [ + 0x6a, 0x09, 0xe6, 0x67, 0xbb, 0x67, 0xae, 0x85, 0x3c, 0x6e, 0xf3, 0x72, 0xa5, 0x4f, 0xf5, + 0x3a, 0x51, 0x0e, 0x52, 0x7f, 0x9b, 0x05, 0x68, 0x8c, 0x1f, 0x83, 0xd9, 0xab, 0x5b, 0xe0, + 0xcd, 0x19, + ]; + let bit_len = (input.len() as u64) + .checked_mul(8) + .expect("SHA256 input too long"); + let mut chunks = input.chunks_exact(64); + for chunk in &mut chunks { + compress(&mut state, chunk.try_into().unwrap()); + } + let rest = chunks.remainder(); + let mut last = [0u8; 64]; + last[..rest.len()].copy_from_slice(rest); + last[rest.len()] = 0x80; + if rest.len() >= 56 { + compress(&mut state, &last); + last = [0; 64]; + } + last[56..].copy_from_slice(&bit_len.to_be_bytes()); + compress(&mut state, &last); + state +} From feb14c8ab4fba3b62827e961c97b72db9b4d9454 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 10:39:31 -0300 Subject: [PATCH 2/8] Drop the dead shape_parts scratch hook --- prover/src/continuation.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index 5b9c7ea69..52f5fadd9 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -490,26 +490,6 @@ impl ContinuationProof { pub fn num_epochs(&self) -> usize { self.epochs.len() } - - /// SCRATCH ANALYSIS HOOK (safe to delete): per-epoch `(proof, table_counts)` - /// plus the cross-epoch global proof, so out-of-crate tooling can measure - /// proof shape (widths, Merkle depths, FRI layers) without re-deriving it. - /// Read-only borrows; nothing here is used by prove/verify. - #[allow(clippy::type_complexity)] - pub fn shape_parts( - &self, - ) -> ( - Vec<(&MultiProof, &TableCounts)>, - &MultiProof, - ) { - ( - self.epochs - .iter() - .map(|e| (&e.proof, &e.table_counts)) - .collect(), - &self.global, - ) - } } /// Borrowed view over an [`EpochProof`] (owned or archived-in-place). Lets From 3f27a3d1d1cfd44d4451be6c461d87e0f28157c5 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 10:39:33 -0300 Subject: [PATCH 3/8] Range-check SHA256 MU on its own --- prover/src/tables/sha256.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/prover/src/tables/sha256.rs b/prover/src/tables/sha256.rs index 055f2a805..7ec0e0363 100644 --- a/prover/src/tables/sha256.rs +++ b/prover/src/tables/sha256.rs @@ -177,7 +177,12 @@ impl ConstraintSet for Constraints { } fn eval>(&self, b: &mut B) { let mut id = 0; - check_bits(b, &mut id, CARRY, 9); + // The eight feed-forward carries, then MU on its own. MU is the + // multiplicity of the ECALL receive, of all fourteen MEMW sends and of + // both SHA256ROUND interactions, so its range check must not depend on + // it happening to sit in the column right after the carries. + check_bits(b, &mut id, CARRY, 8); + check_bits(b, &mut id, MU, 1); emit_add_pair( b, id, From 4e0956aaad9b2d4aafd3eb08f725a326cbce5ae1 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 10:39:34 -0300 Subject: [PATCH 4/8] Ship SHA256_K commitment as static bytes --- prover/src/bin/compute_static_commitments.rs | 10 +- prover/src/tables/sha256_k.rs | 100 +++++++++++++++++-- prover/src/test_utils.rs | 5 +- prover/src/tests/static_commitments_tests.rs | 60 ++++++++++- 4 files changed, 161 insertions(+), 14 deletions(-) diff --git a/prover/src/bin/compute_static_commitments.rs b/prover/src/bin/compute_static_commitments.rs index a4de1ddaa..197296f89 100644 --- a/prover/src/bin/compute_static_commitments.rs +++ b/prover/src/bin/compute_static_commitments.rs @@ -1,4 +1,4 @@ -//! Prints static `(bitwise, keccak_rc, zero_page)` preprocessed-table commitments +//! Prints static `(bitwise, keccak_rc, sha256_k, zero_page)` preprocessed-table commitments //! for a fixed set of `blowup_factor` values. The output is pasted into the //! `static_commitment` match bodies in `prover/src/tables/{bitwise,keccak_rc}.rs` //! and the `static_zero_page_commitment` match body in `prover/src/tables/page.rs`. @@ -13,7 +13,7 @@ //! `keccak_rc.rs` and `static_zero_page_commitment` in `page.rs` for when //! it's actually appropriate to bless new bytes. -use lambda_vm_prover::tables::{STATIC_BLOWUP_FACTORS, bitwise, keccak_rc, page}; +use lambda_vm_prover::tables::{STATIC_BLOWUP_FACTORS, bitwise, keccak_rc, page, sha256_k}; use stark::config::Commitment; use stark::proof::options::GoldilocksCubicProofOptions; @@ -36,7 +36,7 @@ fn format_commitment(commitment: &Commitment) -> String { fn main() { println!( "// Paste these match arms into the `static_commitment` match bodies\n\ - // in `prover/src/tables/{{bitwise,keccak_rc}}.rs` and the\n\ + // in `prover/src/tables/{{bitwise,keccak_rc,sha256_k}}.rs` and the\n\ // `static_zero_page_commitment` match body in `prover/src/tables/page.rs`.\n" ); @@ -53,6 +53,7 @@ fn main() { let bitwise = bitwise::compute_preprocessed_commitment(&options); let keccak_rc = keccak_rc::compute_preprocessed_commitment(&options); + let sha256_k = sha256_k::compute_preprocessed_commitment(&options); let zero_page = page::compute_precomputed_commitment(&zero_page_config, &options); let private_page = page::compute_offset_only_commitment(&options); @@ -62,12 +63,15 @@ fn main() { {blowup} => Some({bitwise_fmt}),\n\ // ---- keccak_rc:\n \ {blowup} => Some({keccak_fmt}),\n\ + // ---- sha256_k:\n \ + {blowup} => Some({sha256_k_fmt}),\n\ // ---- zero_page:\n \ {blowup} => Some({zero_page_fmt}),\n\ // ---- private_page (OFFSET only):\n \ {blowup} => Some({private_page_fmt}),\n", bitwise_fmt = format_commitment(&bitwise), keccak_fmt = format_commitment(&keccak_rc), + sha256_k_fmt = format_commitment(&sha256_k), zero_page_fmt = format_commitment(&zero_page), private_page_fmt = format_commitment(&private_page), ); diff --git a/prover/src/tables/sha256_k.rs b/prover/src/tables/sha256_k.rs index 896a13609..d9b90d595 100644 --- a/prover/src/tables/sha256_k.rs +++ b/prover/src/tables/sha256_k.rs @@ -10,9 +10,15 @@ use stark::{ config::Commitment, lookup::BusInteraction, proof::options::ProofOptions, trace::TraceTable, }; pub const WIDTH: usize = 3; +/// One row per round constant. Already a power of two, so the main trace and +/// the preprocessed columns cover the same domain with no padding row. +pub const NUM_ROWS: usize = 64; +/// Columns 0 and 1 (round index, constant) are the preprocessed pair; column 2 +/// is the prover's multiplicity. +pub const NUM_PRECOMPUTED_COLS: usize = 2; pub fn generate(n: usize) -> TraceTable { trace( - (0..64) + (0..NUM_ROWS) .map(|i| vec![i as u64, executor::sha256::K[i] as u64, n as u64]) .collect(), WIDTH, @@ -21,9 +27,61 @@ pub fn generate(n: usize) -> TraceTable { pub fn bus_interactions() -> Vec { vec![recv(BusId::ShaK, 2, vec![col(0), col(1)])] } -pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { + +/// Returns the static SHA256_K preprocessed commitment for `blowup_factor`, or +/// `None` if no value is shipped for it. Values were generated by the +/// `compute_static_commitments` binary at the project's standard +/// `coset_offset = 3` and are pinned by the `sha256_k_static_matches_recompute_*` +/// tests so any drift in the AIR or FFT pipeline is caught at test time. The +/// verifier reads these from its compiled binary — no input data is trusted, +/// and the recursion guest does not pay an FFT and a Merkle commit to rebuild +/// them on every verification. +/// +/// # Regenerating +/// +/// Only regenerate these match arms after a *deliberate, reviewed* change to +/// the SHA256_K table layout, the AIR's preprocessed column count, or the FFT / +/// LDE / Merkle pipeline. Run: +/// +/// ```text +/// cargo run --bin compute_static_commitments --release +/// ``` +/// +/// and paste the printed match arms over the ones below. +/// +/// **If a drift test failed, do not regenerate first.** Re-pasting on a drift +/// failure silently launders an unintended table change into the verifier's +/// compiled-in trust anchor. +fn static_commitment(blowup_factor: u8) -> Option { + match blowup_factor { + 2 => Some([ + 0xa1, 0xff, 0x3c, 0x73, 0xac, 0xd6, 0x18, 0x9d, 0x56, 0x2e, 0x9e, 0x06, 0x8c, 0xf9, + 0x07, 0x8c, 0xcf, 0x41, 0x5e, 0xe3, 0x7f, 0x69, 0xc5, 0x74, 0xe9, 0x79, 0x39, 0x66, + 0x4b, 0x9f, 0x0a, 0xdf, + ]), + 4 => Some([ + 0x4b, 0x87, 0xa2, 0xff, 0x80, 0xd3, 0x4a, 0x1c, 0x2f, 0xb1, 0x3a, 0x55, 0x14, 0xdf, + 0x82, 0x41, 0x3a, 0x2b, 0x3a, 0x86, 0xb7, 0xa8, 0x74, 0xa9, 0xa8, 0xd8, 0xce, 0x12, + 0xa1, 0xf2, 0x09, 0xe3, + ]), + 8 => Some([ + 0x86, 0x11, 0x6e, 0xf4, 0x85, 0xec, 0xe6, 0x3d, 0x22, 0xf0, 0x5d, 0x2f, 0x10, 0x6b, + 0x27, 0xa4, 0x58, 0xeb, 0x9f, 0x6b, 0xc1, 0x90, 0x8c, 0x5a, 0xa2, 0x22, 0x28, 0x2e, + 0x8d, 0x2b, 0x9e, 0xfe, + ]), + _ => None, + } +} + +/// Computes the Merkle commitment over the SHA256_K round-constant columns. +/// +/// Exposed for the `compute_static_commitments` binary and the drift-detection +/// tests in `static_commitments_tests`. Production callers should go through +/// [`preprocessed_commitment`] so the static const-table shortcut is used. +#[doc(hidden)] +pub fn compute_preprocessed_commitment(options: &ProofOptions) -> Commitment { let columns: Vec> = vec![ - (0..64).map(|i| FE::from(i as u64)).collect(), + (0..NUM_ROWS).map(|i| FE::from(i as u64)).collect(), executor::sha256::K .iter() .map(|k| FE::from(*k as u64)) @@ -31,7 +89,10 @@ pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { ]; let polys: Vec<_> = columns .iter() - .map(|c| Polynomial::interpolate_fft::(c).unwrap()) + .map(|c| { + Polynomial::interpolate_fft::(c) + .expect("FFT interpolation failed for sha256_k column") + }) .collect(); let lde: Vec<_> = polys .iter() @@ -39,11 +100,36 @@ pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { evaluate_polynomial_on_lde_domain( p, options.blowup_factor as usize, - 64, + NUM_ROWS, &FE::from(options.coset_offset), ) - .unwrap() + .expect("LDE evaluation failed for sha256_k polynomial") }) .collect(); - commit_bit_reversed(&lde, ROWS_PER_LEAF).unwrap().1 + commit_bit_reversed(&lde, ROWS_PER_LEAF) + .expect("Failed to build Merkle tree for sha256_k LDE") + .1 +} + +/// Returns the preprocessed commitment for the SHA256_K table. +/// +/// Looks up `blowup_factor` via [`static_commitment`] when `coset_offset == 3` +/// (the value every in-tree `ProofOptions` constructor pins, and the offset the +/// static bytes were generated for); on miss — either a non-3 coset or a +/// `blowup_factor` outside `STATIC_BLOWUP_FACTORS` — recomputes from scratch. +#[inline] +pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { + if options.coset_offset == 3 + && let Some(commitment) = static_commitment(options.blowup_factor) + { + return commitment; + } + log::warn!( + "sha256_k preprocessed commitment not static for (blowup={}, coset={}); \ + falling back to recompute. Add a match arm to `static_commitment` by running \ + `cargo run --bin compute_static_commitments --release`.", + options.blowup_factor, + options.coset_offset, + ); + compute_preprocessed_commitment(options) } diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index 30f013906..5736f644e 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -1089,5 +1089,8 @@ pub fn create_sha256_k_air(options: &ProofOptions) -> ConcreteVmAir stark::proof::options::ProofOptions { GoldilocksCubicProofOptions::with_blowup(blowup).expect("blowup must be a valid power of 2") @@ -76,6 +76,20 @@ fn keccak_rc_static_matches_recompute_for_all_blowups() { } } +#[test] +fn sha256_k_static_matches_recompute_for_all_blowups() { + for &blowup in STATIC_BLOWUP_FACTORS { + let options = options_for(blowup); + let from_wrapper = sha256_k::preprocessed_commitment(&options); + let recomputed = sha256_k::compute_preprocessed_commitment(&options); + assert_eq!( + from_wrapper, recomputed, + "sha256_k commitment drifted (or wrapper dispatch broke) for blowup={blowup}; \ + regenerate constants via `cargo run --bin compute_static_commitments --release`", + ); + } +} + /// Drift / dispatch test for the zero-init PAGE static commitments. For every /// blowup in `STATIC_BLOWUP_FACTORS`, builds a synthetic zero-init page at /// `DEFAULT_PAGE_SIZE` (page_base = 0 — the value doesn't affect the @@ -296,3 +310,43 @@ fn bitwise_non_three_coset_recomputes_and_differs_from_static() { ); } } + +/// SHA256_K counterpart of the two keccak_rc dispatch tests. Both are cheap: +/// the table is 64 rows of 2 columns. +#[test] +fn sha256_k_non_static_blowup_recomputes_via_fallback() { + assert!( + !STATIC_BLOWUP_FACTORS.contains(&NON_STATIC_BLOWUP), + "test relies on NON_STATIC_BLOWUP not being in STATIC_BLOWUP_FACTORS", + ); + let options = options_for(NON_STATIC_BLOWUP); + let from_wrapper = sha256_k::preprocessed_commitment(&options); + let recomputed = sha256_k::compute_preprocessed_commitment(&options); + assert_eq!( + from_wrapper, recomputed, + "sha256_k fallback returned a value that doesn't match direct compute at blowup={NON_STATIC_BLOWUP}", + ); +} + +#[test] +fn sha256_k_non_three_coset_recomputes_and_differs_from_static() { + for &blowup in STATIC_BLOWUP_FACTORS { + let opts_coset3 = options_with_coset(blowup, STANDARD_COSET); + let opts_coset7 = options_with_coset(blowup, NON_STANDARD_COSET); + + let from_wrapper_7 = sha256_k::preprocessed_commitment(&opts_coset7); + let recomputed_7 = sha256_k::compute_preprocessed_commitment(&opts_coset7); + let from_wrapper_3 = sha256_k::preprocessed_commitment(&opts_coset3); + + assert_eq!( + from_wrapper_7, recomputed_7, + "sha256_k wrapper at coset {NON_STANDARD_COSET} must take the recompute path \ + (blowup={blowup})", + ); + assert_ne!( + from_wrapper_7, from_wrapper_3, + "sha256_k commitment at coset {NON_STANDARD_COSET} must differ from coset \ + {STANDARD_COSET} static value (blowup={blowup})", + ); + } +} From 9666f1b71a0443bd2fdac3fbc0164493c85bb1ae Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 10:39:34 -0300 Subject: [PATCH 5/8] Generate the SHA-256 traces in parallel --- prover/src/tables/trace_builder.rs | 35 +++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 98ef9f12d..1a572a984 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -3553,6 +3553,14 @@ fn build_traces( let gen_ecdas = || ecdas::generate_ecdas_trace(&ecdas_ops); // HINT table (all-padding for programs that make no hint ecalls). let gen_hint = || hint::generate_hint_trace(&hint_ops); + // SHA-256 accelerator traces (all-padding for programs that make no SHA + // ecalls). ROTXOR alone is 224 rows of 197 columns per compression call, so + // these belong in the parallel section with the other heavy tables. + let gen_sha256 = || sha256::generate(&sha256_ops); + let gen_sha256_round = || sha256_round::generate(&sha256_ops); + let gen_sha256_schedule = || sha256_schedule::generate(&sha256_ops); + let gen_sha256_rotxor = || sha256_rotxor::generate(&sha256::rot_ops(&sha256_ops)); + let gen_sha256_k = || sha256_k::generate(sha256_ops.len()); let (mut cpus_slot, mut memws_slot, mut memw_aligneds_slot, mut memw_registers_slot) = (None, None, None, None); @@ -3566,6 +3574,8 @@ fn build_traces( (None, None, None, None); let (mut ecsm_slot, mut ecdas_slot) = (None, None); let mut hint_slot = None; + let (mut sha256_slot, mut sha256_round_slot, mut sha256_schedule_slot) = (None, None, None); + let (mut sha256_rotxor_slot, mut sha256_k_slot) = (None, None); #[cfg(feature = "disk-spill")] let sequential = storage_mode == StorageMode::Disk || cfg!(not(feature = "parallel")); @@ -3608,6 +3618,11 @@ fn build_traces( spawn_into!(ecsm_slot, gen_ecsm); spawn_into!(ecdas_slot, gen_ecdas); spawn_into!(hint_slot, gen_hint); + spawn_into!(sha256_rotxor_slot, gen_sha256_rotxor); + spawn_into!(sha256_round_slot, gen_sha256_round); + spawn_into!(sha256_schedule_slot, gen_sha256_schedule); + spawn_into!(sha256_slot, gen_sha256); + spawn_into!(sha256_k_slot, gen_sha256_k); }); } else { cpus_slot = Some(gen_cpus()); @@ -3636,6 +3651,11 @@ fn build_traces( ecsm_slot = Some(gen_ecsm()); ecdas_slot = Some(gen_ecdas()); hint_slot = Some(gen_hint()); + sha256_slot = Some(gen_sha256()); + sha256_round_slot = Some(gen_sha256_round()); + sha256_schedule_slot = Some(gen_sha256_schedule()); + sha256_rotxor_slot = Some(gen_sha256_rotxor()); + sha256_k_slot = Some(gen_sha256_k()); } const PHASE5_RAN: &str = "phase 5 generation ran in one of the branches above"; @@ -3671,6 +3691,11 @@ fn build_traces( let ecsm_trace = ecsm_slot.expect(PHASE5_RAN); let ecdas_trace = ecdas_slot.expect(PHASE5_RAN); let hint_trace = hint_slot.expect(PHASE5_RAN); + let sha256_trace = sha256_slot.expect(PHASE5_RAN); + let sha256_round_trace = sha256_round_slot.expect(PHASE5_RAN); + let sha256_schedule_trace = sha256_schedule_slot.expect(PHASE5_RAN); + let sha256_rotxor_trace = sha256_rotxor_slot.expect(PHASE5_RAN); + let sha256_k_trace = sha256_k_slot.expect(PHASE5_RAN); // Fixed-size and per-page tables aren't built through `chunk_and_generate`, // so spill them here before returning. @@ -3734,11 +3759,11 @@ fn build_traces( halt: halt_trace, commit: commit_trace, keccak: keccak_trace, - sha256: sha256::generate(&sha256_ops), - sha256_round: sha256_round::generate(&sha256_ops), - sha256_schedule: sha256_schedule::generate(&sha256_ops), - sha256_rotxor: sha256_rotxor::generate(&sha256::rot_ops(&sha256_ops)), - sha256_k: sha256_k::generate(sha256_ops.len()), + sha256: sha256_trace, + sha256_round: sha256_round_trace, + sha256_schedule: sha256_schedule_trace, + sha256_rotxor: sha256_rotxor_trace, + sha256_k: sha256_k_trace, keccak_rnd: keccak_rnd_trace, keccak_rc: keccak_rc_trace, ecsm: ecsm_trace, From f07c5536fe32a59874a70527d24016b9477e585a Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 10:39:34 -0300 Subject: [PATCH 6/8] Fill the SHA-256 trace tables in place --- prover/src/tables/sha256.rs | 75 +++++++++++++--------------- prover/src/tables/sha256_common.rs | 49 +++++++++++++++--- prover/src/tables/sha256_k.rs | 15 +++--- prover/src/tables/sha256_rotxor.rs | 45 ++++++++--------- prover/src/tables/sha256_round.rs | 43 ++++++++-------- prover/src/tables/sha256_schedule.rs | 36 ++++++------- 6 files changed, 148 insertions(+), 115 deletions(-) diff --git a/prover/src/tables/sha256.rs b/prover/src/tables/sha256.rs index 7ec0e0363..e63fd5509 100644 --- a/prover/src/tables/sha256.rs +++ b/prover/src/tables/sha256.rs @@ -44,46 +44,43 @@ impl Operation { } } pub fn generate(ops: &[Operation]) -> TraceTable { - trace( - ops.iter() - .map(|op| { - let mut r = vec![0; WIDTH]; - r[0] = op.timestamp & 0xffffffff; - r[1] = op.timestamp >> 32; - r[2] = (op.timestamp + 1) & 0xffffffff; - r[3] = (op.timestamp + 1) >> 32; - for (i, p) in op.pointers().iter().enumerate() { - for j in 0..4 { - r[PTR + 4 * i + j] = (p >> (16 * j)) & 65535; - } - } - for i in 0..32 { - r[H + i] = op.state[i] as u64; - } - for i in 0..64 { - r[M + i] = op.message[i] as u64; - } - let mut out = op.state; - executor::sha256::compress(&mut out, &op.message); - for i in 0..32 { - r[OUT + i] = out[i] as u64; - } - let w = executor::sha256::schedule(&op.message); - let init = op.state_words(); - let mut s = init; - for (&word, &constant) in w.iter().zip(&executor::sha256::K) { - s = executor::sha256::round(s, word, constant); - } - for i in 0..8 { - r[LAST + i] = s[i] as u64; - r[CARRY + i] = (s[i] as u64 + init[i] as u64) >> 32; + let mut rows = TraceRows::new(ops.len(), WIDTH); + for op in ops { + rows.push(|r| { + r[0] = op.timestamp & 0xffffffff; + r[1] = op.timestamp >> 32; + r[2] = (op.timestamp + 1) & 0xffffffff; + r[3] = (op.timestamp + 1) >> 32; + for (i, p) in op.pointers().iter().enumerate() { + for j in 0..4 { + r[PTR + 4 * i + j] = (p >> (16 * j)) & 65535; } - r[MU] = 1; - r - }) - .collect(), - WIDTH, - ) + } + for i in 0..32 { + r[H + i] = op.state[i] as u64; + } + for i in 0..64 { + r[M + i] = op.message[i] as u64; + } + let mut out = op.state; + executor::sha256::compress(&mut out, &op.message); + for i in 0..32 { + r[OUT + i] = out[i] as u64; + } + let w = executor::sha256::schedule(&op.message); + let init = op.state_words(); + let mut s = init; + for (&word, &constant) in w.iter().zip(&executor::sha256::K) { + s = executor::sha256::round(s, word, constant); + } + for i in 0..8 { + r[LAST + i] = s[i] as u64; + r[CARRY + i] = (s[i] as u64 + init[i] as u64) >> 32; + } + r[MU] = 1; + }); + } + rows.finish() } fn mem( ptr: usize, diff --git a/prover/src/tables/sha256_common.rs b/prover/src/tables/sha256_common.rs index 417afb383..621a99df5 100644 --- a/prover/src/tables/sha256_common.rs +++ b/prover/src/tables/sha256_common.rs @@ -38,15 +38,50 @@ pub fn send(bus: BusId, mu: usize, v: Vec) -> BusInteraction { pub fn recv(bus: BusId, mu: usize, v: Vec) -> BusInteraction { BusInteraction::receiver(bus, Multiplicity::Column(mu), v) } -pub fn trace(rows: Vec>, width: usize) -> TraceTable { - let n = rows.len().next_power_of_two().max(4); - let mut t = TraceTable::new_main(super::types::zeroed_fe_vec(n * width), width, 1); - for (r, row) in rows.iter().enumerate() { - for (c, x) in row.iter().enumerate() { - t.main_table.set_u64(r, c, *x); +/// Row-at-a-time writer over a table allocated once at its padded size. +/// +/// The chips here are wide (ROTXOR is 197 columns) and produce hundreds of rows +/// per compression call, so collecting the rows into a `Vec>` first +/// would allocate once per row and keep a second copy of the whole table alive +/// while this one is filled. +pub struct TraceRows { + table: TraceTable, + scratch: Vec, + row: usize, + expected: usize, +} + +impl TraceRows { + pub fn new(num_rows: usize, width: usize) -> Self { + let n = num_rows.next_power_of_two().max(4); + Self { + table: TraceTable::new_main(super::types::zeroed_fe_vec(n * width), width, 1), + scratch: vec![0; width], + row: 0, + expected: num_rows, + } + } + + pub fn push(&mut self, fill: impl FnOnce(&mut [u64])) { + self.scratch.fill(0); + fill(&mut self.scratch); + for (c, x) in self.scratch.iter().enumerate() { + self.table.main_table.set_u64(self.row, c, *x); } + self.row += 1; + } + + /// The row count passed to [`TraceRows::new`] sizes the table, so a caller + /// that pushes a different number of rows would silently leave real rows + /// zeroed (or run past the end). + pub fn finish(self) -> TraceTable { + debug_assert_eq!( + self.row, self.expected, + "pushed {} rows into a table sized for {}", + self.row, self.expected + ); + self.table } - t } pub fn put_bits(row: &mut [u64], c: usize, x: u64, n: usize) { for i in 0..n { diff --git a/prover/src/tables/sha256_k.rs b/prover/src/tables/sha256_k.rs index d9b90d595..ab2c023d7 100644 --- a/prover/src/tables/sha256_k.rs +++ b/prover/src/tables/sha256_k.rs @@ -17,12 +17,15 @@ pub const NUM_ROWS: usize = 64; /// is the prover's multiplicity. pub const NUM_PRECOMPUTED_COLS: usize = 2; pub fn generate(n: usize) -> TraceTable { - trace( - (0..NUM_ROWS) - .map(|i| vec![i as u64, executor::sha256::K[i] as u64, n as u64]) - .collect(), - WIDTH, - ) + let mut rows = TraceRows::new(NUM_ROWS, WIDTH); + for i in 0..NUM_ROWS { + rows.push(|r| { + r[0] = i as u64; + r[1] = executor::sha256::K[i] as u64; + r[2] = n as u64; + }); + } + rows.finish() } pub fn bus_interactions() -> Vec { vec![recv(BusId::ShaK, 2, vec![col(0), col(1)])] diff --git a/prover/src/tables/sha256_rotxor.rs b/prover/src/tables/sha256_rotxor.rs index 69586f0d4..182ddd176 100644 --- a/prover/src/tables/sha256_rotxor.rs +++ b/prover/src/tables/sha256_rotxor.rs @@ -13,30 +13,27 @@ pub const WIDTH: usize = 197; pub const MU: usize = 68; pub const PARAMS: [[u64; 4]; 4] = [[2, 11, 3, 0], [3, 2, 10, 0], [6, 9, 2, 1], [9, 14, 6, 1]]; pub fn generate(ops: &[(u32, usize)]) -> TraceTable { - trace( - ops.iter() - .map(|&(x, k)| { - let mut r = vec![0; WIDTH]; - put_bits(&mut r, 0, x as u64, 32); - put_bits(&mut r, 32, executor::sha256::sigma(x, k) as u64, 32); - r[64 + k] = 1; - r[MU] = 1; - for (j, (a, b)) in [(7, 18), (17, 19), (2, 13), (6, 11)] - .into_iter() - .enumerate() - { - put_bits( - &mut r, - 69 + 32 * j, - (x.rotate_right(a) ^ x.rotate_right(b)) as u64, - 32, - ); - } - r - }) - .collect(), - WIDTH, - ) + let mut rows = TraceRows::new(ops.len(), WIDTH); + for &(x, k) in ops { + rows.push(|r| { + put_bits(r, 0, x as u64, 32); + put_bits(r, 32, executor::sha256::sigma(x, k) as u64, 32); + r[64 + k] = 1; + r[MU] = 1; + for (j, (a, b)) in [(7, 18), (17, 19), (2, 13), (6, 11)] + .into_iter() + .enumerate() + { + put_bits( + r, + 69 + 32 * j, + (x.rotate_right(a) ^ x.rotate_right(b)) as u64, + 32, + ); + } + }); + } + rows.finish() } pub fn bus_interactions() -> Vec { let mut v = vec![bits(0, 32)]; diff --git a/prover/src/tables/sha256_round.rs b/prover/src/tables/sha256_round.rs index c9c7d4e37..1d3aab883 100644 --- a/prover/src/tables/sha256_round.rs +++ b/prover/src/tables/sha256_round.rs @@ -20,35 +20,36 @@ pub const CARRY: usize = 327; pub const MU: usize = 333; pub const WIDTH: usize = 334; pub fn generate(ops: &[super::sha256::Operation]) -> TraceTable { - let mut rows = vec![]; + let mut rows = TraceRows::new(ops.len() * 64, WIDTH); for op in ops { let w = executor::sha256::schedule(&op.message); let mut s = op.state_words(); for (i, &word) in w.iter().enumerate() { - let mut r = vec![0; WIDTH]; - r[0] = op.timestamp & 0xffffffff; - r[1] = op.timestamp >> 32; - r[2] = i as u64; - for (j, &state_word) in s.iter().enumerate() { - put_bits(&mut r, STATE + j * 32, state_word as u64, 32); - } let out = executor::sha256::round(s, word, executor::sha256::K[i]); - put_bits(&mut r, OUT, out[0] as u64, 32); - put_bits(&mut r, OUT + 32, out[4] as u64, 32); - r[S0] = executor::sha256::sigma(s[0], 2) as u64; - r[S1] = executor::sha256::sigma(s[4], 3) as u64; - r[W] = word as u64; - r[K] = executor::sha256::K[i] as u64; - let t1 = s[7] as u64 + r[S1] + ((s[4] & s[5]) ^ (!s[4] & s[6])) as u64 + r[W] + r[K]; - let t2 = r[S0] + ((s[0] & s[1]) ^ (s[0] & s[2]) ^ (s[1] & s[2])) as u64; - put_bits(&mut r, CARRY, (t1 + t2) >> 32, 3); - put_bits(&mut r, CARRY + 3, (s[3] as u64 + t1) >> 32, 3); - r[MU] = 1; - rows.push(r); + rows.push(|r| { + r[0] = op.timestamp & 0xffffffff; + r[1] = op.timestamp >> 32; + r[2] = i as u64; + for (j, &state_word) in s.iter().enumerate() { + put_bits(r, STATE + j * 32, state_word as u64, 32); + } + put_bits(r, OUT, out[0] as u64, 32); + put_bits(r, OUT + 32, out[4] as u64, 32); + r[S0] = executor::sha256::sigma(s[0], 2) as u64; + r[S1] = executor::sha256::sigma(s[4], 3) as u64; + r[W] = word as u64; + r[K] = executor::sha256::K[i] as u64; + let t1 = + s[7] as u64 + r[S1] + ((s[4] & s[5]) ^ (!s[4] & s[6])) as u64 + r[W] + r[K]; + let t2 = r[S0] + ((s[0] & s[1]) ^ (s[0] & s[2]) ^ (s[1] & s[2])) as u64; + put_bits(r, CARRY, (t1 + t2) >> 32, 3); + put_bits(r, CARRY + 3, (s[3] as u64 + t1) >> 32, 3); + r[MU] = 1; + }); s = out; } } - trace(rows, WIDTH) + rows.finish() } pub fn bus_interactions() -> Vec { let mut input = vec![col(0), col(1), col(2)]; diff --git a/prover/src/tables/sha256_schedule.rs b/prover/src/tables/sha256_schedule.rs index 7a5d1c1a1..2ba938ed9 100644 --- a/prover/src/tables/sha256_schedule.rs +++ b/prover/src/tables/sha256_schedule.rs @@ -21,29 +21,29 @@ pub fn amount(i: usize) -> u64 { .count() as u64 } pub fn generate(ops: &[super::sha256::Operation]) -> TraceTable { - let mut rows = vec![]; + let mut rows = TraceRows::new(ops.len() * 48, WIDTH); for op in ops { let w = executor::sha256::schedule(&op.message); for i in 16..64 { - let mut r = vec![0; WIDTH]; - r[0] = op.timestamp & 0xffffffff; - r[1] = op.timestamp >> 32; - r[2] = i as u64; - for (j, d) in [2, 7, 15, 16].into_iter().enumerate() { - r[3 + j] = w[i - d] as u64; - } - r[7] = executor::sha256::sigma(w[i - 15], 0) as u64; - r[8] = executor::sha256::sigma(w[i - 2], 1) as u64; - put_bits(&mut r, 9, w[i] as u64, 32); - let sum = r[6] + r[7] + r[4] + r[8]; - put_bits(&mut r, 41, sum >> 32, 2); - r[43] = amount(i); - r[44] = (i - 16) as u64; - r[MU] = 1; - rows.push(r); + rows.push(|r| { + r[0] = op.timestamp & 0xffffffff; + r[1] = op.timestamp >> 32; + r[2] = i as u64; + for (j, d) in [2, 7, 15, 16].into_iter().enumerate() { + r[3 + j] = w[i - d] as u64; + } + r[7] = executor::sha256::sigma(w[i - 15], 0) as u64; + r[8] = executor::sha256::sigma(w[i - 2], 1) as u64; + put_bits(r, 9, w[i] as u64, 32); + let sum = r[6] + r[7] + r[4] + r[8]; + put_bits(r, 41, sum >> 32, 2); + r[43] = amount(i); + r[44] = (i - 16) as u64; + r[MU] = 1; + }); } } - trace(rows, WIDTH) + rows.finish() } pub fn bus_interactions() -> Vec { let mut v = vec![]; From f511582d287638508a56f9b0ebcce4898a9e84cd Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 12:33:04 -0300 Subject: [PATCH 7/8] Route ethrex's sha256 through the precompile --- crypto/ethrex-crypto/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crypto/ethrex-crypto/src/lib.rs b/crypto/ethrex-crypto/src/lib.rs index d3724f92c..c98ac5b81 100644 --- a/crypto/ethrex-crypto/src/lib.rs +++ b/crypto/ethrex-crypto/src/lib.rs @@ -8,6 +8,8 @@ //! Accelerated today: //! - `keccak256`: a sponge over the `keccak_permute` precompile (riscv64; on //! host it falls back to software keccak for tests). +//! - `sha256`: the padding wrapper over the SHA-256 compression precompile +//! (riscv64; on host it falls back to the trait's own sha2 default). //! - `secp256k1_ecrecover`: the ECDSA recovery's 2-term linear combination is //! evaluated through the ECSM `ecsm_mul` precompile (riscv64), reconstructing //! the full point from x-only queries; on host / degenerate inputs it falls @@ -63,6 +65,18 @@ impl Crypto for LambdaVmEcsmCrypto { #[cfg(not(target_arch = "riscv64"))] return keccak_hash(input); } + + fn sha256(&self, input: &[u8]) -> [u8; 32] { + // riscv64 guest: IV, padding and the length encoding in the wrapper, + // the 64 rounds per block in the compression accelerator. + #[cfg(target_arch = "riscv64")] + return lambda_vm_syscalls::sha256::sha256(input); + // host (tests / non-guest): the ecall isn't available off-target. + // `NativeCrypto` takes every trait default, so this is the same sha2 + // call the default `sha256` would have made, with no extra dependency. + #[cfg(not(target_arch = "riscv64"))] + return ethrex_crypto::NativeCrypto.sha256(input); + } } // ── ECDSA secp256k1 recovery via the ECSM precompile ──────────────────────── From 9b719260e60fefda6c95de3f002202de55741b95 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 15:10:38 -0300 Subject: [PATCH 8/8] Spill the SHA-256 traces in disk mode --- crypto/stark/src/table.rs | 9 ++++++ prover/src/tables/trace_builder.rs | 47 ++++++++++++++++++++++------ prover/src/tests/disk_spill_tests.rs | 36 +++++++++++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/crypto/stark/src/table.rs b/crypto/stark/src/table.rs index 238c4fcfb..132704e59 100644 --- a/crypto/stark/src/table.rs +++ b/crypto/stark/src/table.rs @@ -457,6 +457,15 @@ impl Table { self.data[idx] = value; } + /// Whether the row-major data is backed by the spill mmap rather than the + /// heap. Exposed so callers can assert that a table they expected to spill + /// actually did: nothing else about a spilled table is observable from the + /// outside, since every accessor reads through the backing transparently. + #[cfg(feature = "disk-spill")] + pub fn is_spilled(&self) -> bool { + self.mmap_backing.is_some() + } + /// Spill the table's row-major data to a temp file and mmap it back. /// Frees the heap `data` Vec while preserving access through /// [`Self::get`], [`Self::get_row`], and [`Self::columns`]. diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 1a572a984..171bb5b2f 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -3556,11 +3556,38 @@ fn build_traces( // SHA-256 accelerator traces (all-padding for programs that make no SHA // ecalls). ROTXOR alone is 224 rows of 197 columns per compression call, so // these belong in the parallel section with the other heavy tables. - let gen_sha256 = || sha256::generate(&sha256_ops); - let gen_sha256_round = || sha256_round::generate(&sha256_ops); - let gen_sha256_schedule = || sha256_schedule::generate(&sha256_ops); - let gen_sha256_rotxor = || sha256_rotxor::generate(&sha256::rot_ops(&sha256_ops)); - let gen_sha256_k = || sha256_k::generate(sha256_ops.len()); + // These are fixed-size tables, so `chunk_and_generate` never sees them and + // the fixed-table spill block below would be the only thing that spilled + // them — by which point all five are resident at once. Spill each one as it + // is built instead, so disk mode peaks at the largest and not at the sum. + let spill_now = |t: TraceTable, + name: &str| + -> Result, Error> { + #[cfg(not(feature = "disk-spill"))] + let _ = name; + #[cfg(feature = "disk-spill")] + let t = { + let mut t = t; + if storage_mode == StorageMode::Disk { + t.main_table + .spill_to_disk() + .map_err(|e| Error::Prover(format!("disk-spill {name}: {e}")))?; + } + t + }; + Ok(t) + }; + let gen_sha256 = || spill_now(sha256::generate(&sha256_ops), "sha256"); + let gen_sha256_round = || spill_now(sha256_round::generate(&sha256_ops), "sha256_round"); + let gen_sha256_schedule = + || spill_now(sha256_schedule::generate(&sha256_ops), "sha256_schedule"); + let gen_sha256_rotxor = || { + spill_now( + sha256_rotxor::generate(&sha256::rot_ops(&sha256_ops)), + "sha256_rotxor", + ) + }; + let gen_sha256_k = || spill_now(sha256_k::generate(sha256_ops.len()), "sha256_k"); let (mut cpus_slot, mut memws_slot, mut memw_aligneds_slot, mut memw_registers_slot) = (None, None, None, None); @@ -3691,11 +3718,11 @@ fn build_traces( let ecsm_trace = ecsm_slot.expect(PHASE5_RAN); let ecdas_trace = ecdas_slot.expect(PHASE5_RAN); let hint_trace = hint_slot.expect(PHASE5_RAN); - let sha256_trace = sha256_slot.expect(PHASE5_RAN); - let sha256_round_trace = sha256_round_slot.expect(PHASE5_RAN); - let sha256_schedule_trace = sha256_schedule_slot.expect(PHASE5_RAN); - let sha256_rotxor_trace = sha256_rotxor_slot.expect(PHASE5_RAN); - let sha256_k_trace = sha256_k_slot.expect(PHASE5_RAN); + let sha256_trace = sha256_slot.expect(PHASE5_RAN)?; + let sha256_round_trace = sha256_round_slot.expect(PHASE5_RAN)?; + let sha256_schedule_trace = sha256_schedule_slot.expect(PHASE5_RAN)?; + let sha256_rotxor_trace = sha256_rotxor_slot.expect(PHASE5_RAN)?; + let sha256_k_trace = sha256_k_slot.expect(PHASE5_RAN)?; // Fixed-size and per-page tables aren't built through `chunk_and_generate`, // so spill them here before returning. diff --git a/prover/src/tests/disk_spill_tests.rs b/prover/src/tests/disk_spill_tests.rs index 93945bfff..68469b181 100644 --- a/prover/src/tests/disk_spill_tests.rs +++ b/prover/src/tests/disk_spill_tests.rs @@ -58,3 +58,39 @@ fn test_disk_spill_prove_verify_and_roundtrip_chunked() { "verification failed after serialization roundtrip (chunked)" ); } + +/// The five SHA-256 tables are fixed-size, so `chunk_and_generate` never sees +/// them and the fixed-table spill block covers only bitwise/decode/commit/ +/// register/halt/pages. ROTXOR is 224 rows of 197 columns per compression call, +/// which makes it the largest thing a SHA-heavy run holds — left on the heap it +/// defeats the point of selecting disk mode. Builds traces directly rather than +/// through `prove`, so it needs no `FORCE_DISK_SPILL`. +#[test] +fn sha256_traces_spill_in_disk_mode() { + use crate::tables::trace_builder::Traces; + use crate::test_utils::run_asm_elf; + use stark::storage_mode::StorageMode; + + let (elf, logs, _) = run_asm_elf("test_sha256_overlap"); + let traces = Traces::from_elf_and_logs( + &elf, + &logs, + &MaxRowsConfig::default(), + &[], + StorageMode::Disk, + ) + .expect("trace build failed"); + + for (name, table) in [ + ("sha256", &traces.sha256), + ("sha256_round", &traces.sha256_round), + ("sha256_schedule", &traces.sha256_schedule), + ("sha256_rotxor", &traces.sha256_rotxor), + ("sha256_k", &traces.sha256_k), + ] { + assert!( + table.main_table.is_spilled(), + "{name} stayed on the heap in disk mode", + ); + } +}