diff --git a/forc-test/src/ecal.rs b/forc-test/src/ecal.rs index 23259ff3d1e..d55379b005e 100644 --- a/forc-test/src/ecal.rs +++ b/forc-test/src/ecal.rs @@ -3,37 +3,56 @@ use fuel_vm::{ prelude::{Interpreter, RegId}, }; -// ssize_t write(int fd, const void buf[.count], size_t count); +/// Syscall IDs for forc-test VM operations pub const WRITE_SYSCALL: u64 = 1000; pub const FFLUSH_SYSCALL: u64 = 1001; +pub const RANDOM_SYSCALL: u64 = 1002; +pub const RANDOM_SEEDED_SYSCALL: u64 = 1003; +/// Syscall types that can be captured and applied during VM execution #[derive(Debug, Clone)] pub enum Syscall { + /// Write bytes to a file descriptor Write { fd: u64, bytes: Vec }, + /// Flush a file descriptor Fflush { fd: u64 }, + /// Generate random bytes (non-deterministic) + Random { + dest_addr: u64, + count: u64, + bytes: Vec, + }, + /// Generate random bytes from a seed (deterministic) + RandomSeeded { + dest_addr: u64, + count: u64, + seed: u64, + bytes: Vec, + }, + /// Unknown syscall with raw register values Unknown { ra: u64, rb: u64, rc: u64, rd: u64 }, } impl Syscall { + /// Apply the syscall to the host system pub fn apply(&self) { use std::io::Write; use std::os::fd::FromRawFd; match self { Syscall::Write { fd, bytes } => { let s = std::str::from_utf8(bytes.as_slice()).unwrap(); - let mut f = unsafe { std::fs::File::from_raw_fd(*fd as i32) }; write!(&mut f, "{s}").unwrap(); - - // Don't close the fd - std::mem::forget(f); + std::mem::forget(f); // Prevent closing the fd } Syscall::Fflush { fd } => { let mut f = unsafe { std::fs::File::from_raw_fd(*fd as i32) }; let _ = f.flush(); - - // Don't close the fd - std::mem::forget(f); + std::mem::forget(f); // Prevent closing the fd + } + Syscall::Random { .. } | Syscall::RandomSeeded { .. } => { + // Memory writes happen directly in the ecal handler + // No additional application needed for captured syscalls } Syscall::Unknown { ra, rb, rc, rd } => { println!("Unknown ecal: {ra} {rb} {rc} {rd}"); @@ -42,20 +61,27 @@ impl Syscall { } } -/// Handle VM `ecal` as syscalls. +/// Handles VM environment calls (ecal) as syscalls with configurable capture and application. /// -/// The application of the syscalls can be turned off, -/// guaranteeing total isolation from the outside world. +/// This handler provides a flexible interface for intercepting and processing syscalls +/// during VM execution. Syscalls can be: +/// - **Applied**: Executed on the host system (e.g., writing to stdout) +/// - **Captured**: Recorded for inspection or replay +/// - **Both**: Applied immediately and saved for later analysis /// -/// Capture of the syscalls can be turned on, allowing -/// its application even after the VM is not running anymore. +/// # Supported Syscalls /// -/// Supported syscalls: -/// 1000 - write(fd: u64, buf: raw_ptr, count: u64) -> u64 +/// - `1000` - `write(fd: u64, buf: raw_ptr, count: u64)` - Write bytes to file descriptor +/// - `1001` - `fflush(fd: u64)` - Flush file descriptor +/// - `1002` - `random(dest: raw_ptr, count: u64)` - Generate non-deterministic random bytes +/// - `1003` - `random_seeded(dest: raw_ptr, count: u64, seed: u64)` - Generate deterministic random bytes #[derive(Debug, Clone)] pub struct EcalSyscallHandler { + /// Whether to apply syscalls to the host system pub apply: bool, + /// Whether to capture syscalls for later inspection pub capture: bool, + /// Vector of captured syscalls pub captured: Vec, } @@ -66,6 +92,7 @@ impl Default for EcalSyscallHandler { } impl EcalSyscallHandler { + /// Create a handler that only captures syscalls without applying them pub fn only_capturing() -> Self { Self { apply: false, @@ -74,6 +101,7 @@ impl EcalSyscallHandler { } } + /// Create a handler that only applies syscalls without capturing them pub fn only_applying() -> Self { Self { apply: true, @@ -82,6 +110,7 @@ impl EcalSyscallHandler { } } + /// Clear all captured syscalls pub fn clear(&mut self) { self.captured.clear(); } @@ -111,6 +140,42 @@ impl EcalHandler for EcalSyscallHandler { let fd = regs[b.to_u8() as usize]; Syscall::Fflush { fd } } + RANDOM_SYSCALL => { + use rand::Rng; + let dest_addr = regs[b.to_u8() as usize]; + let count = regs[c.to_u8() as usize]; + + let random_bytes: Vec = + (0..count).map(|_| rand::thread_rng().gen::()).collect(); + + let mem_slice = vm.memory_mut().write_noownerchecks(dest_addr, count)?; + mem_slice.copy_from_slice(&random_bytes); + + Syscall::Random { + dest_addr, + count, + bytes: random_bytes, + } + } + RANDOM_SEEDED_SYSCALL => { + use rand::{rngs::StdRng, Rng, SeedableRng}; + let dest_addr = regs[b.to_u8() as usize]; + let count = regs[c.to_u8() as usize]; + let seed = regs[d.to_u8() as usize]; + + let mut rng = StdRng::seed_from_u64(seed); + let random_bytes: Vec = (0..count).map(|_| rng.gen::()).collect(); + + let mem_slice = vm.memory_mut().write_noownerchecks(dest_addr, count)?; + mem_slice.copy_from_slice(&random_bytes); + + Syscall::RandomSeeded { + dest_addr, + count, + seed, + bytes: random_bytes, + } + } _ => { let ra = regs[a.to_u8() as usize]; let rb = regs[b.to_u8() as usize]; @@ -135,7 +200,7 @@ impl EcalHandler for EcalSyscallHandler { } #[test] -fn ok_capture_ecals() { +fn test_write_syscall_capture() { use fuel_vm::fuel_asm::op::*; use fuel_vm::prelude::*; let vm: Interpreter = <_>::default(); @@ -152,7 +217,6 @@ fn ok_capture_ecals() { .into_iter() .collect(); - // Execute transaction let mut client = MemoryClient::from_txtor(vm.into()); let tx = TransactionBuilder::script(script, script_data) .script_gas_limit(1_000_000) @@ -162,7 +226,6 @@ fn ok_capture_ecals() { .expect("failed to generate a checked tx"); let _ = client.transact(tx); - // Verify let t: Transactor = client.into(); let syscalls = t.interpreter().ecal_state().captured.clone(); @@ -171,3 +234,156 @@ fn ok_capture_ecals() { matches!(&syscalls[0], Syscall::Write { fd: 1, bytes } if std::str::from_utf8(bytes).unwrap() == test_input) ); } + +#[test] +fn test_random_syscall_generates_bytes() { + use fuel_vm::fuel_asm::op::*; + use fuel_vm::prelude::*; + let vm: Interpreter = <_>::default(); + + let num_bytes = 32u64; + let script = vec![ + // Allocate memory on the heap + movi(0x10, 1024), // heap pointer + movi(0x11, num_bytes as u32), // number of random bytes + movi(0x20, RANDOM_SYSCALL as u32), // syscall number + ecal(0x20, 0x10, 0x11, RegId::ZERO), // call random syscall + ret(RegId::ONE), + ] + .into_iter() + .collect(); + + // Execute transaction + let mut client = MemoryClient::from_txtor(vm.into()); + let tx = TransactionBuilder::script(script, vec![]) + .script_gas_limit(1_000_000) + .add_fee_input() + .finalize() + .into_checked(Default::default(), &ConsensusParameters::standard()) + .expect("failed to generate a checked tx"); + let _ = client.transact(tx); + + // Verify + let t: Transactor = client.into(); + let syscalls = t.interpreter().ecal_state().captured.clone(); + + assert_eq!(syscalls.len(), 1); + assert!( + matches!(&syscalls[0], Syscall::Random { dest_addr: 1024, count, bytes } + if *count == num_bytes && bytes.len() == num_bytes as usize) + ); +} + +#[test] +fn test_random_seeded_syscall_generates_deterministic_bytes() { + use fuel_vm::fuel_asm::op::*; + use fuel_vm::prelude::*; + let vm: Interpreter = <_>::default(); + + let num_bytes = 32u64; + let seed = 12345u64; + let script = vec![ + // Allocate memory on the heap + movi(0x10, 1024), // heap pointer + movi(0x11, num_bytes as u32), // number of random bytes + movi(0x12, seed as u32), // seed value + movi(0x20, RANDOM_SEEDED_SYSCALL as u32), // syscall number + ecal(0x20, 0x10, 0x11, 0x12), // call random_seeded syscall + ret(RegId::ONE), + ] + .into_iter() + .collect(); + + let mut client = MemoryClient::from_txtor(vm.into()); + let tx = TransactionBuilder::script(script, vec![]) + .script_gas_limit(1_000_000) + .add_fee_input() + .finalize() + .into_checked(Default::default(), &ConsensusParameters::standard()) + .expect("failed to generate a checked tx"); + let _ = client.transact(tx); + + let t: Transactor = client.into(); + let syscalls = t.interpreter().ecal_state().captured.clone(); + + assert_eq!(syscalls.len(), 1); + assert!( + matches!(&syscalls[0], Syscall::RandomSeeded { dest_addr: 1024, count, seed: s, bytes } + if *count == num_bytes && *s == seed && bytes.len() == num_bytes as usize) + ); +} + +#[test] +fn test_random_seeded_syscall_produces_same_output_with_same_seed() { + use fuel_vm::fuel_asm::op::*; + use fuel_vm::prelude::*; + + let num_bytes = 32u64; + let seed = 42u64; + + // First execution + let vm1: Interpreter = + <_>::default(); + let script1 = vec![ + movi(0x10, 1024), + movi(0x11, num_bytes as u32), + movi(0x12, seed as u32), + movi(0x20, RANDOM_SEEDED_SYSCALL as u32), + ecal(0x20, 0x10, 0x11, 0x12), + ret(RegId::ONE), + ] + .into_iter() + .collect(); + + let mut client1 = MemoryClient::from_txtor(vm1.into()); + let tx1 = TransactionBuilder::script(script1, vec![]) + .script_gas_limit(1_000_000) + .add_fee_input() + .finalize() + .into_checked(Default::default(), &ConsensusParameters::standard()) + .expect("failed to generate a checked tx"); + let _ = client1.transact(tx1); + + let t1: Transactor = client1.into(); + let syscalls1 = t1.interpreter().ecal_state().captured.clone(); + + // Second execution with same seed + let vm2: Interpreter = + <_>::default(); + let script2 = vec![ + movi(0x10, 1024), + movi(0x11, num_bytes as u32), + movi(0x12, seed as u32), + movi(0x20, RANDOM_SEEDED_SYSCALL as u32), + ecal(0x20, 0x10, 0x11, 0x12), + ret(RegId::ONE), + ] + .into_iter() + .collect(); + + let mut client2 = MemoryClient::from_txtor(vm2.into()); + let tx2 = TransactionBuilder::script(script2, vec![]) + .script_gas_limit(1_000_000) + .add_fee_input() + .finalize() + .into_checked(Default::default(), &ConsensusParameters::standard()) + .expect("failed to generate a checked tx"); + let _ = client2.transact(tx2); + + let t2: Transactor = client2.into(); + let syscalls2 = t2.interpreter().ecal_state().captured.clone(); + + // Verify both produce the same random bytes + if let ( + Syscall::RandomSeeded { bytes: bytes1, .. }, + Syscall::RandomSeeded { bytes: bytes2, .. }, + ) = (&syscalls1[0], &syscalls2[0]) + { + assert_eq!( + bytes1, bytes2, + "Seeded random should produce deterministic results" + ); + } else { + panic!("Expected RandomSeeded syscalls"); + } +} diff --git a/forc-test/sway-test/.gitignore b/forc-test/sway-test/.gitignore new file mode 100644 index 00000000000..77d3844f58c --- /dev/null +++ b/forc-test/sway-test/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/forc-test/sway-test/Forc.lock b/forc-test/sway-test/Forc.lock new file mode 100644 index 00000000000..82977b76460 --- /dev/null +++ b/forc-test/sway-test/Forc.lock @@ -0,0 +1,8 @@ +[[package]] +name = "std" +source = "path+from-root-6727D9AAD62BD14B" + +[[package]] +name = "sway_test" +source = "member" +dependencies = ["std"] diff --git a/forc-test/sway-test/Forc.toml b/forc-test/sway-test/Forc.toml new file mode 100644 index 00000000000..6af7068f744 --- /dev/null +++ b/forc-test/sway-test/Forc.toml @@ -0,0 +1,8 @@ +[project] +authors=["Fuel Labs "] +entry = "lib.sw" +license = "Apache-2.0" +name = "sway_test" + +[dependencies] +std = { path = "../../sway-lib-std" } diff --git a/forc-test/sway-test/README.md b/forc-test/sway-test/README.md new file mode 100644 index 00000000000..2ca5988c73f --- /dev/null +++ b/forc-test/sway-test/README.md @@ -0,0 +1,131 @@ +# sway_test + +A property-based testing and fuzzing library for Sway. + +## Overview + +`sway_test` provides a simple and powerful fuzzing framework that works with any Sway type automatically. No trait implementations or manual configuration required. + +## Features + +- **Universal Type Support** - Fuzz any struct, enum, or primitive type +- **Deterministic Testing** - Reproducible test runs with seed-based randomness +- **Zero Configuration** - No trait implementations needed +- **Memory-Safe** - Leverages Sway's type system for safe fuzzing + +## Installation + +Add to your `Forc.toml`: + +```toml +[dependencies] +sway_test = { path = "path/to/sway_test" } +``` + +## Usage + +### Basic Fuzzing + +```sway +use sway_test::*; + +struct Transaction { + from: u64, + to: u64, + amount: u32, +} + +#[test] +fn test_transaction_processing() { + let mut fuzzer = Fuzzer::::new(100); + let mut i = 0; + + while i < 100 { + let tx = fuzzer.next(); + process_transaction(tx); + i += 1; + } +} +``` + +### Deterministic Fuzzing + +```sway +#[test] +fn test_with_seed() { + let config = FuzzConfig::new(50).with_seed(42); + let mut fuzzer = Fuzzer::::with_config(config); + + // Same seed always produces same sequence + let value = fuzzer.next(); +} +``` + +### Direct Value Generation + +```sway +// Generate a single fuzzed value +let value: MyStruct = fuzz_any(42); +``` + +## API + +### Core Types + +#### `Fuzzer` + +Iterator-like fuzzer for generating random values of type `T`. + +**Methods:** +- `new(iterations: u64) -> Self` - Create fuzzer with iteration count +- `with_config(config: FuzzConfig) -> Self` - Create with custom configuration +- `next() -> T` - Generate next fuzzed value +- `has_next() -> bool` - Check if more values available + +#### `FuzzConfig` + +Configuration for fuzzing behavior. + +**Methods:** +- `new(iterations: u64) -> Self` - Create configuration +- `with_seed(seed: u64) -> Self` - Set deterministic seed + +### Functions + +#### `fuzz_any(seed: u64) -> T` + +Generate a single fuzzed value of any type. + +#### Random Number Generation + +```sway +// Non-deterministic +random_u64() -> u64 +random_u32() -> u32 +random_u8() -> u8 + +// Deterministic (seeded) +random_u64_seeded(seed: u64) -> u64 +random_u32_seeded(seed: u64) -> u32 +random_u8_seeded(seed: u64) -> u8 +``` + +## How It Works + +The library uses memory-level fuzzing to generate random values: + +1. Determines type size using `__size_of::()` +2. Fills memory with random bytes via ecal syscalls +3. Returns the initialized value + +This approach works for any type without requiring trait implementations. + +## Testing + +```bash +forc test +``` + +## License + +Apache-2.0 diff --git a/forc-test/sway-test/src/fuzz.sw b/forc-test/sway-test/src/fuzz.sw new file mode 100644 index 00000000000..f4fbee80644 --- /dev/null +++ b/forc-test/sway-test/src/fuzz.sw @@ -0,0 +1,161 @@ +library; + +use ::random::*; + +/// Generate a fuzzed value of any type by filling its memory with random bytes. +/// +/// This function uses memory-level fuzzing to create random instances of any Sway type +/// without requiring trait implementations. It leverages the VM's random syscall to fill +/// the type's memory representation with deterministic random bytes based on the seed. +/// +/// # Type Parameters +/// * `T` - Any Sway type to generate a random value for +/// +/// # Arguments +/// * `seed` - Seed value for reproducible random generation +/// +/// # Returns +/// A random instance of type `T` with all fields filled with random data +/// +/// # Examples +/// ```sway +/// // Generate random primitive +/// let random_number: u64 = fuzz_any(42); +/// +/// // Generate random struct +/// struct MyStruct { a: u64, b: u32 } +/// let random_struct: MyStruct = fuzz_any(100); +/// +/// // Same seed produces same value +/// let val1: u64 = fuzz_any(42); +/// let val2: u64 = fuzz_any(42); +/// assert(val1 == val2); +/// ``` +pub fn fuzz_any(seed: u64) -> T { + let size_in_bytes = __size_of::(); + + let mut value: T = asm(size: size_in_bytes) { + size: T + }; + + let ptr = asm(r1: __addr_of(value)) { r1: u64 }; + random_bytes_seeded(ptr, size_in_bytes, seed); + + value +} + +/// Configuration for fuzzing behavior. +pub struct FuzzConfig { + /// Number of fuzz iterations to run + pub iterations: u64, + /// Base seed value for deterministic generation (defaults to 0) + pub base_seed: u64, +} + +impl FuzzConfig { + /// Create a new fuzzing configuration with default base seed of 0. + /// + /// # Arguments + /// * `iterations` - Number of random values to generate + /// + /// # Returns + /// A new `FuzzConfig` instance + pub fn new(iterations: u64) -> Self { + Self { + iterations, + base_seed: 0, + } + } + + /// Set a custom base seed for deterministic fuzzing. + /// + /// # Arguments + /// * `seed` - Base seed value (each iteration uses base_seed + iteration_number) + /// + /// # Returns + /// Updated `FuzzConfig` with the specified seed + pub fn with_seed(self, seed: u64) -> Self { + Self { + iterations: self.iterations, + base_seed: seed, + } + } +} + +/// Iterator-like fuzzer for generating sequences of random test inputs. +/// +/// The fuzzer automatically generates random values of any type without requiring +/// trait implementations. Each call to `next()` produces a new random value using +/// an incrementing seed (base_seed + iteration_count). +/// +/// # Type Parameters +/// * `T` - Any Sway type to generate random values for +/// +/// # Examples +/// ```sway +/// // Create fuzzer with 100 iterations +/// let mut fuzzer = Fuzzer::::new(100); +/// while fuzzer.has_next() { +/// let value = fuzzer.next(); +/// // Test with random value +/// } +/// +/// // Deterministic fuzzing with custom seed +/// let config = FuzzConfig::new(50).with_seed(12345); +/// let mut fuzzer = Fuzzer::::with_config(config); +/// ``` +pub struct Fuzzer { + config: FuzzConfig, + current: u64, +} + +impl Fuzzer { + /// Create a new fuzzer with the specified number of iterations. + /// + /// # Arguments + /// * `iterations` - Number of random values to generate + /// + /// # Returns + /// A new `Fuzzer` instance with base seed 0 + pub fn new(iterations: u64) -> Self { + Self { + config: FuzzConfig::new(iterations), + current: 0, + } + } + + /// Create a fuzzer with custom configuration. + /// + /// # Arguments + /// * `config` - Fuzzing configuration with iterations and base seed + /// + /// # Returns + /// A new `Fuzzer` instance with the specified configuration + pub fn with_config(config: FuzzConfig) -> Self { + Self { + config, + current: 0, + } + } + + /// Check if there are more values to generate. + /// + /// # Returns + /// `true` if more iterations remain, `false` otherwise + pub fn has_next(self) -> bool { + self.current < self.config.iterations + } + + /// Generate the next random value in the sequence. + /// + /// Each call increments the iteration counter and generates a new random value + /// using seed = base_seed + current_iteration. + /// + /// # Returns + /// A random value of type `T` + pub fn next(ref mut self) -> T { + let seed = self.config.base_seed + self.current; + self.current += 1; + fuzz_any::(seed) + } +} diff --git a/forc-test/sway-test/src/lib.sw b/forc-test/sway-test/src/lib.sw new file mode 100644 index 00000000000..76d3b1daed9 --- /dev/null +++ b/forc-test/sway-test/src/lib.sw @@ -0,0 +1,212 @@ +library; + +pub mod random; +pub mod fuzz; + +use fuzz::*; + +/// Test struct with mixed field types +pub struct MyStruct { + pub field1: u8, + pub field2: bool, + pub field3: u32, +} + +/// Test struct with multiple u64 fields for range validation +pub struct ComplexStruct { + pub a: u64, + pub b: u64, + pub c: u32, + pub d: u8, +} + +/// Simple enum with unit variants +pub enum SimpleEnum { + A: (), + B: (), + C: (), +} + +/// Enum with primitive type variants +pub enum PrimitiveEnum { + Value: u64, + Flag: bool, + Count: u32, +} + +/// Enum with nested complex variants +pub enum ComplexEnum { + Simple: SimpleEnum, + Struct: MyStruct, + Primitive: u64, +} + +#[test] +fn test_u64_fuzzing_generates_varied_values() { + let mut fuzzer = Fuzzer::::new(100); + let mut i = 0; + let mut has_zero = false; + let mut has_non_zero = false; + + while i < 100 { + let value = fuzzer.next(); + if value == 0 { + has_zero = true; + } else { + has_non_zero = true; + } + i += 1; + } + + assert(has_zero || has_non_zero); +} + +#[test] +fn test_u32_fuzzing_without_panics() { + let mut fuzzer = Fuzzer::::new(100); + let mut i = 0; + + while i < 100 { + let _value = fuzzer.next(); + i += 1; + } +} + +#[test] +fn test_deterministic_fuzzing() { + let config1 = FuzzConfig::new(5).with_seed(12345); + let mut fuzzer1 = Fuzzer::::with_config(config1); + let mut values1: [u64; 5] = [0; 5]; + let mut i = 0; + + while i < 5 { + values1[i] = fuzzer1.next(); + i += 1; + } + + let config2 = FuzzConfig::new(5).with_seed(12345); + let mut fuzzer2 = Fuzzer::::with_config(config2); + let mut values2: [u64; 5] = [0; 5]; + i = 0; + + while i < 5 { + values2[i] = fuzzer2.next(); + i += 1; + } + + assert(values1[0] == values2[0]); + assert(values1[1] == values2[1]); + assert(values1[2] == values2[2]); + assert(values1[3] == values2[3]); + assert(values1[4] == values2[4]); +} + +#[test] +fn test_different_seeds_produce_different_values() { + let mut values: [u64; 3] = [0; 3]; + values[0] = fuzz_any(1); + values[1] = fuzz_any(2); + values[2] = fuzz_any(3); + + let all_same = values[0] == values[1] && values[1] == values[2]; + assert(!all_same); +} + +#[test] +fn test_bool_distribution() { + let mut fuzzer = Fuzzer::::with_config(FuzzConfig::new(1000).with_seed(42)); + let mut true_count = 0; + let mut false_count = 0; + let mut i = 0; + + while i < 1000 { + let value = fuzzer.next(); + if value { + true_count += 1; + } else { + false_count += 1; + } + i += 1; + } + + assert(true_count > 0); + assert(false_count > 0); + assert(true_count + false_count == 1000); +} + +#[test] +fn test_struct_field_independence() { + let s1: MyStruct = fuzz_any(100); + let s2: MyStruct = fuzz_any(101); + + let all_same = s1.field1 == s2.field1 && s1.field2 == s2.field2 && s1.field3 == s2.field3; + assert(!all_same); +} + +#[test] +fn test_complex_struct_field_ranges() { + let mut fuzzer = Fuzzer::::with_config(FuzzConfig::new(20).with_seed(777)); + let mut i = 0; + let mut has_non_zero_a = false; + let mut has_non_zero_b = false; + + while i < 20 { + let s = fuzzer.next(); + assert(s.d <= 255); + + if s.a != 0 { + has_non_zero_a = true; + } + if s.b != 0 { + has_non_zero_b = true; + } + i += 1; + } + + assert(has_non_zero_a); + assert(has_non_zero_b); +} + +#[test] +fn test_simple_enum_fuzzing() { + let mut fuzzer = Fuzzer::::new(30); + let mut i = 0; + + while i < 30 { + let _e = fuzzer.next(); + i += 1; + } +} + +#[test] +fn test_primitive_enum_fuzzing() { + let mut fuzzer = Fuzzer::::new(40); + let mut i = 0; + + while i < 40 { + let _e = fuzzer.next(); + i += 1; + } +} + +#[test] +fn test_complex_enum_fuzzing() { + let mut fuzzer = Fuzzer::::new(25); + let mut i = 0; + + while i < 25 { + let _e = fuzzer.next(); + i += 1; + } +} + +#[test] +fn test_fuzz_any_determinism() { + let s1: ComplexStruct = fuzz_any(42); + let s2: ComplexStruct = fuzz_any(42); + + assert(s1.a == s2.a); + assert(s1.b == s2.b); + assert(s1.c == s2.c); + assert(s1.d == s2.d); +} diff --git a/forc-test/sway-test/src/random.sw b/forc-test/sway-test/src/random.sw new file mode 100644 index 00000000000..5973eabc7ac --- /dev/null +++ b/forc-test/sway-test/src/random.sw @@ -0,0 +1,104 @@ +library; + +/// Syscall ID for non-deterministic random generation +const RANDOM_SYSCALL: u64 = 1002; +/// Syscall ID for deterministic seeded random generation +const RANDOM_SEEDED_SYSCALL: u64 = 1003; + +/// Fill a buffer with random bytes using a non-deterministic source. +/// +/// # Arguments +/// * `buffer_ptr` - Memory address of the buffer to fill +/// * `count` - Number of bytes to generate +pub fn random_bytes(buffer_ptr: u64, count: u64) { + asm(r1: RANDOM_SYSCALL, r2: buffer_ptr, r3: count) { + ecal r1 r2 r3 zero; + } +} + +/// Fill a buffer with random bytes using a deterministic seeded source. +/// +/// # Arguments +/// * `buffer_ptr` - Memory address of the buffer to fill +/// * `count` - Number of bytes to generate +/// * `seed` - Seed value for reproducible generation +pub fn random_bytes_seeded(buffer_ptr: u64, count: u64, seed: u64) { + asm(r1: RANDOM_SEEDED_SYSCALL, r2: buffer_ptr, r3: count, r4: seed) { + ecal r1 r2 r3 r4; + } +} + +/// Generate a random `u64` value using a non-deterministic source. +/// +/// # Returns +/// A random 64-bit unsigned integer +pub fn random_u64() -> u64 { + let mut buffer: u64 = 0; + let buffer_ptr = asm(r1: buffer) { r1: u64 }; + random_bytes(buffer_ptr, 8); + buffer +} + +/// Generate a random `u64` value using a deterministic seeded source. +/// +/// # Arguments +/// * `seed` - Seed value for reproducible generation +/// +/// # Returns +/// A deterministic 64-bit unsigned integer based on the seed +pub fn random_u64_seeded(seed: u64) -> u64 { + let mut buffer: u64 = 0; + let buffer_ptr = asm(r1: buffer) { r1: u64 }; + random_bytes_seeded(buffer_ptr, 8, seed); + buffer +} + +/// Generate a random `u32` value using a non-deterministic source. +/// +/// # Returns +/// A random 32-bit unsigned integer +pub fn random_u32() -> u32 { + let mut buffer: u32 = 0; + let buffer_ptr = asm(r1: buffer) { r1: u64 }; + random_bytes(buffer_ptr, 4); + buffer +} + +/// Generate a random `u32` value using a deterministic seeded source. +/// +/// # Arguments +/// * `seed` - Seed value for reproducible generation +/// +/// # Returns +/// A deterministic 32-bit unsigned integer based on the seed +pub fn random_u32_seeded(seed: u64) -> u32 { + let mut buffer: u32 = 0; + let buffer_ptr = asm(r1: buffer) { r1: u64 }; + random_bytes_seeded(buffer_ptr, 4, seed); + buffer +} + +/// Generate a random `u8` value using a non-deterministic source. +/// +/// # Returns +/// A random 8-bit unsigned integer +pub fn random_u8() -> u8 { + let mut buffer: u8 = 0; + let buffer_ptr = asm(r1: buffer) { r1: u64 }; + random_bytes(buffer_ptr, 1); + buffer +} + +/// Generate a random `u8` value using a deterministic seeded source. +/// +/// # Arguments +/// * `seed` - Seed value for reproducible generation +/// +/// # Returns +/// A deterministic 8-bit unsigned integer based on the seed +pub fn random_u8_seeded(seed: u64) -> u8 { + let mut buffer: u8 = 0; + let buffer_ptr = asm(r1: buffer) { r1: u64 }; + random_bytes_seeded(buffer_ptr, 1, seed); + buffer +} diff --git a/test/src/e2e_vm_tests/mod.rs b/test/src/e2e_vm_tests/mod.rs index 87fd2360a36..c8562d7416d 100644 --- a/test/src/e2e_vm_tests/mod.rs +++ b/test/src/e2e_vm_tests/mod.rs @@ -421,6 +421,8 @@ impl TestContext { output.push_str(s); } Syscall::Fflush { .. } => {} + Syscall::Random { .. } => {} + Syscall::RandomSeeded { .. } => {} Syscall::Unknown { ra, rb, rc, rd } => { let _ = writeln!(output, "Unknown ecal: {ra} {rb} {rc} {rd}"); }