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/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 ──────────────────────── 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/docs/precompiles/sha256.md b/docs/precompiles/sha256.md new file mode 100644 index 000000000..f8869eb6e --- /dev/null +++ b/docs/precompiles/sha256.md @@ -0,0 +1,62 @@ +# 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 five tables are counted, not fixed: a program that makes no SHA ecall +carries none of them, the same way it carries no KECCAK or ECSM table. They add +five fields to `TableCounts`, so the statement encoding changes and its domain +tag moves to `_V5`; 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 000000000..81cf111f7 Binary files /dev/null and b/executor/tests/sha256_vectors.bin differ diff --git a/prover/src/auto_storage.rs b/prover/src/auto_storage.rs index 8cc437edd..b900d8179 100644 --- a/prover/src/auto_storage.rs +++ b/prover/src/auto_storage.rs @@ -207,6 +207,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/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/continuation.rs b/prover/src/continuation.rs index 5b16fcd29..5573e224e 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -2299,6 +2299,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 2a1772d0d..d0e771e36 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -120,6 +120,11 @@ pub struct TableCounts { pub ecdas: usize, pub hint: usize, pub commit: usize, + pub sha256: usize, + pub sha256_round: usize, + pub sha256_schedule: usize, + pub sha256_rotxor: usize, + pub sha256_k: usize, } impl TableCounts { @@ -148,6 +153,11 @@ impl TableCounts { self.cpu32, self.keccak, self.keccak_rnd, + self.sha256, + self.sha256_round, + self.sha256_schedule, + self.sha256_rotxor, + self.sha256_k, self.ecsm, self.ecdas, self.hint, @@ -212,6 +222,11 @@ impl TableCounts { ("ecdas", self.ecdas), ("hint", self.hint), ("commit", self.commit), + ("sha256", self.sha256), + ("sha256_round", self.sha256_round), + ("sha256_schedule", self.sha256_schedule), + ("sha256_rotxor", self.sha256_rotxor), + ("sha256_k", self.sha256_k), ]; for (name, count) in at_most_one { if count > 1 { @@ -589,6 +604,11 @@ pub(crate) struct VmAirs { pub commits: Vec, pub keccaks: Vec, pub keccak_rnds: Vec, + pub sha256s: Vec, + pub sha256_rounds: Vec, + pub sha256_schedules: Vec, + pub sha256_rotxors: Vec, + pub sha256_ks: Vec, pub keccak_rc: VmAir, pub ecsms: Vec, pub ecdases: Vec, @@ -627,6 +647,11 @@ impl VmAirs { self.commits.len(), self.keccaks.len(), self.keccak_rnds.len(), + self.sha256s.len(), + self.sha256_rounds.len(), + self.sha256_schedules.len(), + self.sha256_rotxors.len(), + self.sha256_ks.len(), self.ecsms.len(), self.ecdases.len(), self.hints.len(), @@ -650,6 +675,11 @@ impl VmAirs { traces.commits.len(), traces.keccaks.len(), traces.keccak_rnds.len(), + traces.sha256s.len(), + traces.sha256_rounds.len(), + traces.sha256_schedules.len(), + traces.sha256_rotxors.len(), + traces.sha256_ks.len(), traces.ecsms.len(), traces.ecdases.len(), traces.hints.len(), @@ -689,6 +719,33 @@ impl VmAirs { for (air, trace) in self.hints.iter().zip(traces.hints.iter_mut()) { pairs.push((air.as_ref(), trace, &())); } + for (air, trace) in self.sha256s.iter().zip(traces.sha256s.iter_mut()) { + pairs.push((air.as_ref(), trace, &())); + } + for (air, trace) in self + .sha256_rounds + .iter() + .zip(traces.sha256_rounds.iter_mut()) + { + pairs.push((air.as_ref(), trace, &())); + } + for (air, trace) in self + .sha256_schedules + .iter() + .zip(traces.sha256_schedules.iter_mut()) + { + pairs.push((air.as_ref(), trace, &())); + } + for (air, trace) in self + .sha256_rotxors + .iter() + .zip(traces.sha256_rotxors.iter_mut()) + { + pairs.push((air.as_ref(), trace, &())); + } + for (air, trace) in self.sha256_ks.iter().zip(traces.sha256_ks.iter_mut()) { + pairs.push((air.as_ref(), trace, &())); + } for (air, trace) in self.cpus.iter().zip(traces.cpus.iter_mut()) { pairs.push((air.as_ref(), trace, &())); @@ -776,6 +833,21 @@ impl VmAirs { for air in &self.hints { refs.push(air.as_ref()); } + for air in &self.sha256s { + refs.push(air.as_ref()); + } + for air in &self.sha256_rounds { + refs.push(air.as_ref()); + } + for air in &self.sha256_schedules { + refs.push(air.as_ref()); + } + for air in &self.sha256_rotxors { + refs.push(air.as_ref()); + } + for air in &self.sha256_ks { + refs.push(air.as_ref()); + } for air in &self.cpus { refs.push(air.as_ref()); @@ -952,6 +1024,45 @@ impl VmAirs { ) as VmAir }) .collect(); + let sha256s: Vec<_> = (0..table_counts.sha256) + .map(|i| { + Box::new( + test_utils::create_sha256_air(proof_options).with_name(&format!("SHA256[{i}]")), + ) as VmAir + }) + .collect(); + let sha256_rounds: Vec<_> = (0..table_counts.sha256_round) + .map(|i| { + Box::new( + test_utils::create_sha256_round_air(proof_options) + .with_name(&format!("SHA256ROUND[{i}]")), + ) as VmAir + }) + .collect(); + let sha256_schedules: Vec<_> = (0..table_counts.sha256_schedule) + .map(|i| { + Box::new( + test_utils::create_sha256_schedule_air(proof_options) + .with_name(&format!("SHA256MSGSCHED[{i}]")), + ) as VmAir + }) + .collect(); + let sha256_rotxors: Vec<_> = (0..table_counts.sha256_rotxor) + .map(|i| { + Box::new( + test_utils::create_sha256_rotxor_air(proof_options) + .with_name(&format!("ROTXOR[{i}]")), + ) as VmAir + }) + .collect(); + let sha256_ks: Vec<_> = (0..table_counts.sha256_k) + .map(|i| { + Box::new( + test_utils::create_sha256_k_air(proof_options) + .with_name(&format!("SHA256_K[{i}]")), + ) as VmAir + }) + .collect(); let keccak_rc: VmAir = Box::new(create_keccak_rc_air(proof_options).with_preprocessed( tables::keccak_rc::preprocessed_commitment(proof_options), tables::keccak_rc::NUM_PRECOMPUTED_COLS, @@ -1088,6 +1199,11 @@ impl VmAirs { commits, keccaks, keccak_rnds, + sha256s, + sha256_rounds, + sha256_schedules, + sha256_rotxors, + sha256_ks, keccak_rc, ecsms, ecdases, diff --git a/prover/src/statement.rs b/prover/src/statement.rs index edac11b90..a1fb32fd5 100644 --- a/prover/src/statement.rs +++ b/prover/src/statement.rs @@ -17,7 +17,7 @@ use crate::test_utils::E; use crate::{RuntimePageRange, TableCounts}; /// Domain-separation tag. Bump the suffix (`_V2`, ...) on any encoding change. -const DOMAIN_TAG: &[u8] = b"LAMBDAVM_STARK_STATEMENT_V4"; +const DOMAIN_TAG: &[u8] = b"LAMBDAVM_STARK_STATEMENT_V5"; /// Canonical full-ELF identity digest — exactly what [`absorb_statement`] binds /// into the transcript. The recursion attestation folds the same digest into @@ -118,6 +118,11 @@ pub(crate) fn absorb_statement_with_digest( ecdas, hint, commit, + sha256, + sha256_round, + sha256_schedule, + sha256_rotxor, + sha256_k, } = table_counts; for count in [ cpu, @@ -140,6 +145,11 @@ pub(crate) fn absorb_statement_with_digest( ecdas, hint, commit, + sha256, + sha256_round, + sha256_schedule, + sha256_rotxor, + sha256_k, ] { t.append_bytes(&(count as u64).to_le_bytes()); } @@ -189,7 +199,7 @@ pub(crate) fn absorb_statement_with_digest( /// Continuation domain tags. Distinct from the monolithic `DOMAIN_TAG` so a /// monolithic proof and a continuation proof can never share a transcript prefix. -const CONTINUATION_EPOCH_TAG: &[u8] = b"LAMBDAVM_CONTINUATION_EPOCH_V4"; +const CONTINUATION_EPOCH_TAG: &[u8] = b"LAMBDAVM_CONTINUATION_EPOCH_V5"; const CONTINUATION_GLOBAL_TAG: &[u8] = b"LAMBDAVM_CONTINUATION_GLOBAL_V2"; /// Statement bound into the cross-epoch **global** proof's transcript before 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..e63fd5509 --- /dev/null +++ b/prover/src/tables/sha256.rs @@ -0,0 +1,275 @@ +//! 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 { + 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; + } + } + 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, + 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; + // 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, + &[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..621a99df5 --- /dev/null +++ b/prover/src/tables/sha256_common.rs @@ -0,0 +1,104 @@ +//! 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) +} +/// 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 + } +} +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..ab2c023d7 --- /dev/null +++ b/prover/src/tables/sha256_k.rs @@ -0,0 +1,138 @@ +//! 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; +/// 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 { + 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)])] +} + +/// 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..NUM_ROWS).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) + .expect("FFT interpolation failed for sha256_k column") + }) + .collect(); + let lde: Vec<_> = polys + .iter() + .map(|p| { + evaluate_polynomial_on_lde_domain( + p, + options.blowup_factor as usize, + NUM_ROWS, + &FE::from(options.coset_offset), + ) + .expect("LDE evaluation failed for sha256_k polynomial") + }) + .collect(); + 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/tables/sha256_rotxor.rs b/prover/src/tables/sha256_rotxor.rs new file mode 100644 index 000000000..182ddd176 --- /dev/null +++ b/prover/src/tables/sha256_rotxor.rs @@ -0,0 +1,98 @@ +//! 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 { + 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)]; + 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..1d3aab883 --- /dev/null +++ b/prover/src/tables/sha256_round.rs @@ -0,0 +1,119 @@ +//! 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 = 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 out = executor::sha256::round(s, word, executor::sha256::K[i]); + 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; + } + } + rows.finish() +} +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..2ba938ed9 --- /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 = TraceRows::new(ops.len() * 48, WIDTH); + for op in ops { + let w = executor::sha256::schedule(&op.message); + for i in 16..64 { + 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; + }); + } + } + rows.finish() +} +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 c3b695a80..2c93de37e 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, @@ -2867,6 +2876,14 @@ pub struct Traces { /// KECCAK core table (one row per keccak permutation call). Empty when the /// run makes no keccak call. pub keccaks: Vec>, + /// SHA-256 accelerator tables. Empty when the run makes no SHA ecall: like + /// the other accelerators they are counted, not fixed, so a program that + /// never hashes carries none of them. + pub sha256s: Vec>, + pub sha256_rounds: Vec>, + pub sha256_schedules: Vec>, + pub sha256_rotxors: Vec>, + pub sha256_ks: Vec>, /// KECCAK_RND round table (24 rows per keccak call). Empty alongside KECCAK. pub keccak_rnds: Vec>, @@ -2919,6 +2936,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, @@ -3048,6 +3066,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, @@ -3188,6 +3207,7 @@ fn collect_all_ops( dvrm_ops, commit_ops, keccak_ops, + sha256_ops, eq_ops, bytewise_ops, store_ops, @@ -3232,6 +3252,7 @@ fn build_traces( dvrm_ops, commit_ops, keccak_ops, + sha256_ops, eq_ops, bytewise_ops, store_ops, @@ -3323,6 +3344,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))), @@ -3654,6 +3676,55 @@ fn build_traces( storage_mode, ) }; + // SHA-256 accelerator traces. Absent entirely for programs that make no SHA + // ecall — which matters more here than for the other accelerators: ROTXOR is + // 224 rows of 197 columns per compression call, so it is the heaviest table + // the accelerator adds and the one a non-hashing run most wants to skip. + // `generate_optional` also spills in disk mode, so these need no separate + // spill of their own. + let gen_sha256s = || { + generate_optional( + &sha256_ops, + sha256::generate, + #[cfg(feature = "disk-spill")] + storage_mode, + ) + }; + let gen_sha256_rounds = || { + generate_optional( + &sha256_ops, + sha256_round::generate, + #[cfg(feature = "disk-spill")] + storage_mode, + ) + }; + let gen_sha256_schedules = || { + generate_optional( + &sha256_ops, + sha256_schedule::generate, + #[cfg(feature = "disk-spill")] + storage_mode, + ) + }; + let gen_sha256_rotxors = || { + let rot_ops = sha256::rot_ops(&sha256_ops); + generate_optional( + &rot_ops, + sha256_rotxor::generate, + #[cfg(feature = "disk-spill")] + storage_mode, + ) + }; + // The constant table's multiplicity column counts the calls, so it is keyed + // off the op count rather than the ops themselves. + let gen_sha256_ks = || { + generate_optional( + &sha256_ops, + |ops: &[sha256::Operation]| sha256_k::generate(ops.len()), + #[cfg(feature = "disk-spill")] + storage_mode, + ) + }; let (mut cpus_slot, mut memws_slot, mut memw_aligneds_slot, mut memw_registers_slot) = (None, None, None, None); @@ -3667,6 +3738,8 @@ fn build_traces( (None, None, None, None); let (mut ecsms_slot, mut ecdases_slot) = (None, None); let mut hints_slot = None; + let (mut sha256s_slot, mut sha256_rounds_slot, mut sha256_schedules_slot) = (None, None, None); + let (mut sha256_rotxors_slot, mut sha256_ks_slot) = (None, None); #[cfg(feature = "disk-spill")] let sequential = storage_mode == StorageMode::Disk || cfg!(not(feature = "parallel")); @@ -3709,6 +3782,11 @@ fn build_traces( spawn_into!(ecsms_slot, gen_ecsms); spawn_into!(ecdases_slot, gen_ecdases); spawn_into!(hints_slot, gen_hints); + spawn_into!(sha256_rotxors_slot, gen_sha256_rotxors); + spawn_into!(sha256_rounds_slot, gen_sha256_rounds); + spawn_into!(sha256_schedules_slot, gen_sha256_schedules); + spawn_into!(sha256s_slot, gen_sha256s); + spawn_into!(sha256_ks_slot, gen_sha256_ks); }); } else { cpus_slot = Some(gen_cpus()); @@ -3737,6 +3815,11 @@ fn build_traces( ecsms_slot = Some(gen_ecsms()); ecdases_slot = Some(gen_ecdases()); hints_slot = Some(gen_hints()); + sha256s_slot = Some(gen_sha256s()); + sha256_rounds_slot = Some(gen_sha256_rounds()); + sha256_schedules_slot = Some(gen_sha256_schedules()); + sha256_rotxors_slot = Some(gen_sha256_rotxors()); + sha256_ks_slot = Some(gen_sha256_ks()); } const PHASE5_RAN: &str = "phase 5 generation ran in one of the branches above"; @@ -3771,6 +3854,11 @@ fn build_traces( let ecsms = ecsms_slot.expect(PHASE5_RAN)?; let ecdases = ecdases_slot.expect(PHASE5_RAN)?; let hints = hints_slot.expect(PHASE5_RAN)?; + let sha256s = sha256s_slot.expect(PHASE5_RAN)?; + let sha256_rounds = sha256_rounds_slot.expect(PHASE5_RAN)?; + let sha256_schedules = sha256_schedules_slot.expect(PHASE5_RAN)?; + let sha256_rotxors = sha256_rotxors_slot.expect(PHASE5_RAN)?; + let sha256_ks = sha256_ks_slot.expect(PHASE5_RAN)?; // Fixed-size and per-page tables aren't built through `chunk_and_generate`, // so spill them here before returning. @@ -3831,6 +3919,11 @@ fn build_traces( commits, keccaks, keccak_rnds, + sha256s, + sha256_rounds, + sha256_schedules, + sha256_rotxors, + sha256_ks, keccak_rc: keccak_rc_trace, ecsms, ecdases, @@ -3872,6 +3965,7 @@ fn padded_chunked_rows_optional(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, @@ -3922,6 +4016,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, @@ -3989,6 +4084,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 @@ -4077,6 +4185,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_optional(memw_count, max_rows.memw), memw_aligned_padded_rows: padded_chunked_rows_optional( @@ -4241,6 +4350,11 @@ impl Traces { commits, keccaks, keccak_rnds, + sha256s, + sha256_rounds, + sha256_schedules, + sha256_rotxors, + sha256_ks, keccak_rc, ecsms, ecdases, @@ -4303,6 +4417,21 @@ impl Traces { for t in keccak_rnds { total += (t.num_rows() * KECCAK_RND_COLS) as u64; } + for t in sha256s { + total += (t.num_rows() * sha256::WIDTH) as u64; + } + for t in sha256_rounds { + total += (t.num_rows() * sha256_round::WIDTH) as u64; + } + for t in sha256_schedules { + total += (t.num_rows() * sha256_schedule::WIDTH) as u64; + } + for t in sha256_rotxors { + total += (t.num_rows() * sha256_rotxor::WIDTH) as u64; + } + for t in sha256_ks { + total += (t.num_rows() * (sha256_k::WIDTH - sha256_k::NUM_PRECOMPUTED_COLS)) as u64; + } total += (keccak_rc.num_rows() * (KECCAK_RC_COLS - KECCAK_RC_PRECOMPUTED)) as u64; for t in eqs { total += (t.num_rows() * EQ_COLS) as u64; @@ -4386,6 +4515,11 @@ impl Traces { commits, keccaks, keccak_rnds, + sha256s, + sha256_rounds, + sha256_schedules, + sha256_rotxors, + sha256_ks, keccak_rc, ecsms, ecdases, @@ -4448,6 +4582,21 @@ impl Traces { for t in keccak_rnds { total += (t.num_rows() * n_keccak_rnd) as u64; } + for t in sha256s { + total += (t.num_rows() * aux_cols(sha256::bus_interactions().len())) as u64; + } + for t in sha256_rounds { + total += (t.num_rows() * aux_cols(sha256_round::bus_interactions().len())) as u64; + } + for t in sha256_schedules { + total += (t.num_rows() * aux_cols(sha256_schedule::bus_interactions().len())) as u64; + } + for t in sha256_rotxors { + total += (t.num_rows() * aux_cols(sha256_rotxor::bus_interactions().len())) as u64; + } + for t in sha256_ks { + total += (t.num_rows() * aux_cols(sha256_k::bus_interactions().len())) as u64; + } total += (keccak_rc.num_rows() * n_keccak_rc) as u64; for t in eqs { total += (t.num_rows() * n_eq) as u64; @@ -4496,6 +4645,11 @@ impl Traces { ecdas: self.ecdases.len(), hint: self.hints.len(), commit: self.commits.len(), + sha256: self.sha256s.len(), + sha256_round: self.sha256_rounds.len(), + sha256_schedule: self.sha256_schedules.len(), + sha256_rotxor: self.sha256_rotxors.len(), + sha256_k: self.sha256_ks.len(), } } @@ -4826,6 +4980,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + sha256_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4845,6 +5000,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + sha256_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4939,6 +5095,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + sha256_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4954,6 +5111,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + sha256_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4983,3 +5141,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 5b4206356..5736f644e 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -1022,3 +1022,75 @@ 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), + chip::NUM_PRECOMPUTED_COLS, + ) +} diff --git a/prover/src/tests/auto_storage_tests.rs b/prover/src/tests/auto_storage_tests.rs index e26674d27..cea4f1223 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 b2ebddfff..d4d3e778f 100644 --- a/prover/src/tests/count_table_lengths_drift_tests.rs +++ b/prover/src/tests/count_table_lengths_drift_tests.rs @@ -127,3 +127,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/disk_spill_tests.rs b/prover/src/tests/disk_spill_tests.rs index 93945bfff..6654cd74b 100644 --- a/prover/src/tests/disk_spill_tests.rs +++ b/prover/src/tests/disk_spill_tests.rs @@ -58,3 +58,41 @@ 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.sha256s), + ("sha256_round", &traces.sha256_rounds), + ("sha256_schedule", &traces.sha256_schedules), + ("sha256_rotxor", &traces.sha256_rotxors), + ("sha256_k", &traces.sha256_ks), + ] + .map(|(name, v)| (name, v.first().expect("the program makes SHA calls"))) + { + assert!( + table.main_table.is_spilled(), + "{name} stayed on the heap in disk mode", + ); + } +} diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 73ff6ee45..6e44e271c 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -101,3 +101,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 2707e67af..ef0770d70 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -2845,6 +2845,11 @@ fn test_verify_rejects_zero_table_counts() { ecdas: 0, hint: 0, commit: 0, + sha256: 0, + sha256_round: 0, + sha256_schedule: 0, + sha256_rotxor: 0, + sha256_k: 0, }, ..vm_proof }; @@ -2936,6 +2941,11 @@ fn test_crafted_zero_count_proof_must_not_verify() { ecdas: 0, hint: 0, commit: 0, + sha256: 0, + sha256_round: 0, + sha256_schedule: 0, + sha256_rotxor: 0, + sha256_k: 0, }; let airs = VmAirs::new( &elf, @@ -3973,3 +3983,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.sha256s[0], crate::tables::sha256::OUT), + 1 => (&mut traces.sha256s[0], crate::tables::sha256::PTR), + 2 => (&mut traces.sha256_rounds[0], crate::tables::sha256_round::K), + 3 => (&mut traces.sha256_schedules[0], 3), + _ => (&mut traces.sha256_rotxors[0], 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/prover/src/tests/skip_empty_tables_tests.rs b/prover/src/tests/skip_empty_tables_tests.rs index 8fef3d190..d7789452b 100644 --- a/prover/src/tests/skip_empty_tables_tests.rs +++ b/prover/src/tests/skip_empty_tables_tests.rs @@ -100,6 +100,11 @@ fn every_table_participates_in_the_bus() { ecdas: 1, hint: 1, commit: 1, + sha256: 1, + sha256_round: 1, + sha256_schedule: 1, + sha256_rotxor: 1, + sha256_k: 1, }; let airs = VmAirs::new( &elf, @@ -158,6 +163,11 @@ fn droppable_air_names(counts: &TableCounts) -> Vec<&'static str> { ecdas, hint, commit, + sha256, + sha256_round, + sha256_schedule, + sha256_rotxor, + sha256_k, } = counts; [ ("CPU", cpu), @@ -180,6 +190,11 @@ fn droppable_air_names(counts: &TableCounts) -> Vec<&'static str> { ("ECDAS", ecdas), ("HINT", hint), ("COMMIT", commit), + ("SHA256", sha256), + ("SHA256ROUND", sha256_round), + ("SHA256MSGSCHED", sha256_schedule), + ("ROTXOR", sha256_rotxor), + ("SHA256_K", sha256_k), ] .into_iter() .map(|(name, _count)| name) @@ -237,6 +252,9 @@ fn no_present_table_contributes_zero_to_the_bus() { "test_keccak", "test_ecsm", "test_commit_4", + // Reaches the five SHA-256 tables, which are droppable like the other + // accelerators and so have to be weighed here too. + "test_sha256", ]; let mut droppable: Vec<&'static str> = Vec::new(); diff --git a/prover/src/tests/statement_tests.rs b/prover/src/tests/statement_tests.rs index 065d66ee7..53842a86d 100644 --- a/prover/src/tests/statement_tests.rs +++ b/prover/src/tests/statement_tests.rs @@ -29,6 +29,11 @@ fn sample_counts() -> TableCounts { ecdas: 1, hint: 1, commit: 1, + sha256: 1, + sha256_round: 1, + sha256_schedule: 1, + sha256_rotxor: 1, + sha256_k: 1, } } @@ -98,6 +103,11 @@ fn each_count_mut(counts: &mut TableCounts) -> Vec<(&'static str, &mut usize)> { ecdas, hint, commit, + sha256, + sha256_round, + sha256_schedule, + sha256_rotxor, + sha256_k, } = counts; vec![ ("cpu", cpu), @@ -120,6 +130,11 @@ fn each_count_mut(counts: &mut TableCounts) -> Vec<(&'static str, &mut usize)> { ("ecdas", ecdas), ("hint", hint), ("commit", commit), + ("sha256", sha256), + ("sha256_round", sha256_round), + ("sha256_schedule", sha256_schedule), + ("sha256_rotxor", sha256_rotxor), + ("sha256_k", sha256_k), ] } @@ -136,7 +151,7 @@ fn state_depends_on_every_table_count() { .into_iter() .map(|(name, _)| name) .collect(); - assert_eq!(names.len(), 20, "every count must be probed"); + assert_eq!(names.len(), 25, "every count must be probed"); for name in names { let mut counts = sample_counts(); diff --git a/prover/src/tests/static_commitments_tests.rs b/prover/src/tests/static_commitments_tests.rs index 7b3d38e12..00d7d9147 100644 --- a/prover/src/tests/static_commitments_tests.rs +++ b/prover/src/tests/static_commitments_tests.rs @@ -1,11 +1,11 @@ //! Drift-detection and lookup-dispatch tests for the static preprocessed-table -//! commitments shipped in `bitwise`, `keccak_rc`, and `page` (the shared +//! commitments shipped in `bitwise`, `keccak_rc`, `sha256_k`, and `page` (the shared //! zero-init page commitment). //! //! - The drift tests recompute the commitment for every blowup in //! `STATIC_BLOWUP_FACTORS` (the list shared with the generator binary) and //! compare against the value the table-module's wrapper returns -//! (`preprocessed_commitment` for `bitwise`/`keccak_rc`, +//! (`preprocessed_commitment` for `bitwise`/`keccak_rc`/`sha256_k`, //! `zero_init_preprocessed_commitment` for `page`). This catches AIR or //! FFT-pipeline drift; the page test additionally pins the static bytes //! against the recompute directly. @@ -22,7 +22,7 @@ use stark::proof::options::GoldilocksCubicProofOptions; -use crate::tables::{STATIC_BLOWUP_FACTORS, bitwise, keccak_rc, page}; +use crate::tables::{STATIC_BLOWUP_FACTORS, bitwise, keccak_rc, page, sha256_k}; fn options_for(blowup: u8) -> 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})", + ); + } +} 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 +}