diff --git a/bench_vs/lambda/recursion/Cargo.lock b/bench_vs/lambda/recursion/Cargo.lock index 3e7f8e9a5..061f211c1 100644 --- a/bench_vs/lambda/recursion/Cargo.lock +++ b/bench_vs/lambda/recursion/Cargo.lock @@ -129,8 +129,6 @@ dependencies = [ "digest", "lambda-vm-syscalls", "math", - "rand 0.8.6", - "rand_chacha 0.3.1", "rkyv", "serde", "sha3", @@ -399,7 +397,7 @@ dependencies = [ "getrandom 0.2.17", "getrandom 0.3.4", "lazy_static", - "rand 0.9.4", + "rand", "riscv", "thiserror", ] @@ -435,7 +433,6 @@ dependencies = [ "getrandom 0.2.17", "num-bigint", "num-traits", - "rand 0.8.6", "rayon", "rkyv", "serde", @@ -585,35 +582,16 @@ dependencies = [ "ptr_meta", ] -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 0336ff821..aa47b70e0 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -1132,6 +1132,7 @@ mod tests { (SyscallNumbers::KeccakPermute, Some(Accelerator::Keccak)), (SyscallNumbers::Ecsm, Some(Accelerator::Ecsm)), (SyscallNumbers::DmaMemcpy, Some(Accelerator::Dma)), + (SyscallNumbers::DmaMemset, Some(Accelerator::Dma)), (SyscallNumbers::Print, None), (SyscallNumbers::Panic, None), (SyscallNumbers::Commit, None), diff --git a/executor/programs/rust/dma_memmove_cases/.cargo/config.toml b/executor/programs/rust/dma_memmove_cases/.cargo/config.toml new file mode 100644 index 000000000..8ef8239bb --- /dev/null +++ b/executor/programs/rust/dma_memmove_cases/.cargo/config.toml @@ -0,0 +1,9 @@ +[target.riscv64im-lambda-vm-elf] +rustflags = [ + "--cfg", "getrandom_backend=\"custom\"", + "-C", "passes=lower-atomic" +] + +[env] +CC_riscv64im_lambda_vm_elf = "clang" +CFLAGS_riscv64im_lambda_vm_elf = "--target=riscv64 -march=rv64im -mabi=lp64 --sysroot=/opt/lambda-vm-sysroot" diff --git a/executor/programs/rust/dma_memmove_cases/Cargo.lock b/executor/programs/rust/dma_memmove_cases/Cargo.lock new file mode 100644 index 000000000..04c10ccfe --- /dev/null +++ b/executor/programs/rust/dma_memmove_cases/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 = "dma_memmove_cases" +version = "0.1.0" +dependencies = [ + "lambda-vm-syscalls", +] + +[[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 = "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.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/executor/programs/rust/dma_memmove_cases/Cargo.toml b/executor/programs/rust/dma_memmove_cases/Cargo.toml new file mode 100644 index 000000000..b81ea25a9 --- /dev/null +++ b/executor/programs/rust/dma_memmove_cases/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] + +[package] +name = "dma_memmove_cases" +version = "0.1.0" +edition = "2024" + +[dependencies] +lambda-vm-syscalls = { path = "../../../../syscalls" } diff --git a/executor/programs/rust/dma_memmove_cases/src/main.rs b/executor/programs/rust/dma_memmove_cases/src/main.rs new file mode 100644 index 000000000..45ecdb0de --- /dev/null +++ b/executor/programs/rust/dma_memmove_cases/src/main.rs @@ -0,0 +1,70 @@ +use lambda_vm_syscalls as syscalls; + +unsafe extern "C" { + fn memmove(dst: *mut u8, src: *const u8, count: usize) -> *mut u8; +} + +#[inline(never)] +fn dma_move(dst: *mut u8, src: *const u8, count: usize) -> *mut u8 { + let count = core::hint::black_box(count); + unsafe { memmove(dst, src, count) } +} + +fn fill_pattern(bytes: &mut [u8], seed: u8) { + for (i, byte) in bytes.iter_mut().enumerate() { + *byte = (i as u8).wrapping_mul(37).wrapping_add(seed); + } +} + +pub fn main() { + // Disjoint regions behave like memcpy. + let mut source = [0u8; 777]; + let mut destination = [0xA5u8; 777]; + fill_pattern(&mut source, 11); + for count in [0usize, 1, 7, 8, 255, 256, 257, 777] { + destination.fill(0xA5); + let returned = dma_move(destination.as_mut_ptr(), source.as_ptr(), count); + assert_eq!(returned, destination.as_mut_ptr()); + assert_eq!(&destination[..count], &source[..count]); + assert!(destination[count..].iter().all(|&b| b == 0xA5)); + } + + // Forward overlap (dst inside [src, src+n)) is the case that needs BACKWARD + // chunking; a forward-chunked copy corrupts it once n exceeds one chunk. + // Offsets below and above 256 exercise both sides of the chunk boundary. + for (offset, count) in [(1usize, 600usize), (17, 600), (255, 600), (256, 600), (300, 700), (4, 8)] { + let mut buffer = [0u8; 1600]; + fill_pattern(&mut buffer, 23); + let before = buffer; + dma_move( + unsafe { buffer.as_mut_ptr().add(offset) }, + buffer.as_ptr(), + count, + ); + assert_eq!(&buffer[offset..offset + count], &before[..count]); + // Bytes below the destination must be untouched. + assert_eq!(&buffer[..offset], &before[..offset]); + } + + // Backward overlap (dst below src) stays forward-chunked. + for (offset, count) in [(1usize, 600usize), (17, 600), (300, 700)] { + let mut buffer = [0u8; 1600]; + fill_pattern(&mut buffer, 41); + let before = buffer; + dma_move( + buffer.as_mut_ptr(), + unsafe { buffer.as_ptr().add(offset) }, + count, + ); + assert_eq!(&buffer[..count], &before[offset..offset + count]); + } + + // Exact aliasing must be a no-op. + let mut same = [0u8; 300]; + fill_pattern(&mut same, 7); + let before = same; + dma_move(same.as_mut_ptr(), same.as_ptr(), 300); + assert_eq!(same, before); + + syscalls::syscalls::commit(b"dma-memmove-ok"); +} diff --git a/executor/programs/rust/dma_memset_cases/.cargo/config.toml b/executor/programs/rust/dma_memset_cases/.cargo/config.toml new file mode 100644 index 000000000..8ef8239bb --- /dev/null +++ b/executor/programs/rust/dma_memset_cases/.cargo/config.toml @@ -0,0 +1,9 @@ +[target.riscv64im-lambda-vm-elf] +rustflags = [ + "--cfg", "getrandom_backend=\"custom\"", + "-C", "passes=lower-atomic" +] + +[env] +CC_riscv64im_lambda_vm_elf = "clang" +CFLAGS_riscv64im_lambda_vm_elf = "--target=riscv64 -march=rv64im -mabi=lp64 --sysroot=/opt/lambda-vm-sysroot" diff --git a/executor/programs/rust/dma_memset_cases/Cargo.lock b/executor/programs/rust/dma_memset_cases/Cargo.lock new file mode 100644 index 000000000..22c1e11fe --- /dev/null +++ b/executor/programs/rust/dma_memset_cases/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 = "dma_memset_cases" +version = "0.1.0" +dependencies = [ + "lambda-vm-syscalls", +] + +[[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 = "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.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/executor/programs/rust/dma_memset_cases/Cargo.toml b/executor/programs/rust/dma_memset_cases/Cargo.toml new file mode 100644 index 000000000..de5dc5ede --- /dev/null +++ b/executor/programs/rust/dma_memset_cases/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] + +[package] +name = "dma_memset_cases" +version = "0.1.0" +edition = "2024" + +[dependencies] +lambda-vm-syscalls = { path = "../../../../syscalls" } diff --git a/executor/programs/rust/dma_memset_cases/src/main.rs b/executor/programs/rust/dma_memset_cases/src/main.rs new file mode 100644 index 000000000..5caf0e285 --- /dev/null +++ b/executor/programs/rust/dma_memset_cases/src/main.rs @@ -0,0 +1,49 @@ +use lambda_vm_syscalls as syscalls; + +unsafe extern "C" { + fn memset(dst: *mut u8, fill: i32, count: usize) -> *mut u8; +} + +/// `black_box` on the count keeps LLVM from turning these into inline stores, +/// so every call really does reach the strong `memset` symbol and the DMA ecall. +#[inline(never)] +fn dma_set(dst: *mut u8, fill: i32, count: usize) -> *mut u8 { + let count = core::hint::black_box(count); + unsafe { memset(dst, fill, count) } +} + +pub fn main() { + let mut buffer = [0u8; 777]; + + // Every row-schedule boundary: empty, sub-tail, exact widths, the 256-byte + // per-ecall cap, and one length that forces several chunked ecalls. + for count in [0usize, 1, 7, 8, 9, 31, 32, 33, 127, 128, 255, 256] { + buffer.fill(0xA5); + let returned = dma_set(buffer.as_mut_ptr(), 0x3C, count); + assert_eq!(returned, buffer.as_mut_ptr()); + assert!(buffer[..count].iter().all(|&byte| byte == 0x3C)); + assert!(buffer[count..].iter().all(|&byte| byte == 0xA5)); + } + + // More than one chunk: 777 bytes becomes four bounded DMA ecalls. + buffer.fill(0); + dma_set(buffer.as_mut_ptr(), 0x5A, buffer.len()); + assert!(buffer.iter().all(|&byte| byte == 0x5A)); + + // The guest stub masks the fill to its low byte, matching C's + // `memset(void*, int, size_t)` writing `(unsigned char)c`. + buffer.fill(0); + dma_set(buffer.as_mut_ptr(), 0x1FF, 64); + assert!(buffer[..64].iter().all(|&byte| byte == 0xFF)); + + // Unaligned destination that also crosses a 4 KiB page boundary. + let mut page_buffer = [0u8; 8192]; + let to_boundary = 4096 - (page_buffer.as_ptr() as usize & 4095); + let offset = to_boundary.saturating_sub(5); + dma_set(unsafe { page_buffer.as_mut_ptr().add(offset) }, 0x77, 256); + assert!(page_buffer[offset..offset + 256] + .iter() + .all(|&byte| byte == 0x77)); + + syscalls::syscalls::commit(b"dma-memset-ok"); +} diff --git a/executor/programs/rust/keccak_transcript_pattern/Cargo.lock b/executor/programs/rust/keccak_transcript_pattern/Cargo.lock index 4e5afb1bd..0b59195aa 100644 --- a/executor/programs/rust/keccak_transcript_pattern/Cargo.lock +++ b/executor/programs/rust/keccak_transcript_pattern/Cargo.lock @@ -88,8 +88,6 @@ dependencies = [ "digest", "lambda-vm-syscalls", "math", - "rand 0.8.7", - "rand_chacha 0.3.1", "serde", "sha3", ] @@ -240,7 +238,7 @@ dependencies = [ "getrandom 0.2.17", "getrandom 0.3.4", "lazy_static", - "rand 0.9.5", + "rand", "riscv", "thiserror", ] @@ -270,7 +268,6 @@ dependencies = [ "getrandom 0.2.17", "num-bigint", "num-traits", - "rand 0.8.7", "rayon", "serde", "serde_json", @@ -361,33 +358,14 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -397,15 +375,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - [[package]] name = "rand_core" version = "0.9.5" diff --git a/executor/src/tests/dma_tests.rs b/executor/src/tests/dma_tests.rs index 7965bfbdb..637507c89 100644 --- a/executor/src/tests/dma_tests.rs +++ b/executor/src/tests/dma_tests.rs @@ -1,6 +1,7 @@ use crate::vm::instruction::decoding::Instruction; use crate::vm::instruction::execution::{ - DMA_MEMCPY_MAX_BYTES, DMA_MEMCPY_SYSCALL_NUMBER, ExecutionError, + DMA_MEMCPY_MAX_BYTES, DMA_MEMCPY_SYSCALL_NUMBER, DMA_MEMSET_MAX_FILL, + DMA_MEMSET_SYSCALL_NUMBER, ExecutionError, }; use crate::vm::memory::Memory; use crate::vm::registers::Registers; @@ -115,3 +116,81 @@ proptest! { prop_assert_eq!(actual, expected); } } + +fn run_memset(memory: &mut Memory, dst: u64, fill: u64, count: u64) -> Result<(), ExecutionError> { + let mut registers = Registers::default(); + let mut pc = 0; + registers.write(17, DMA_MEMSET_SYSCALL_NUMBER)?; + registers.write(10, dst)?; + registers.write(11, fill)?; + registers.write(12, count)?; + Instruction::EcallEbreak.run(&mut pc, &mut registers, memory)?; + Ok(()) +} + +#[test] +fn dma_memset_fills_unaligned_body_and_tail() { + let mut memory = Memory::default(); + // 27 bytes = three eight-byte rows plus a three-byte tail, at an unaligned base. + run_memset(&mut memory, 0x2005, 0x3C, 27).unwrap(); + + assert_eq!(memory.load_bytes(0x2005, 27).unwrap(), vec![0x3Cu8; 27]); + // Neighbours must be untouched. + assert_eq!(memory.load_byte(0x2004), 0); + assert_eq!(memory.load_byte(0x2005 + 27), 0); +} + +#[test] +fn dma_memset_zero_count_writes_nothing() { + let mut memory = Memory::default(); + memory.store_byte(0x3000, 0x11); + run_memset(&mut memory, 0x3000, 0xFF, 0).unwrap(); + assert_eq!(memory.load_byte(0x3000), 0x11); +} + +#[test] +fn dma_memset_rejects_wrapping_range() { + let mut memory = Memory::default(); + assert!(run_memset(&mut memory, u64::MAX - 3, 0x11, 8).is_err()); +} + +#[test] +fn dma_memset_rejects_oversized_chunk() { + let mut memory = Memory::default(); + assert!(matches!( + run_memset(&mut memory, 0x2000, 0x11, DMA_MEMCPY_MAX_BYTES + 1), + Err(ExecutionError::DmaMemcpyChunkTooLarge(n)) if n == DMA_MEMCPY_MAX_BYTES + 1 + )); +} + +#[test] +fn dma_memset_rejects_fill_wider_than_a_byte() { + // The guest stub masks `a1` with `andi ..., 255`, so only a malformed call + // reaches here. Rejecting it is what lets the AIR prove the bound with one LT. + let mut memory = Memory::default(); + assert!(matches!( + run_memset(&mut memory, 0x2000, DMA_MEMSET_MAX_FILL + 1, 8), + Err(ExecutionError::DmaMemsetFillTooLarge(c)) if c == DMA_MEMSET_MAX_FILL + 1 + )); +} + +proptest! { + #[test] + fn dma_memset_matches_reference_fill( + dst_offset in 0usize..64, + count in 0usize..200, + fill in 0u8..=255, + ) { + const BASE: u64 = 0x9000; + const REGION: usize = 320; + + let mut expected = vec![0u8; REGION]; + expected[dst_offset..dst_offset + count].fill(fill); + + let mut memory = Memory::default(); + run_memset(&mut memory, BASE + dst_offset as u64, u64::from(fill), count as u64).unwrap(); + + let actual = memory.load_bytes(BASE, REGION as u64).unwrap(); + prop_assert_eq!(actual, expected); + } +} diff --git a/executor/src/vm/instruction/execution.rs b/executor/src/vm/instruction/execution.rs index 7af76dd02..4fbe46325 100644 --- a/executor/src/vm/instruction/execution.rs +++ b/executor/src/vm/instruction/execution.rs @@ -37,6 +37,9 @@ syscall_numbers! { /// `DMA_MEMCPY_SYSCALL_NUMBER`. DMA memcpy chunks are proven by the /// dedicated DMA table. DmaMemcpy = 95, + // Placeholder discriminant. The actual syscall value is DMA_MEMSET_SYSCALL_NUMBER. + // DMA memset chunks are proven by the dedicated DMA_SET table. + DmaMemset = 96, } /// Syscall number for KeccakPermute (u64::MAX - 1 = 0xFFFF_FFFF_FFFF_FFFE). @@ -58,6 +61,14 @@ pub const DMA_MEMCPY_SYSCALL_NUMBER: u64 = u64::MAX - 2; /// larger copies, and the prover enforces this bound on every first DMA row. pub const DMA_MEMCPY_MAX_BYTES: u64 = 256; +/// DMA memset syscall number. Must match `syscalls/src/syscalls.rs`. +pub const DMA_MEMSET_SYSCALL_NUMBER: u64 = u64::MAX - 3; +/// Largest fill value a DMA memset ecall accepts. C's `memset` writes +/// `(unsigned char)c`, so the guest stub masks `a1` down to this range; a wider +/// value is a malformed call. Bounding it here lets the DMA_SET AIR prove the +/// same bound with one ALU LT instead of decomposing the register. +pub const DMA_MEMSET_MAX_FILL: u64 = 255; + /// `2^32`. ECSM memory operands must not overflow their lower 32-bit address limb when the /// largest per-access offset is added: the 32-byte operands reach offset +31 (last byte). const LOW_LIMB: u64 = 1 << 32; @@ -73,6 +84,7 @@ impl TryFrom for SyscallNumbers { v if v == KECCAK_SYSCALL_NUMBER => Ok(SyscallNumbers::KeccakPermute), v if v == ECSM_SYSCALL_NUMBER => Ok(SyscallNumbers::Ecsm), v if v == DMA_MEMCPY_SYSCALL_NUMBER => Ok(SyscallNumbers::DmaMemcpy), + v if v == DMA_MEMSET_SYSCALL_NUMBER => Ok(SyscallNumbers::DmaMemset), _ => Err(()), } } @@ -94,6 +106,7 @@ impl SyscallNumbers { SyscallNumbers::KeccakPermute => KECCAK_SYSCALL_NUMBER, SyscallNumbers::Ecsm => ECSM_SYSCALL_NUMBER, SyscallNumbers::DmaMemcpy => DMA_MEMCPY_SYSCALL_NUMBER, + SyscallNumbers::DmaMemset => DMA_MEMSET_SYSCALL_NUMBER, SyscallNumbers::Print => SyscallNumbers::Print as u64, SyscallNumbers::Panic => SyscallNumbers::Panic as u64, SyscallNumbers::Commit => SyscallNumbers::Commit as u64, @@ -108,7 +121,7 @@ impl SyscallNumbers { match self { SyscallNumbers::KeccakPermute => Some(Accelerator::Keccak), SyscallNumbers::Ecsm => Some(Accelerator::Ecsm), - SyscallNumbers::DmaMemcpy => Some(Accelerator::Dma), + SyscallNumbers::DmaMemcpy | SyscallNumbers::DmaMemset => Some(Accelerator::Dma), SyscallNumbers::Print | SyscallNumbers::Panic | SyscallNumbers::Commit @@ -524,6 +537,29 @@ impl Instruction { src2_val = src; dst_val = n; } + SyscallNumbers::DmaMemset => { + // memset(dst = x10, fill = x11, n = x12). No source range + // to snapshot: every byte written is the same constant, so + // the DMA_SET trace carries one fill column instead of the + // eight value columns memcpy needs. + let dst = registers.read(10)?; + let fill = registers.read(11)?; + let n = registers.read(12)?; + if n > DMA_MEMCPY_MAX_BYTES { + return Err(ExecutionError::DmaMemcpyChunkTooLarge(n)); + } + if fill > DMA_MEMSET_MAX_FILL { + return Err(ExecutionError::DmaMemsetFillTooLarge(fill)); + } + dst.checked_add(n).ok_or(MemoryError::AddressOverflow)?; + + let byte = fill as u8; + for i in 0..n { + memory.store_byte(dst + i, byte); + } + src2_val = fill; + dst_val = n; + } SyscallNumbers::Halt => { // halt return Ok(Log { @@ -706,6 +742,8 @@ pub enum ExecutionError { EcsmOperandOverlap, #[error("DMA memcpy chunk has {0} bytes; maximum per ecall is {DMA_MEMCPY_MAX_BYTES}")] DmaMemcpyChunkTooLarge(u64), + #[error("DMA memset fill is {0}; maximum is {DMA_MEMSET_MAX_FILL}")] + DmaMemsetFillTooLarge(u64), #[error("ECSM scalar multiplication error: {0}")] Ecsm(#[from] ecsm::EcsmError), } diff --git a/executor/tests/rust.rs b/executor/tests/rust.rs index 4eb3b32f9..037b64656 100644 --- a/executor/tests/rust.rs +++ b/executor/tests/rust.rs @@ -1,7 +1,10 @@ use executor::{ elf::Elf, vm::execution::{Executor, ReturnValues}, - vm::instruction::{decoding::Instruction, execution::DMA_MEMCPY_SYSCALL_NUMBER}, + vm::instruction::{ + decoding::Instruction, + execution::{DMA_MEMCPY_SYSCALL_NUMBER, DMA_MEMSET_SYSCALL_NUMBER}, + }, }; // NOTE: These tests require 64-bit RISC-V ELF files (RV64IM). @@ -149,6 +152,25 @@ fn test_dma_memcpy_cases() { ); } +#[test] +fn test_dma_memset_cases() { + let elf_data = std::fs::read("./program_artifacts/rust/dma_memset_cases.elf").unwrap(); + let program = Elf::load(&elf_data).unwrap(); + let result = Executor::new(&program, vec![]).unwrap().run().unwrap(); + + assert_eq!(result.return_values.memory_values, b"dma-memset-ok"); + assert!( + result.logs.iter().any(|log| { + log.src1_val == DMA_MEMSET_SYSCALL_NUMBER + && matches!( + result.instructions.get(&log.current_pc), + Some(Instruction::EcallEbreak) + ) + }), + "the strong memset symbol must execute at least one DMA ecall" + ); +} + #[test] fn test_hashmap() { run_program_and_check_output("./program_artifacts/rust/hashmap.elf", 3, vec![]); diff --git a/prover/src/auto_storage.rs b/prover/src/auto_storage.rs index 4c5e06783..ee13acb53 100644 --- a/prover/src/auto_storage.rs +++ b/prover/src/auto_storage.rs @@ -11,6 +11,9 @@ use crate::tables::commit::{bus_interactions as commit_buses, cols::NUM_COLUMNS use crate::tables::cpu::{bus_interactions as cpu_buses, cols::NUM_COLUMNS as CPU_COLS}; use crate::tables::decode::{bus_interactions as decode_buses, cols::NUM_COLUMNS as DECODE_COLS}; use crate::tables::dma::{bus_interactions as dma_buses, cols::NUM_COLUMNS as DMA_COLS}; +use crate::tables::dma_set::{ + bus_interactions as dma_set_buses, cols::NUM_COLUMNS as DMA_SET_COLS, +}; use crate::tables::dvrm::{bus_interactions as dvrm_buses, cols::NUM_COLUMNS as DVRM_COLS}; use crate::tables::halt::{bus_interactions as halt_buses, cols::NUM_COLUMNS as HALT_COLS}; use crate::tables::load::{bus_interactions as load_buses, cols::NUM_COLUMNS as LOAD_COLS}; @@ -185,6 +188,12 @@ fn table_specs(lengths: &TableLengths) -> Vec { aux_cols(dma_buses().len()), 1, ), + ( + lengths.dma_set_padded_rows, + DMA_SET_COLS as u64, + aux_cols(dma_set_buses().len()), + 1, + ), // BITWISE / DECODE / PAGE / REGISTER take the preprocessed-trace commit // path: it extracts ALL columns into the LDE and builds two Merkle trees // (precomputed_tree + mult_tree), so main_cols = full NUM_COLUMNS and diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 26398acfa..4501eaa3c 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -52,7 +52,7 @@ use crate::tables::trace_builder::count_table_lengths; use crate::tables::types::BusId; use crate::test_utils::{ E, F, VmAir, create_bitwise_air, create_branch_air, create_bytewise_air, create_commit_air, - create_cpu_air, create_cpu32_air, create_decode_air, create_dma_air, create_dvrm_air, + create_cpu_air, create_cpu32_air, create_decode_air, create_dma_air, create_dma_set_air, create_dvrm_air, create_ecdas_air, create_ecsm_air, create_eq_air, create_halt_air, create_keccak_air, create_keccak_rc_air, create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, create_memw_aligned_air, create_memw_register_air, create_mul_air, create_page_air, @@ -82,8 +82,8 @@ pub struct RuntimePageRange { /// Number of tables that always contribute exactly one sub-proof, regardless /// of `TableCounts`: bitwise, decode, halt, commit, keccak, keccak_rnd, -/// keccak_rc, register, ecsm, ecdas, dma. -pub const FIXED_TABLE_COUNT: usize = 11; +/// keccak_rc, register, ecsm, ecdas, dma, dma_set. +pub const FIXED_TABLE_COUNT: usize = 12; /// Number of chunks for each split table. /// The verifier needs this to reconstruct matching AIRs. @@ -518,6 +518,7 @@ pub(crate) struct VmAirs { pub ecsm: VmAir, pub ecdas: VmAir, pub dma: VmAir, + pub dma_set: VmAir, pub register: VmAir, pub pages: Vec, pub memw_registers: Vec, @@ -544,6 +545,7 @@ impl VmAirs { (self.ecsm.as_ref(), &mut traces.ecsm, &()), (self.ecdas.as_ref(), &mut traces.ecdas, &()), (self.dma.as_ref(), &mut traces.dma, &()), + (self.dma_set.as_ref(), &mut traces.dma_set, &()), (self.register.as_ref(), &mut traces.register, &()), ]; if self.include_halt { @@ -619,6 +621,7 @@ impl VmAirs { self.ecsm.as_ref(), self.ecdas.as_ref(), self.dma.as_ref(), + self.dma_set.as_ref(), self.register.as_ref(), ]; if self.include_halt { @@ -777,6 +780,7 @@ impl VmAirs { let ecsm: VmAir = Box::new(create_ecsm_air(proof_options)); let ecdas: VmAir = Box::new(create_ecdas_air(proof_options)); let dma: VmAir = Box::new(create_dma_air(proof_options)); + let dma_set: VmAir = Box::new(create_dma_set_air(proof_options)); let register: VmAir = if let Some((commitment, num_preprocessed_cols)) = register_preprocessed { Box::new( @@ -884,6 +888,7 @@ impl VmAirs { ecsm, ecdas, dma, + dma_set, register, pages, memw_registers, diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 88d0bf041..5c0a94be1 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -191,6 +191,9 @@ pub struct CpuOperation { /// Whether this ECALL is a DMA memcpy. Operands are recovered from x10/x11/x12. pub ecall_dma_memcpy: bool, + + /// Whether this ECALL is a DMA memset. Operands are recovered from x10/x11/x12. + pub ecall_dma_memset: bool, } impl CpuOperation { @@ -240,6 +243,8 @@ impl CpuOperation { f.ecall && log.src1_val == executor::vm::instruction::execution::ECSM_SYSCALL_NUMBER; let ecall_dma_memcpy = f.ecall && log.src1_val == executor::vm::instruction::execution::DMA_MEMCPY_SYSCALL_NUMBER; + let ecall_dma_memset = f.ecall + && log.src1_val == executor::vm::instruction::execution::DMA_MEMSET_SYSCALL_NUMBER; // Word instructions are fully handled by CPU32; the main CPU row is a // delegate that only advances the PC and sends the CPU32 lookup. We still @@ -359,6 +364,7 @@ impl CpuOperation { keccak_state_addr, ecall_ecsm, ecall_dma_memcpy, + ecall_dma_memset, } } diff --git a/prover/src/tables/dma_set.rs b/prover/src/tables/dma_set.rs new file mode 100644 index 000000000..01e11426a --- /dev/null +++ b/prover/src/tables/dma_set.rs @@ -0,0 +1,453 @@ +//! DMA memset table — proves a `memset(dst, fill, n)` off the CPU execution trace. +//! +//! The guest's strong `memset` symbol (see `syscalls/src/syscalls.rs`) dispatches +//! bulk fills to the DMA memset ecall (`DMA_MEMSET_SYSCALL_NUMBER`); this table +//! proves the fill so the per-byte store loop leaves the CPU trace. +//! +//! Same streaming shape as the memcpy table (`dma.rs`): a row writes eight bytes +//! while `count >= 8`, otherwise one byte, and rows chain through `DmaSetNext` +//! until a terminal row where `count == 0`. The LT table pins that choice, so the +//! prover cannot select a convenient partition. +//! +//! Two things make this cheaper than memcpy rather than a copy of it: +//! +//! * **No source.** There is nothing to read, so a row emits one MEMW *write* at +//! `T+1` and no read at all — half the memory traffic per byte. There is also +//! no `src`/`src_incr` pair to carry or range-check. +//! * **No value lanes.** Every byte written is the same constant, so one `fill` +//! column replaces memcpy's eight value columns. `fill_wide` is `fill` on +//! eight-byte rows and zero on one-byte tail rows, which is what lets the same +//! write tuple serve both widths without per-lane constraints. +//! +//! The result is 20 columns against memcpy's 32, and 18 bus interactions against +//! 23. `fill <= 255` is proven on the first row, mirroring how `dma.rs` proves +//! the per-ecall byte bound: the executor rejects a wider value, so an honest +//! guest (whose stub masks `a1`) never trips it. +//! +//! ## Columns (20 total) +//! - `timestamp`: DWordWL (2) — the ECALL timestamp +//! - `dst`: DWordWL (2) — current destination byte address +//! - `dst_incr`: DWordHL (4) — dst + selected width +//! - `count`: DWordWL (2) — remaining byte count (including this byte; 0 on the end row) +//! - `count_decr`: DWordHL (4) — count - width (all 0xFFFF when count == 0) +//! - `fill`: byte being written +//! - `fill_wide`: `fill` on eight-byte rows, 0 on one-byte tail rows +//! - `first`: Bit — first row of a fill +//! - `end`: Bit — last row (count was 0) +//! - `tail`: Bit — `count < 8`; selects a 1-byte rather than 8-byte row +//! - `mu`: Bit — multiplicity (1 real, 0 padding) +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; +use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; +use stark::trace::TraceTable; + +use crate::constraints::templates::{ + AddLinearTerm, AddOperand, emit_add_pair, emit_add_pair_no_overflow, emit_is_bit, +}; + +use executor::vm::instruction::execution::{ + DMA_MEMCPY_MAX_BYTES as EXECUTOR_DMA_MEMCPY_MAX_BYTES, + DMA_MEMSET_MAX_FILL as EXECUTOR_DMA_MEMSET_MAX_FILL, DMA_MEMSET_SYSCALL_NUMBER, +}; + +use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; + +/// DMA memset syscall value, split into 32-bit limbs for the Ecall bus. +const DMA_MEMSET_LO32: u64 = DMA_MEMSET_SYSCALL_NUMBER & 0xFFFF_FFFF; +const DMA_MEMSET_HI32: u64 = DMA_MEMSET_SYSCALL_NUMBER >> 32; +/// Per-ecall byte bound, shared with memcpy so both stubs chunk identically. +pub const DMA_MEMSET_MAX_BYTES: u64 = EXECUTOR_DMA_MEMCPY_MAX_BYTES; +/// Largest accepted fill value, taken from the executor so the bound the AIR +/// proves cannot drift from the bound execution enforces. +pub const DMA_MEMSET_MAX_FILL: u64 = EXECUTOR_DMA_MEMSET_MAX_FILL; + +pub mod cols { + pub const TIMESTAMP_0: usize = 0; + pub const TIMESTAMP_1: usize = 1; + + pub const DST_0: usize = 2; + pub const DST_1: usize = 3; + + pub const DST_INCR_0: usize = 4; + pub const DST_INCR_1: usize = 5; + pub const DST_INCR_2: usize = 6; + pub const DST_INCR_3: usize = 7; + + pub const COUNT_0: usize = 8; + pub const COUNT_1: usize = 9; + + pub const COUNT_DECR_0: usize = 10; + pub const COUNT_DECR_1: usize = 11; + pub const COUNT_DECR_2: usize = 12; + pub const COUNT_DECR_3: usize = 13; + + pub const FILL: usize = 14; + pub const FILL_WIDE: usize = 15; + + pub const FIRST: usize = 16; + pub const END: usize = 17; + pub const TAIL: usize = 18; + pub const MU: usize = 19; + + pub const NUM_COLUMNS: usize = 20; +} + +/// One row of the DMA memset table: eight bytes, one tail byte, or the terminal row. +#[derive(Debug, Clone)] +pub struct DmaSetOperation { + pub timestamp: u64, + pub dst: u64, + /// Remaining byte count (including this byte; 0 on the end row). + pub count: u64, + pub fill: u8, + pub first: bool, + pub end: bool, +} + +/// Generates the DMA memset trace. One row per operation; padded to the next +/// power of two (min 4). Padding rows model an inactive one-byte step so the +/// unconditional `count_decr + step == count` relation still holds. +pub fn generate_dma_set_trace( + ops: &[DmaSetOperation], +) -> TraceTable { + let n = ops.len(); + let num_rows = n.next_power_of_two().max(4); + let mut trace = TraceTable::new_main( + crate::tables::types::zeroed_fe_vec(num_rows * cols::NUM_COLUMNS), + cols::NUM_COLUMNS, + 1, + ); + let table = &mut trace.main_table; + + for (row_idx, op) in ops.iter().enumerate() { + let tail = op.count < 8; + let width = if tail { 1 } else { 8 }; + table.set_dword_wl(row_idx, cols::TIMESTAMP_0, op.timestamp); + + table.set_dword_wl(row_idx, cols::DST_0, op.dst); + table.set_dword_hl(row_idx, cols::DST_INCR_0, op.dst.wrapping_add(width)); + + table.set_dword_wl(row_idx, cols::COUNT_0, op.count); + table.set_dword_hl(row_idx, cols::COUNT_DECR_0, op.count.wrapping_sub(width)); + + table.set_byte(row_idx, cols::FILL, op.fill); + // Zero on tail rows so the shared write tuple narrows to a single byte. + table.set_byte(row_idx, cols::FILL_WIDE, if tail { 0 } else { op.fill }); + + table.set_bool(row_idx, cols::FIRST, op.first); + table.set_bool(row_idx, cols::END, op.end); + table.set_bool(row_idx, cols::TAIL, tail); + table.set_fe(row_idx, cols::MU, FE::one()); + } + + for row_idx in n..num_rows { + table.set_fe(row_idx, cols::COUNT_0, FE::one()); + table.set_fe(row_idx, cols::DST_INCR_0, FE::one()); + table.set_fe(row_idx, cols::TAIL, FE::one()); + } + + trace +} + +/// Helper: a MEMW register read (CO24, is_register=1, width2), value == old == the +/// register's two 32-bit limbs. Binds `x{reg}` to `(lo_col, hi_col)` at the ecall ts. +fn memw_register_read(reg_addr: u64, lo_col: usize, hi_col: usize) -> Vec { + let limb = |c: usize| BusValue::Packed { + start_column: c, + packing: Packing::Direct, + }; + vec![ + limb(lo_col), + limb(hi_col), + BusValue::constant(0), + BusValue::constant(0), + BusValue::constant(0), + BusValue::constant(0), + BusValue::constant(0), + BusValue::constant(0), + BusValue::constant(1), // is_register = 1 + BusValue::constant(reg_addr), // base_address lo = 2*reg + BusValue::constant(0), // base_address hi + limb(lo_col), + limb(hi_col), + BusValue::constant(0), + BusValue::constant(0), + BusValue::constant(0), + BusValue::constant(0), + BusValue::constant(0), + BusValue::constant(0), + limb(cols::TIMESTAMP_0), + limb(cols::TIMESTAMP_1), + BusValue::constant(1), // w2 = 1 (register = 2 words) + BusValue::constant(0), + BusValue::constant(0), + ] +} + +/// An `IsHalfword` range-check sender for one halfword column (mult = mu). +fn halfword(column: usize) -> BusInteraction { + BusInteraction::sender( + BusId::IsHalfword, + Multiplicity::Column(cols::MU), + vec![BusValue::Packed { + start_column: column, + packing: Packing::Direct, + }], + ) +} + +/// DMA memset bus interactions (18 total). +pub fn bus_interactions() -> Vec { + let mu_minus_end = Multiplicity::Diff(cols::MU, cols::END); + let mu_minus_first = Multiplicity::Diff(cols::MU, cols::FIRST); + let direct = |c: usize| BusValue::Packed { + start_column: c, + packing: Packing::Direct, + }; + + vec![ + // 1. Receive ECALL from CPU (mult = first). + BusInteraction::receiver( + BusId::Ecall, + Multiplicity::Column(cols::FIRST), + vec![ + direct(cols::TIMESTAMP_0), + direct(cols::TIMESTAMP_1), + BusValue::constant(DMA_MEMSET_LO32), + BusValue::constant(DMA_MEMSET_HI32), + ], + ), + // 2. Send to DmaSetNext (mult = mu - end): [ts, dst_incr, count_decr, fill]. + // `fill` rides the chain so every row of one call writes the same byte. + BusInteraction::sender( + BusId::DmaSetNext, + mu_minus_end.clone(), + vec![ + direct(cols::TIMESTAMP_0), + direct(cols::TIMESTAMP_1), + BusValue::Packed { + start_column: cols::DST_INCR_0, + packing: Packing::DWordHL, + }, + BusValue::Packed { + start_column: cols::COUNT_DECR_0, + packing: Packing::DWordHL, + }, + direct(cols::FILL), + ], + ), + // 3. Receive from DmaSetNext (mult = mu - first): [ts, dst, count, fill]. + BusInteraction::receiver( + BusId::DmaSetNext, + mu_minus_first, + vec![ + direct(cols::TIMESTAMP_0), + direct(cols::TIMESTAMP_1), + BusValue::Packed { + start_column: cols::DST_0, + packing: Packing::DWordWL, + }, + BusValue::Packed { + start_column: cols::COUNT_0, + packing: Packing::DWordWL, + }, + direct(cols::FILL), + ], + ), + // 4-7. IsHalfword: count_decr (mult = mu). + halfword(cols::COUNT_DECR_0), + halfword(cols::COUNT_DECR_1), + halfword(cols::COUNT_DECR_2), + halfword(cols::COUNT_DECR_3), + // 8-11. IsHalfword: dst_incr (mult = mu). + halfword(cols::DST_INCR_0), + halfword(cols::DST_INCR_1), + halfword(cols::DST_INCR_2), + halfword(cols::DST_INCR_3), + // 12. ZERO bus end detection: end == 1 iff all count_decr halfwords are 0xFFFF. + BusInteraction::sender( + BusId::Zero, + Multiplicity::Column(cols::MU), + vec![ + BusValue::linear(vec![ + LinearTerm::Constant(4 * 65535), + LinearTerm::Column { + coefficient: -1, + column: cols::COUNT_DECR_0, + }, + LinearTerm::Column { + coefficient: -1, + column: cols::COUNT_DECR_1, + }, + LinearTerm::Column { + coefficient: -1, + column: cols::COUNT_DECR_2, + }, + LinearTerm::Column { + coefficient: -1, + column: cols::COUNT_DECR_3, + }, + ]), + direct(cols::END), + ], + ), + // 13-15. Register reads (mult = first): x10 = dst, x11 = fill, x12 = count. + // x11's high limb is pinned to 0 by the constant below, so a fill wider + // than 32 bits cannot be smuggled past the `fill <= 255` check. + BusInteraction::sender( + BusId::Memw, + Multiplicity::Column(cols::FIRST), + memw_register_read(20, cols::DST_0, cols::DST_1), + ), + BusInteraction::sender(BusId::Memw, Multiplicity::Column(cols::FIRST), { + let mut tuple = memw_register_read(22, cols::FILL, cols::FILL); + // x11 = (fill, 0): overwrite both high-limb slots with the constant 0. + tuple[1] = BusValue::constant(0); + tuple[12] = BusValue::constant(0); + tuple + }), + BusInteraction::sender( + BusId::Memw, + Multiplicity::Column(cols::FIRST), + memw_register_read(24, cols::COUNT_0, cols::COUNT_1), + ), + // 16. ALU LT pins `tail = (count < 8)`. + BusInteraction::sender( + BusId::Alu, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: cols::COUNT_0, + packing: Packing::DWordWL, + }, + BusValue::constant(8), + BusValue::constant(0), + BusValue::constant(alu_op::LT as u64), + direct(cols::TAIL), + BusValue::constant(0), + ], + ), + // 17. The first row proves `count <= DMA_MEMSET_MAX_BYTES`. + BusInteraction::sender( + BusId::Alu, + Multiplicity::Column(cols::FIRST), + vec![ + BusValue::Packed { + start_column: cols::COUNT_0, + packing: Packing::DWordWL, + }, + BusValue::constant(DMA_MEMSET_MAX_BYTES + 1), + BusValue::constant(0), + BusValue::constant(alu_op::LT as u64), + BusValue::constant(1), + BusValue::constant(0), + ], + ), + // 18. The first row proves `fill <= DMA_MEMSET_MAX_FILL`, so the byte the + // write tuple broadcasts really is a byte. + BusInteraction::sender( + BusId::Alu, + Multiplicity::Column(cols::FIRST), + vec![ + // The ALU bus takes its left operand as two 32-bit limbs; `fill` + // is a single byte column, so the high limb is a literal zero. + direct(cols::FILL), + BusValue::constant(0), + BusValue::constant(DMA_MEMSET_MAX_FILL + 1), + BusValue::constant(0), + BusValue::constant(alu_op::LT as u64), + BusValue::constant(1), + BusValue::constant(0), + ], + ), + // 19. MEMW write to dst at T+1. `w8 = 1-tail`; lanes 1..7 carry `fill_wide`, + // which the constraints force to 0 exactly on one-byte tail rows. + BusInteraction::sender(BusId::Memw, mu_minus_end, { + let mut tuple = Vec::with_capacity(16); + tuple.push(BusValue::constant(0)); // is_register + tuple.push(direct(cols::DST_0)); + tuple.push(direct(cols::DST_1)); + tuple.push(direct(cols::FILL)); + for _ in 1..8 { + tuple.push(direct(cols::FILL_WIDE)); + } + tuple.push(BusValue::linear(vec![ + LinearTerm::Constant(1), + LinearTerm::Column { + coefficient: 1, + column: cols::TIMESTAMP_0, + }, + ])); + tuple.push(direct(cols::TIMESTAMP_1)); + tuple.push(BusValue::constant(0)); // w2 + tuple.push(BusValue::constant(0)); // w4 + tuple.push(BusValue::linear(vec![ + LinearTerm::Constant(1), + LinearTerm::Column { + coefficient: -1, + column: cols::TAIL, + }, + ])); // w8 = 1-tail + tuple + }), + ] +} + +/// The DMA memset constraints: +/// - bitness for `first`, `end`, `tail`, `mu`; +/// - active first/end rows; +/// - `step = 8 - 7*tail` address/count arithmetic; +/// - `fill_wide` equals `fill` on wide rows and 0 on tail rows. +#[derive(Clone, Copy)] +pub struct DmaSetConstraints; + +impl ConstraintSet for DmaSetConstraints { + fn eval>(&self, b: &mut B) { + emit_is_bit(b, 0, cols::FIRST, None); + emit_is_bit(b, 1, cols::END, None); + emit_is_bit(b, 2, cols::TAIL, None); + emit_is_bit(b, 3, cols::MU, None); + + let one = b.one(); + let first = b.main(0, cols::FIRST); + let end = b.main(0, cols::END); + let mu = b.main(0, cols::MU); + b.emit_base(4, (first + end) * (one.clone() - mu)); + + let step = AddOperand::linear( + &[ + AddLinearTerm::Constant(8), + AddLinearTerm::Column { + coefficient: -7, + column: cols::TAIL, + }, + ], + &[], + ); + + emit_add_pair_no_overflow( + b, + 5, + cols::MU, + cols::END, + &AddOperand::dword(cols::DST_0), + &step, + &AddOperand::from_dword_hl(cols::DST_INCR_0), + ); + emit_add_pair( + b, + 7, + &[], + &AddOperand::from_dword_hl(cols::COUNT_DECR_0), + &step, + &AddOperand::dword(cols::COUNT_0), + ); + + // fill_wide == (1 - tail) * fill, expressed as the two cases so the + // degree stays at 2: zero on tail rows, equal to fill otherwise. + let tail = b.main(0, cols::TAIL); + let fill = b.main(0, cols::FILL); + let fill_wide = b.main(0, cols::FILL_WIDE); + b.emit_base(9, tail.clone() * fill_wide.clone()); + b.emit_base(10, (one - tail) * (fill_wide - fill)); + } +} diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index 2f78ec872..950d2cddf 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -29,6 +29,7 @@ pub mod cpu; pub mod cpu32; pub mod decode; pub mod dma; +pub mod dma_set; pub mod dvrm; pub mod ecdas; pub mod ecsm; diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index cc1484148..f76fab4d4 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -47,6 +47,7 @@ use super::cpu::{self, CpuOperation}; use super::cpu32; use super::decode; use super::dma; +use super::dma_set; use super::dvrm::{self, DvrmOperation}; use super::ecdas; use super::ecsm; @@ -551,6 +552,7 @@ fn collect_ops_from_cpu( Vec, Vec, Vec, + Vec, ) { let mut memw = MemwBuckets::with_register_capacity(cpu_ops.len() * 3); let mut load_ops = Vec::with_capacity(cpu_ops.len() / 8 + 1); @@ -563,6 +565,7 @@ fn collect_ops_from_cpu( let mut ecsm_ops = Vec::new(); let mut ecdas_ops = Vec::new(); let mut dma_ops = Vec::new(); + let mut dma_set_ops = Vec::new(); // Seed from the carried x254 (0 for a monolithic run or the first epoch) so a // continuation epoch indexes its commits globally, matching the x254 the // register binding transports across epochs. Resetting to 0 here would drift @@ -665,6 +668,15 @@ fn collect_ops_from_cpu( dma_ops.extend(rows); } + // DMA memset: authenticate x10/x11/x12, then write every destination byte + // at T+1. There is no source phase — every byte written is the same + // constant, so no snapshot is needed and overlap cannot arise. + if op.ecall_dma_memset { + let (memset_memw, rows) = collect_dma_memset_ops(op, memory_state, register_state); + memw.extend_ops(memset_memw); + dma_set_ops.extend(rows); + } + // --- ALU chip dispatch (no state tracking) --- // Word (`*W`) instructions are delegated to CPU32 (which itself drives // the ALU chips); the main CPU does not send the ALU bus for them, so we @@ -721,6 +733,7 @@ fn collect_ops_from_cpu( ecsm_ops, ecdas_ops, dma_ops, + dma_set_ops, ) } @@ -1069,6 +1082,104 @@ fn collect_dma_memcpy_ops( (memw_ops, rows) } +/// Replays one DMA memset ecall. +/// +/// Register operands are read at `T`; every destination chunk is written at +/// `T+1`. Chunks are eight bytes while `remaining >= 8`, then one byte per tail +/// row, matching the row schedule the DMA_SET AIR pins through the LT table. +fn collect_dma_memset_ops( + op: &CpuOperation, + memory_state: &mut MemoryState, + register_state: &mut RegisterState, +) -> (Vec, Vec) { + let t = op.timestamp; + let dst = register_state.read(10).0; + let fill = register_state.read(11).0; + let count = register_state.read(12).0; + assert!( + count <= dma_set::DMA_MEMSET_MAX_BYTES, + "successful DMA memset ecall must respect the per-call chunk bound" + ); + assert!( + fill <= dma_set::DMA_MEMSET_MAX_FILL, + "successful DMA memset ecall must carry a byte-sized fill" + ); + let fill_byte = fill as u8; + + let data_rows = count / 8 + count % 8; + let capacity = usize::try_from(data_rows) + .ok() + .and_then(|n| n.checked_add(3)) + .expect("successful DMA memset execution must fit host address space"); + let mut memw_ops = Vec::with_capacity(capacity); + + // Bind the ecall's three argument registers to the first DMA_SET row. + for (reg, value) in [(10u8, dst), (11u8, fill), (12u8, count)] { + let packed = pack_register_value(value); + let (_old_value, old_ts) = register_state.read(reg); + memw_ops.push( + MemwOperation::new(true, 2 * reg as u64, packed, t, 2, true) + .with_old(packed, [old_ts, old_ts, 0, 0, 0, 0, 0, 0]), + ); + register_state.write(reg, value, t); + } + + let rows_capacity = usize::try_from(data_rows + 1) + .expect("successful DMA memset execution must fit host address space"); + let mut rows = Vec::with_capacity(rows_capacity); + let mut offset = 0u64; + let mut remaining = count; + let mut first = true; + + while remaining != 0 { + let width = if remaining >= 8 { 8u8 } else { 1u8 }; + let destination_addr = dst + .checked_add(offset) + .expect("DMA memset range was validated by executor"); + // Only the lanes actually written carry the fill; the rest stay zero so + // this matches the AIR, which sends `fill` in lane 0 and `fill_wide` + // (zero on one-byte tail rows) in lanes 1..7. + let mut value = [0u32; 8]; + for lane in value.iter_mut().take(width as usize) { + *lane = fill_byte as u32; + } + let (old_values, old_timestamps) = + memory_state.read_bytes(destination_addr, width as usize); + memw_ops.push( + MemwOperation::new(false, destination_addr, value, t + 1, width, false) + .with_old(old_values, old_timestamps), + ); + let dword = u64::from_le_bytes([fill_byte; 8]); + memory_state.write_bytes(destination_addr, dword, width as usize, t + 1); + + rows.push(dma_set::DmaSetOperation { + timestamp: t, + dst: destination_addr, + count: remaining, + fill: fill_byte, + first, + end: false, + }); + + first = false; + offset += u64::from(width); + remaining -= width as u64; + } + + rows.push(dma_set::DmaSetOperation { + timestamp: t, + dst: dst + .checked_add(count) + .expect("DMA memset range was validated by executor"), + count: 0, + fill: fill_byte, + first, + end: true, + }); + + (memw_ops, rows) +} + /// Sizing-pass replay of one bounded DMA ecall. /// /// This mirrors [`collect_dma_memcpy_ops`] but counts rows and routes each @@ -1166,6 +1277,73 @@ fn replay_dma_memcpy_for_sizing( snapshot_count + 1 } +/// Sizing-pass replay of one bounded DMA memset ecall. +/// +/// Mirrors [`collect_dma_memset_ops`] but counts rows and routes each +/// `MemwOperation` immediately instead of allocating vectors. No snapshot buffer +/// is needed: memset writes a constant, so there is no source to preserve. +#[cfg(feature = "disk-spill")] +fn replay_dma_memset_for_sizing( + op: &CpuOperation, + memory_state: &mut MemoryState, + register_state: &mut RegisterState, + mut visit_memw: impl FnMut(&MemwOperation), +) -> usize { + let t = op.timestamp; + let dst = register_state.read(10).0; + let fill = register_state.read(11).0; + let count = register_state.read(12).0; + assert!( + count <= dma_set::DMA_MEMSET_MAX_BYTES, + "successful DMA memset ecall must respect the per-call chunk bound" + ); + assert!( + fill <= dma_set::DMA_MEMSET_MAX_FILL, + "successful DMA memset ecall must carry a byte-sized fill" + ); + let fill_byte = fill as u8; + + for (reg, value) in [(10u8, dst), (11u8, fill), (12u8, count)] { + let packed = pack_register_value(value); + let (_old_value, old_ts) = register_state.read(reg); + let memw = MemwOperation::new(true, 2 * reg as u64, packed, t, 2, true) + .with_old(packed, [old_ts, old_ts, 0, 0, 0, 0, 0, 0]); + visit_memw(&memw); + register_state.write(reg, value, t); + } + + let mut rows = 0usize; + let mut offset = 0u64; + let mut remaining = count; + let dword = u64::from_le_bytes([fill_byte; 8]); + + while remaining != 0 { + let width = if remaining >= 8 { 8u8 } else { 1u8 }; + let destination_addr = dst + .checked_add(offset) + .expect("DMA memset range was validated by executor"); + // Only the lanes actually written carry the fill; the rest stay zero so + // this matches the AIR, which sends `fill` in lane 0 and `fill_wide` + // (zero on one-byte tail rows) in lanes 1..7. + let mut value = [0u32; 8]; + for lane in value.iter_mut().take(width as usize) { + *lane = fill_byte as u32; + } + let (old_values, old_timestamps) = + memory_state.read_bytes(destination_addr, width as usize); + let memw = MemwOperation::new(false, destination_addr, value, t + 1, width, false) + .with_old(old_values, old_timestamps); + visit_memw(&memw); + memory_state.write_bytes(destination_addr, dword, width as usize, t + 1); + + rows += 1; + offset += u64::from(width); + remaining -= u64::from(width); + } + + rows + 1 +} + /// Collects register read/write operations (M1, M3, M5) from CpuOperation, /// pushing them into `memw_ops`. fn collect_register_ops_from_cpu( @@ -2466,6 +2644,36 @@ fn collect_bitwise_from_commit(commit_ops: &[CommitOperation]) -> Vec Vec { + let mut lookups = Vec::with_capacity(ops.len() * 9); + for op in ops { + let width = if op.count < 8 { 1 } else { 8 }; + let count_decr = op.count.wrapping_sub(width); + let dst_incr = op.dst.wrapping_add(width); + + for value in [count_decr, dst_incr] { + for shift in [0, 16, 32, 48] { + let half = ((value >> shift) & 0xFFFF) as u16; + lookups.push(BitwiseOperation::halfword( + BitwiseOperationType::IsHalf, + (half & 0xFF) as u8, + (half >> 8) as u8, + )); + } + } + + let halves = [ + (count_decr & 0xFFFF) as u32, + ((count_decr >> 16) & 0xFFFF) as u32, + ((count_decr >> 32) & 0xFFFF) as u32, + ((count_decr >> 48) & 0xFFFF) as u32, + ]; + let zero_input = halves.into_iter().map(|half| 65535 - half).sum(); + lookups.push(BitwiseOperation::zero(zero_input)); + } + lookups +} + fn collect_bitwise_from_dma(dma_ops: &[dma::DmaOperation]) -> Vec { let mut lookups = Vec::with_capacity(dma_ops.len() * 13); for op in dma_ops { @@ -3019,6 +3227,9 @@ pub struct Traces { /// DMA memcpy table (eight-byte body rows plus byte tail rows). pub dma: TraceTable, + /// DMA memset table (eight-byte body rows plus byte tail rows). + pub dma_set: TraceTable, + /// MEMW_R register-only fast-path traces (split into chunks of max_rows::MEMW_R) pub memw_registers: Vec>, /// Local-to-global boundary table for continuation epochs. Empty unless the @@ -3063,6 +3274,8 @@ struct CollectedOps { ecdas_ops: Vec, // DMA memcpy rows (eight bytes per body row, byte tail, plus terminal rows). dma_ops: Vec, + // DMA memset rows (same schedule; one fill byte instead of eight value lanes). + dma_set_ops: Vec, } /// Chunk raw ops and generate one trace table per chunk. When `storage_mode` @@ -3118,6 +3331,7 @@ fn collect_all_ops( ecsm_ops: Vec, ecdas_ops: Vec, dma_ops: Vec, + dma_set_ops: Vec, register_state: &mut RegisterState, is_final: bool, ) -> CollectedOps { @@ -3261,6 +3475,7 @@ fn collect_all_ops( ecsm_ops, ecdas_ops, dma_ops, + dma_set_ops, } } @@ -3305,6 +3520,7 @@ fn build_traces( ecsm_ops, ecdas_ops, dma_ops, + dma_set_ops, } = ops; // ===================================================================== @@ -3323,6 +3539,17 @@ fn build_traces( .filter(|op| op.first) .map(|op| LtOperation::new(op.count, dma::DMA_MEMCPY_MAX_BYTES + 1, false)), ); + lt_ops.extend( + dma_set_ops + .iter() + .map(|op| LtOperation::new(op.count, 8, false)), + ); + lt_ops.extend(dma_set_ops.iter().filter(|op| op.first).flat_map(|op| { + [ + LtOperation::new(op.count, dma_set::DMA_MEMSET_MAX_BYTES + 1, false), + LtOperation::new(u64::from(op.fill), dma_set::DMA_MEMSET_MAX_FILL + 1, false), + ] + })); // ===================================================================== // PHASE 4: All → Bitwise lookups @@ -3389,6 +3616,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_dma(&dma_ops))), + Box::new(|h| h.add_ops(&collect_bitwise_from_dma_set(&dma_set_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_keccak(&keccak_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))), @@ -3679,6 +3907,7 @@ fn build_traces( let gen_ecsm = || ecsm::generate_ecsm_trace(&ecsm_ops); let gen_ecdas = || ecdas::generate_ecdas_trace(&ecdas_ops); let gen_dma = || dma::generate_dma_trace(&dma_ops); + let gen_dma_set = || dma_set::generate_dma_set_trace(&dma_set_ops); let (mut cpus_slot, mut memws_slot, mut memw_aligneds_slot, mut memw_registers_slot) = (None, None, None, None); @@ -3692,6 +3921,7 @@ fn build_traces( (None, None, None, None); let (mut ecsm_slot, mut ecdas_slot) = (None, None); let mut dma_slot = None; + let mut dma_set_slot = None; #[cfg(feature = "disk-spill")] let sequential = storage_mode == StorageMode::Disk || cfg!(not(feature = "parallel")); @@ -3734,6 +3964,7 @@ fn build_traces( spawn_into!(ecsm_slot, gen_ecsm); spawn_into!(ecdas_slot, gen_ecdas); spawn_into!(dma_slot, gen_dma); + spawn_into!(dma_set_slot, gen_dma_set); }); } else { cpus_slot = Some(gen_cpus()); @@ -3762,6 +3993,7 @@ fn build_traces( ecsm_slot = Some(gen_ecsm()); ecdas_slot = Some(gen_ecdas()); dma_slot = Some(gen_dma()); + dma_set_slot = Some(gen_dma_set()); } const PHASE5_RAN: &str = "phase 5 generation ran in one of the branches above"; @@ -3798,6 +4030,8 @@ fn build_traces( let ecdas_trace = ecdas_slot.expect(PHASE5_RAN); #[allow(unused_mut)] let mut dma_trace = dma_slot.expect(PHASE5_RAN); + #[allow(unused_mut)] + let mut dma_set_trace = dma_set_slot.expect(PHASE5_RAN); // Fixed-size and per-page tables aren't built through `chunk_and_generate`, // so spill them here before returning. @@ -3819,6 +4053,10 @@ fn build_traces( .main_table .spill_to_disk() .map_err(|e| Error::Prover(format!("disk-spill dma: {e}")))?; + dma_set_trace + .main_table + .spill_to_disk() + .map_err(|e| Error::Prover(format!("disk-spill dma_set: {e}")))?; register_trace .main_table .spill_to_disk() @@ -3870,6 +4108,7 @@ fn build_traces( ecsm: ecsm_trace, ecdas: ecdas_trace, dma: dma_trace, + dma_set: dma_set_trace, memw_registers, local_to_global, touched_memory_cells, @@ -3914,6 +4153,7 @@ pub struct TableLengths { pub branch_padded_rows: u64, pub commit_padded_rows: u64, pub dma_padded_rows: u64, + pub dma_set_padded_rows: u64, pub decode_rows: u64, pub unique_page_count: u64, pub cycle_count: u64, @@ -3954,6 +4194,7 @@ pub fn count_table_lengths( let mut branch_count = 0usize; let mut commit_count = 0usize; let mut dma_count = 0usize; + let mut dma_set_count = 0usize; let mut current_commit_index = 0u32; let partition_memw = |op: &MemwOperation, @@ -4065,6 +4306,26 @@ pub fn count_table_lengths( lt_count += dma_rows + 1; } + if cpu_op.ecall_dma_memset { + let rows = replay_dma_memset_for_sizing( + &cpu_op, + &mut memory_state, + &mut register_state, + |memw_op| { + partition_memw( + memw_op, + &mut memw_by_width, + &mut memw_aligned_count, + &mut memw_register_count, + ); + }, + ); + dma_set_count += rows; + // One LT per row pins the 1-vs-8-byte width; the first row adds two + // more (the chunk cap and the fill-byte bound). + lt_count += rows + 2; + } + // CPU-side per-instruction-kind counters (non-word; word → CPU32, B5b) let f = &cpu_op.decode.fields; if !f.word_instr && f.is_lt() { @@ -4128,6 +4389,10 @@ pub fn count_table_lengths( .checked_next_power_of_two() .unwrap_or(usize::MAX) .max(4) as u64, + dma_set_padded_rows: dma_set_count + .checked_next_power_of_two() + .unwrap_or(usize::MAX) + .max(4) as u64, decode_rows, unique_page_count, cycle_count, @@ -4154,6 +4419,7 @@ impl Traces { use super::decode::NUM_PRECOMPUTED_COLS as DECODE_PRECOMPUTED; use super::decode::cols::NUM_COLUMNS as DECODE_COLS; use super::dma::cols::NUM_COLUMNS as DMA_COLS; + use super::dma_set::cols::NUM_COLUMNS as DMA_SET_COLS; use super::dvrm::cols::NUM_COLUMNS as DVRM_COLS; use super::ecdas::cols::NUM_COLUMNS as ECDAS_COLS; use super::ecsm::cols::NUM_COLUMNS as ECSM_COLS; @@ -4198,6 +4464,7 @@ impl Traces { ecsm, ecdas, dma, + dma_set, memw_registers, eqs, bytewises, @@ -4266,6 +4533,7 @@ impl Traces { total += (ecsm.num_rows() * ECSM_COLS) as u64; total += (ecdas.num_rows() * ECDAS_COLS) as u64; total += (dma.num_rows() * DMA_COLS) as u64; + total += (dma_set.num_rows() * DMA_SET_COLS) as u64; total } @@ -4308,6 +4576,7 @@ impl Traces { let n_ecsm = aux_cols(super::ecsm::bus_interactions().len()); let n_ecdas = aux_cols(super::ecdas::bus_interactions().len()); let n_dma = aux_cols(super::dma::bus_interactions().len()); + let n_dma_set = aux_cols(super::dma_set::bus_interactions().len()); let Traces { cpus, @@ -4331,6 +4600,7 @@ impl Traces { ecsm, ecdas, dma, + dma_set, memw_registers, eqs, bytewises, @@ -4399,6 +4669,7 @@ impl Traces { total += (ecsm.num_rows() * n_ecsm) as u64; total += (ecdas.num_rows() * n_ecdas) as u64; total += (dma.num_rows() * n_dma) as u64; + total += (dma_set.num_rows() * n_dma_set) as u64; total } @@ -4673,6 +4944,7 @@ impl Traces { ecsm_ops, ecdas_ops, dma_ops, + dma_set_ops, ) = collect_ops_from_cpu(&cpu_ops, &mut memory_state, &mut register_state); #[cfg(feature = "instruments")] drop(__sp); @@ -4692,6 +4964,7 @@ impl Traces { ecsm_ops, ecdas_ops, dma_ops, + dma_set_ops, &mut register_state, is_final, ); @@ -4786,6 +5059,7 @@ impl Traces { ecsm_ops, ecdas_ops, dma_ops, + dma_set_ops, ) = collect_ops_from_cpu(&cpu_ops, &mut memory_state, &mut register_state); let ops = collect_all_ops( @@ -4801,6 +5075,7 @@ impl Traces { ecsm_ops, ecdas_ops, dma_ops, + dma_set_ops, &mut register_state, true, ); diff --git a/prover/src/tables/types.rs b/prover/src/tables/types.rs index 0d4a093ee..98c0910f5 100644 --- a/prover/src/tables/types.rs +++ b/prover/src/tables/types.rs @@ -362,6 +362,12 @@ pub enum BusId { /// copy. Only the first row receives the CPU's `Ecall`; the rest chain here. DmaNext = 29, + /// DMA memset streaming bus: each DMA_SET row sends + /// `(timestamp, dst_incr, count_decr, fill)` to the next row and receives + /// `(timestamp, dst, count, fill)` from the previous one. Separate from + /// [`BusId::DmaNext`] so a memcpy row can never consume a memset token. + DmaSetNext = 32, + // ========================================================================= // Continuations // ========================================================================= @@ -397,6 +403,7 @@ impl BusId { BusId::Ecdas => "Ecdas", BusId::Bit => "Bit", BusId::DmaNext => "DmaNext", + BusId::DmaSetNext => "DmaSetNext", BusId::GlobalMemory => "GlobalMemory", } } @@ -429,6 +436,7 @@ impl TryFrom for BusId { 27 => Ok(BusId::Cpu32), 28 => Ok(BusId::Ecdas), 29 => Ok(BusId::DmaNext), + 32 => Ok(BusId::DmaSetNext), 30 => Ok(BusId::Bit), 31 => Ok(BusId::GlobalMemory), other => Err(other), diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index dd7f97bc3..eab775764 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -58,6 +58,9 @@ use crate::tables::decode::{bus_interactions as decode_bus_interactions, cols as use crate::tables::dma::{ DmaConstraints, bus_interactions as dma_bus_interactions, cols as dma_cols, }; +use crate::tables::dma_set::{ + DmaSetConstraints, bus_interactions as dma_set_bus_interactions, cols as dma_set_cols, +}; use crate::tables::dvrm::{ DvrmConstraints, bus_interactions as dvrm_bus_interactions, cols as dvrm_cols, }; @@ -909,6 +912,18 @@ pub fn create_dma_air(proof_options: &ProofOptions) -> ConcreteVmAir ConcreteVmAir { + build_air( + dma_set_cols::NUM_COLUMNS, + dma_set_bus_interactions(), + proof_options, + 1, + DmaSetConstraints, + "DMA_SET", + ) +} + /// Create COMMIT AIR with constraints and bus interactions. pub fn create_commit_air(proof_options: &ProofOptions) -> ConcreteVmAir { build_air( diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index bdf94b65a..0a61b5046 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1231,6 +1231,49 @@ fn test_prove_dma_memcpy_rust_guest() { ); } +/// End-to-end memset: the guest exercises every row-schedule boundary (empty, +/// sub-tail, exact widths, the per-ecall cap, multi-chunk, a masked wide fill, +/// and an unaligned page-crossing destination), so a passing proof covers the +/// DMA_SET trace, its bus balance, and the fill-byte bound together. +#[test] +fn test_prove_dma_memset_cases_rust_guest() { + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("workspace root") + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/dma_memset_cases.elf")) + .expect("dma_memset_cases.elf not found — build its make target"); + + let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); + assert!( + verify_vm_minimal(&proof, &elf_bytes), + "DMA memset guest should verify" + ); + assert_eq!(proof.public_output, b"dma-memset-ok"); +} + +/// memmove rides the memcpy ecall unchanged. The interesting case is a forward +/// overlap longer than one 256-byte chunk: the stub must walk chunks backwards, +/// or an earlier chunk clobbers source bytes a later one still needs. +#[test] +fn test_prove_dma_memmove_cases_rust_guest() { + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("workspace root") + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/dma_memmove_cases.elf")) + .expect("dma_memmove_cases.elf not found — build its make target"); + + let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); + assert!( + verify_vm_minimal(&proof, &elf_bytes), + "DMA memmove guest should verify" + ); + assert_eq!(proof.public_output, b"dma-memmove-ok"); +} + #[test] fn test_prove_dma_memcpy_cases_rust_guest() { let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index a8a5a3415..f8df0fb7b 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -41,6 +41,10 @@ const DMA_MEMCPY_SYSCALL_NUMBER: usize = usize::MAX - 2; #[cfg(target_arch = "riscv64")] const DMA_MEMCPY_MAX_BYTES: usize = 256; +/// DMA memset syscall number. Must match the executor. +#[cfg(target_arch = "riscv64")] +const DMA_MEMSET_SYSCALL_NUMBER: usize = usize::MAX - 3; + /// No-op. The `Print` ecall (a7=1) has no receiver on the Ecall bus, so emitting /// it makes the LogUp bus unbalance and the proof fail to verify. Printing isn't /// needed in provable programs, so `print_string` does nothing on every target. @@ -241,6 +245,110 @@ memcpy: max_bytes = const DMA_MEMCPY_MAX_BYTES, ); +// --------------------------------------------------------------------------- +// DMA memmove symbol override +// +// Reuses the memcpy ecall unchanged — no new table, no new syscall. Each ecall +// already snapshots its whole source range before writing (all reads at T+1, +// all writes at T+2), so a single chunk has memmove semantics for free. +// +// Chunking is what breaks it: copying [0,256) -> [4,260) clobbers source bytes +// that a later forward chunk still needs. So when the destination starts inside +// the source range (src < dst < src+n) the chunks are walked from the END +// backwards; every chunk then reads bytes no earlier chunk has written yet. +// Otherwise (disjoint, or dst below src) forward chunking is already safe. +// --------------------------------------------------------------------------- + +#[cfg(target_arch = "riscv64")] +global_asm!( + r#" + .section .text.memmove,"ax",@progbits + .globl memmove + .type memmove,@function +memmove: + mv t0, a0 + beqz a2, .Ldma_memmove_done + bgeu a1, a0, .Ldma_memmove_fwd // src >= dst: forward is safe + add t2, a1, a2 + bgeu a0, t2, .Ldma_memmove_fwd // dst >= src+n: disjoint + // Overlapping with dst inside [src, src+n): walk chunks from the end. + add a0, a0, a2 + add a1, a1, a2 + mv t1, a2 +.Ldma_memmove_back_loop: + li a2, {max_bytes} + bgeu t1, a2, .Ldma_memmove_back_call + mv a2, t1 +.Ldma_memmove_back_call: + sub a0, a0, a2 + sub a1, a1, a2 + li a7, {syscall} + ecall + sub t1, t1, a2 + bnez t1, .Ldma_memmove_back_loop + j .Ldma_memmove_done +.Ldma_memmove_fwd: + mv t1, a2 +.Ldma_memmove_fwd_loop: + li a2, {max_bytes} + bgeu t1, a2, .Ldma_memmove_fwd_call + mv a2, t1 +.Ldma_memmove_fwd_call: + li a7, {syscall} + ecall + sub t1, t1, a2 + add a0, a0, a2 + add a1, a1, a2 + bnez t1, .Ldma_memmove_fwd_loop +.Ldma_memmove_done: + mv a0, t0 + ret + .size memmove, .-memmove +"#, + syscall = const DMA_MEMCPY_SYSCALL_NUMBER, + max_bytes = const DMA_MEMCPY_MAX_BYTES, +); + +// --------------------------------------------------------------------------- +// DMA memset symbol override +// +// Same shape as `memcpy` above: a strong assembly symbol that splits the fill +// into bounded DMA ecalls. `a1` carries the fill byte rather than a source +// address, so it is NOT advanced across chunks. The `andi` keeps only the low +// byte — C's `memset` takes an `int` but writes `(unsigned char)c`, and the +// executor rejects a wider value so the AIR can prove the byte bound. +// --------------------------------------------------------------------------- + +#[cfg(target_arch = "riscv64")] +global_asm!( + r#" + .section .text.memset,"ax",@progbits + .globl memset + .type memset,@function +memset: + mv t0, a0 + andi a1, a1, 255 + mv t1, a2 + beqz t1, .Ldma_memset_done +.Ldma_memset_loop: + li a2, {max_bytes} + bgeu t1, a2, .Ldma_memset_call + mv a2, t1 +.Ldma_memset_call: + li a7, {syscall} + ecall + sub t1, t1, a2 + add a0, a0, a2 + bnez t1, .Ldma_memset_loop +.Ldma_memset_done: + mv a0, t0 + ret + .size memset, .-memset +"#, + syscall = const DMA_MEMSET_SYSCALL_NUMBER, + max_bytes = const DMA_MEMCPY_MAX_BYTES, +); + // ============================================================================= // Stub implementations for unsupported std functions // These functions are required by Rust's std zkvm module but are not supported