diff --git a/Cargo.lock b/Cargo.lock index a5a6360b..32c4fb30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,12 +38,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.21" @@ -426,6 +441,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "castaway" version = "0.2.4" @@ -493,6 +514,33 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b4b0fc281743d80256607bd65e8beedc42cb0787ea119c85b81b4c0eab85e5f" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -695,6 +743,66 @@ dependencies = [ "serde_json", ] +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.28.1" @@ -738,6 +846,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "ctor" version = "0.6.1" @@ -1158,6 +1272,7 @@ name = "fuzzamoto-ir" version = "0.1.0" dependencies = [ "bitcoin", + "criterion", "fuzzamoto", "log", "murmurs", @@ -1180,13 +1295,16 @@ dependencies = [ "libafl_nyx", "log", "nix 0.30.1", + "num-traits", "postcard", "rand 0.8.5", "rangemap", "readonly", + "regex", "reqwest", "serde", "serde_json", + "strum 0.27.2", "typed-builder 0.20.1", "vergen", ] @@ -1282,6 +1400,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -2182,6 +2311,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl" version = "0.10.75" @@ -2236,6 +2371,16 @@ dependencies = [ "serde", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2289,6 +2434,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -2484,12 +2657,32 @@ dependencies = [ "itertools", "lru", "paste", - "strum", + "strum 0.26.3", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.0", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "readonly" version = "0.2.13" @@ -2676,6 +2869,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scc" version = "2.4.0" @@ -3090,7 +3292,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -3106,6 +3317,18 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "subprocess" version = "0.2.9" @@ -3343,6 +3566,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" version = "1.48.0" @@ -3658,6 +3891,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3791,6 +4034,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Dockerfile b/Dockerfile index 3ad3a295..96a9d01a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -126,6 +126,7 @@ COPY ./fuzzamoto-cli/src/ src/ WORKDIR /fuzzamoto/fuzzamoto-ir COPY ./fuzzamoto-ir/Cargo.toml . COPY ./fuzzamoto-ir/src/ src/ +COPY ./fuzzamoto-ir/benches/ benches/ WORKDIR /fuzzamoto/fuzzamoto-libafl COPY ./fuzzamoto-libafl/Cargo.toml . diff --git a/Dockerfile.coverage b/Dockerfile.coverage index 36d678ae..8c913094 100644 --- a/Dockerfile.coverage +++ b/Dockerfile.coverage @@ -82,6 +82,7 @@ COPY ./fuzzamoto-cli/src/ src/ WORKDIR /fuzzamoto/fuzzamoto-ir COPY ./fuzzamoto-ir/Cargo.toml . COPY ./fuzzamoto-ir/src/ src/ +COPY ./fuzzamoto-ir/benches/ benches/ WORKDIR /fuzzamoto/fuzzamoto-libafl COPY ./fuzzamoto-libafl/Cargo.toml . diff --git a/fuzzamoto-ir/Cargo.toml b/fuzzamoto-ir/Cargo.toml index 56ecda33..94e7008c 100644 --- a/fuzzamoto-ir/Cargo.toml +++ b/fuzzamoto-ir/Cargo.toml @@ -24,3 +24,10 @@ serde = { version = "1.0.197", features = ["derive"] } postcard = { version = "1.1.1", features = ["alloc"], default-features = false } log = "0.4.27" murmurs = { version = "1.0.0" } + +[dev-dependencies] +criterion = "0.8" + +[[bench]] +name = "builder_bench" +harness = false diff --git a/fuzzamoto-ir/benches/builder_bench.rs b/fuzzamoto-ir/benches/builder_bench.rs new file mode 100644 index 00000000..da20fd9b --- /dev/null +++ b/fuzzamoto-ir/benches/builder_bench.rs @@ -0,0 +1,60 @@ +use std::hint::black_box; + +use criterion::{Criterion, criterion_group, criterion_main}; +use fuzzamoto_ir::{Operation, Program, ProgramBuilder, ProgramContext, VariableLookup}; + +const NUM_INSTRUCTIONS: usize = 100_000; + +fn test_context() -> ProgramContext { + ProgramContext { + num_nodes: 2, + num_connections: 4, + timestamp: 1_700_000_000, + } +} + +fn create_test_program() -> Program { + let operations = [ + Operation::LoadBytes(vec![0u8; 32]), + Operation::LoadAmount(1000), + Operation::LoadTime(1_700_000_000), + Operation::LoadNode(0), + Operation::LoadConnection(0), + Operation::LoadSize(100), + Operation::LoadTxVersion(2), + Operation::LoadLockTime(0), + Operation::LoadSequence(0xffffffff), + Operation::LoadBlockHeight(100), + ]; + + let mut builder = ProgramBuilder::new(test_context()); + for i in 0..NUM_INSTRUCTIONS { + builder.force_append(vec![], &operations[i % operations.len()]); + } + builder.finalize().expect("Program should be valid") +} + +fn bench_large_program(c: &mut Criterion) { + let program = create_test_program(); + + c.bench_function("program_builder_100k", |b| { + b.iter(|| { + let rebuilt = ProgramBuilder::from_program(Program::unchecked_new( + program.context.clone(), + program.instructions.clone(), + )) + .unwrap(); + black_box(rebuilt.variable_count()) + }) + }); + + c.bench_function("variable_lookup_100k", |b| { + b.iter(|| { + let lookup = VariableLookup::from_instructions(&program.instructions); + black_box(lookup.variable_count()) + }) + }); +} + +criterion_group!(benches, bench_large_program); +criterion_main!(benches); diff --git a/fuzzamoto-ir/src/builder.rs b/fuzzamoto-ir/src/builder.rs index b77e3db8..f792de57 100644 --- a/fuzzamoto-ir/src/builder.rs +++ b/fuzzamoto-ir/src/builder.rs @@ -23,12 +23,116 @@ pub struct ScopedVariable { } /// Variable and its index -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct IndexedVariable { pub var: Variable, pub index: usize, } +/// Lightweight structure for variable lookups without full program building. +/// Built from instruction slices when only variable type information is needed. +pub struct VariableLookup { + variables: Vec, + active_scopes_set: HashSet, +} + +impl VariableLookup { + /// Build variable lookup state from instructions without validation. + /// Use when you only need variable type information, not a full program builder. + #[must_use] + pub fn from_instructions(instructions: &[Instruction]) -> Self { + let mut variables = Vec::with_capacity(instructions.len()); + let mut active_scopes: Vec = vec![1]; // Start with global scope + let mut active_scopes_set: HashSet = [1].into(); + let mut scope_counter = 1usize; + + for instruction in instructions { + if instruction.operation.is_block_end() + && let Some(exited) = active_scopes.pop() + { + active_scopes_set.remove(&exited); + } + + let current_scope_id = match instruction.operation { + Operation::Nop { .. } => 0usize, + _ => *active_scopes.last().unwrap_or(&1), + }; + + variables.extend( + instruction + .operation + .get_output_variables() + .iter() + .map(|v| ScopedVariable { + var: v.clone(), + scope_id: current_scope_id, + }), + ); + + if instruction.operation.is_block_begin() { + scope_counter += 1; + active_scopes.push(scope_counter); + active_scopes_set.insert(scope_counter); + } + + let scope_id = match instruction.operation { + Operation::Nop { .. } => 0usize, + _ => scope_counter, + }; + variables.extend( + instruction + .operation + .get_inner_output_variables() + .iter() + .map(|v| ScopedVariable { + var: v.clone(), + scope_id, + }), + ); + } + + Self { + variables, + active_scopes_set, + } + } + + #[must_use] + pub fn variable_count(&self) -> usize { + self.variables.len() + } + + #[must_use] + pub fn get_variable(&self, index: usize) -> Option { + let scoped_variable = self.variables.get(index)?; + if self.active_scopes_set.contains(&scoped_variable.scope_id) { + Some(IndexedVariable { + var: scoped_variable.var.clone(), + index, + }) + } else { + None + } + } + + /// Get a random variable of a given type that is in scope + pub fn get_random_variable( + &self, + rng: &mut R, + find: &Variable, + ) -> Option { + self.variables + .iter() + .enumerate() + .filter(|(_, sv)| self.active_scopes_set.contains(&sv.scope_id) && sv.var == *find) + .map(|(index, sv)| IndexedVariable { + var: sv.var.clone(), + index, + }) + .choose(rng) + } +} + pub struct ProgramBuilder { // Context of the program to be created context: ProgramContext, @@ -515,3 +619,420 @@ impl ProgramBuilder { all_utxos.choose_multiple(rng, n + 1) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ProgramContext; + + fn default_context() -> ProgramContext { + ProgramContext { + num_nodes: 2, + num_connections: 4, + timestamp: 0, + } + } + + /// Asserts that VariableLookup and ProgramBuilder produce identical variable state + /// for the given slice of instructions. + fn assert_lookup_matches_builder(instructions: &[Instruction], context: &ProgramContext) { + // Allow invalid programs (unclosed scopes) since we're testing partial slices + let mut from_builder = ProgramBuilder::new(context.clone()); + for instr in instructions { + let _ = from_builder.append(instr.clone()); + } + + let lookup = VariableLookup::from_instructions(instructions); + + assert_eq!( + from_builder.variable_count(), + lookup.variable_count(), + "Variable count mismatch for {} instructions", + instructions.len() + ); + + for i in 0..from_builder.variable_count() { + assert_eq!( + from_builder.get_variable(i), + lookup.get_variable(i), + "Variable mismatch at index {} for {} instructions", + i, + instructions.len() + ); + } + } + + /// Asserts that get_random_variable returns the same candidates from both implementations. + fn assert_random_variable_candidates_match( + instructions: &[Instruction], + context: &ProgramContext, + find: &Variable, + ) { + let mut from_builder = ProgramBuilder::new(context.clone()); + for instr in instructions { + let _ = from_builder.append(instr.clone()); + } + let lookup = VariableLookup::from_instructions(instructions); + + let builder_candidates: HashSet = (0..from_builder.variable_count()) + .filter_map(|i| from_builder.get_variable(i)) + .filter(|v| v.var == *find) + .map(|v| v.index) + .collect(); + + let lookup_candidates: HashSet = (0..lookup.variable_count()) + .filter_map(|i| lookup.get_variable(i)) + .filter(|v| v.var == *find) + .map(|v| v.index) + .collect(); + + assert_eq!( + builder_candidates, lookup_candidates, + "Random variable candidates mismatch for {:?}", + find + ); + } + + #[test] + fn variable_lookup_matches_simple_operations() { + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + for _ in 0..50 { + builder.force_append(vec![], &Operation::LoadAmount(100)); + } + let program = builder.finalize().unwrap(); + + for slice_idx in [10, 25, 49] { + assert_lookup_matches_builder(&program.instructions[..slice_idx], &context); + } + } + + #[test] + fn variable_lookup_matches_with_single_scope() { + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + + // Create some variables in global scope + builder.force_append(vec![], &Operation::LoadAmount(100)); // var 0 + builder.force_append(vec![], &Operation::LoadAmount(200)); // var 1 + + // Enter a scope (BeginBuildAddrList has no inputs and creates MutAddrList inner output) + builder.force_append(vec![], &Operation::BeginBuildAddrList); // var 2 (MutAddrList, inner) + + // Create variables inside the scope + builder.force_append(vec![], &Operation::LoadAmount(300)); // var 3 + + // Exit the scope + builder.force_append(vec![2], &Operation::EndBuildAddrList); // var 4 (ConstAddrList) + + // Create variable after scope exits + builder.force_append(vec![], &Operation::LoadAmount(400)); // var 5 + + let program = builder.finalize().unwrap(); + + // Test at various points: + // - Before scope: vars 0,1 in scope + // - Inside scope: vars 0,1,2,3 in scope + // - After scope closes: vars 0,1,4,5 in scope (2,3 out of scope) + for slice_idx in 1..=program.instructions.len() { + assert_lookup_matches_builder(&program.instructions[..slice_idx], &context); + } + } + + #[test] + fn variable_lookup_matches_with_nested_scopes() { + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + + // Global scope variable + builder.force_append(vec![], &Operation::LoadAmount(100)); // var 0 + + // Enter outer scope + builder.force_append(vec![], &Operation::BeginBuildInventory); // var 1 (MutInventory) + + // Variable in outer scope + builder.force_append(vec![], &Operation::LoadAmount(200)); // var 2 + + // Enter inner scope (BeginWitnessStack has no inputs) + builder.force_append(vec![], &Operation::BeginWitnessStack); // var 3 (MutWitnessStack) + + // Variable in inner scope + builder.force_append(vec![], &Operation::LoadAmount(300)); // var 4 + + // Exit inner scope + builder.force_append(vec![3], &Operation::EndWitnessStack); // var 5 (ConstWitnessStack) + + // Variable in outer scope after inner scope closed + builder.force_append(vec![], &Operation::LoadAmount(400)); // var 6 + + // Exit outer scope + builder.force_append(vec![1], &Operation::EndBuildInventory); // var 7 (ConstInventory) + + // Global scope variable after all scopes closed + builder.force_append(vec![], &Operation::LoadAmount(500)); // var 8 + + let program = builder.finalize().unwrap(); + + // Test at every instruction boundary + for slice_idx in 1..=program.instructions.len() { + assert_lookup_matches_builder(&program.instructions[..slice_idx], &context); + } + } + + #[test] + fn variable_lookup_handles_nop_operations() { + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + + // Create a regular variable + builder.force_append(vec![], &Operation::LoadAmount(100)); // var 0 (in scope) + + // Nop variables should always be out of scope (scope_id = 0) + builder.force_append( + vec![], + &Operation::Nop { + outputs: 2, + inner_outputs: 1, + }, + ); // vars 1,2 (outputs), var 3 (inner output) - all out of scope + + // Another regular variable + builder.force_append(vec![], &Operation::LoadAmount(200)); // var 4 (in scope) + + let instructions = builder.instructions.clone(); + + assert_lookup_matches_builder(&instructions, &context); + + // Verify specifically that Nop variables are out of scope + let lookup = VariableLookup::from_instructions(&instructions); + assert!(lookup.get_variable(0).is_some(), "var 0 should be in scope"); + assert!( + lookup.get_variable(1).is_none(), + "var 1 (Nop output) should be out of scope" + ); + assert!( + lookup.get_variable(2).is_none(), + "var 2 (Nop output) should be out of scope" + ); + assert!( + lookup.get_variable(3).is_none(), + "var 3 (Nop inner output) should be out of scope" + ); + assert!(lookup.get_variable(4).is_some(), "var 4 should be in scope"); + } + + #[test] + fn variable_lookup_handles_inner_output_variables() { + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + + // BeginBuildTx requires TxVersion and LockTime inputs, so let's use BeginBuildAddrList + // which has no inputs and produces a MutAddrList inner output + builder.force_append(vec![], &Operation::BeginBuildAddrList); // var 0 (MutAddrList, inner) + + // The inner output (MutAddrList) should be in the new scope + builder.force_append(vec![], &Operation::LoadAmount(100)); // var 1 + + // End the scope - the inner output should still be accessible until scope ends + builder.force_append(vec![0], &Operation::EndBuildAddrList); // var 2 (ConstAddrList) + + let program = builder.finalize().unwrap(); + + // Test at each instruction + for slice_idx in 1..=program.instructions.len() { + assert_lookup_matches_builder(&program.instructions[..slice_idx], &context); + } + + // After scope closes, the inner variable (0) should be out of scope + let lookup = VariableLookup::from_instructions(&program.instructions); + assert!( + lookup.get_variable(0).is_none(), + "MutAddrList should be out of scope after EndBuildAddrList" + ); + assert!( + lookup.get_variable(1).is_none(), + "var 1 should be out of scope after EndBuildAddrList" + ); + assert!( + lookup.get_variable(2).is_some(), + "ConstAddrList should be in scope" + ); + } + + #[test] + fn variable_lookup_get_random_variable_matches() { + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + + // Create multiple variables of the same type + builder.force_append(vec![], &Operation::LoadAmount(100)); // var 0 + builder.force_append(vec![], &Operation::LoadAmount(200)); // var 1 + + // Enter a scope + builder.force_append(vec![], &Operation::BeginBuildAddrList); // var 2 (MutAddrList) + + // More amounts inside scope + builder.force_append(vec![], &Operation::LoadAmount(300)); // var 3 + builder.force_append(vec![], &Operation::LoadAmount(400)); // var 4 + + // Exit scope + builder.force_append(vec![2], &Operation::EndBuildAddrList); // var 5 (ConstAddrList) + + // After scope, vars 3,4 are out of scope but 0,1 still in scope + let program = builder.finalize().unwrap(); + + // Test that both implementations agree on which ConstAmount variables are in scope + assert_random_variable_candidates_match( + &program.instructions, + &context, + &Variable::ConstAmount, + ); + + // Also test for ConstAddrList - should have exactly one (var 5) + assert_random_variable_candidates_match( + &program.instructions, + &context, + &Variable::ConstAddrList, + ); + + // MutAddrList should have none in scope (var 2 is in closed scope) + assert_random_variable_candidates_match( + &program.instructions, + &context, + &Variable::MutAddrList, + ); + } + + #[test] + fn variable_lookup_matches_with_multiple_variable_types() { + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + + // Mix of different variable types + builder.force_append(vec![], &Operation::LoadAmount(100)); // ConstAmount + builder.force_append(vec![], &Operation::LoadTxVersion(2)); // TxVersion + builder.force_append(vec![], &Operation::LoadLockTime(0)); // LockTime + builder.force_append(vec![], &Operation::LoadConnection(0)); // Connection + builder.force_append(vec![], &Operation::LoadNode(0)); // Node + builder.force_append(vec![], &Operation::LoadSequence(0xFFFFFFFF)); // Sequence + + // Enter scope with BeginBuildTx (requires TxVersion and LockTime) + builder.force_append(vec![1, 2], &Operation::BeginBuildTx); // MutTx (inner) + + builder.force_append(vec![], &Operation::LoadAmount(200)); // ConstAmount in scope + + let instructions = builder.instructions.clone(); + + // Test incrementally + for slice_idx in 1..=instructions.len() { + assert_lookup_matches_builder(&instructions[..slice_idx], &context); + } + } + + #[test] + fn variable_lookup_handles_unclosed_scopes() { + // Test behavior when scopes are not properly closed (partial program slices) + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + + builder.force_append(vec![], &Operation::LoadAmount(100)); // var 0 + builder.force_append(vec![], &Operation::BeginBuildAddrList); // var 1 (MutAddrList, inner) + builder.force_append(vec![], &Operation::LoadAmount(200)); // var 2 + + // Don't close the scope - this simulates a partial slice + let instructions = builder.instructions.clone(); + + // VariableLookup should handle this gracefully + let lookup = VariableLookup::from_instructions(&instructions); + + // var 0 should be in scope (global) + assert!(lookup.get_variable(0).is_some()); + // var 1 (MutAddrList) should be in scope (inner scope is still active) + assert!(lookup.get_variable(1).is_some()); + // var 2 should be in scope (inside still-open scope) + assert!(lookup.get_variable(2).is_some()); + } + + #[test] + fn variable_lookup_deeply_nested_scopes() { + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + + // Create a deeply nested scope structure + builder.force_append(vec![], &Operation::LoadAmount(0)); // var 0 (global) + + // Level 1 + builder.force_append(vec![], &Operation::BeginBuildInventory); // var 1 + builder.force_append(vec![], &Operation::LoadAmount(1)); // var 2 + + // Level 2 + builder.force_append(vec![], &Operation::BeginWitnessStack); // var 3 + builder.force_append(vec![], &Operation::LoadAmount(2)); // var 4 + + // Level 3 + builder.force_append(vec![], &Operation::BeginBuildAddrList); // var 5 + builder.force_append(vec![], &Operation::LoadAmount(3)); // var 6 + + // Close level 3 + builder.force_append(vec![5], &Operation::EndBuildAddrList); // var 7 + + // var 6 should now be out of scope, but vars 0-4 still in scope + builder.force_append(vec![], &Operation::LoadAmount(4)); // var 8 + + // Close level 2 + builder.force_append(vec![3], &Operation::EndWitnessStack); // var 9 + + // Close level 1 + builder.force_append(vec![1], &Operation::EndBuildInventory); // var 10 + + builder.force_append(vec![], &Operation::LoadAmount(5)); // var 11 (global) + + let program = builder.finalize().unwrap(); + + // Test at every point + for slice_idx in 1..=program.instructions.len() { + assert_lookup_matches_builder(&program.instructions[..slice_idx], &context); + } + } + + #[test] + fn variable_lookup_multiple_scopes_same_level() { + let context = default_context(); + let mut builder = ProgramBuilder::new(context.clone()); + + builder.force_append(vec![], &Operation::LoadAmount(100)); // var 0 + + // First scope + builder.force_append(vec![], &Operation::BeginBuildAddrList); // var 1 + builder.force_append(vec![], &Operation::LoadAmount(200)); // var 2 + builder.force_append(vec![1], &Operation::EndBuildAddrList); // var 3 + + // var 2 is now out of scope + + // Second scope at same level + builder.force_append(vec![], &Operation::BeginBuildInventory); // var 4 + builder.force_append(vec![], &Operation::LoadAmount(300)); // var 5 + builder.force_append(vec![4], &Operation::EndBuildInventory); // var 6 + + // var 5 is now out of scope + + builder.force_append(vec![], &Operation::LoadAmount(400)); // var 7 + + let program = builder.finalize().unwrap(); + + for slice_idx in 1..=program.instructions.len() { + assert_lookup_matches_builder(&program.instructions[..slice_idx], &context); + } + + // Final state: only vars 0, 3, 6, 7 should be in scope + let lookup = VariableLookup::from_instructions(&program.instructions); + assert!(lookup.get_variable(0).is_some()); + assert!(lookup.get_variable(1).is_none()); // MutAddrList from closed scope + assert!(lookup.get_variable(2).is_none()); // Inside closed scope + assert!(lookup.get_variable(3).is_some()); // ConstAddrList + assert!(lookup.get_variable(4).is_none()); // MutInventory from closed scope + assert!(lookup.get_variable(5).is_none()); // Inside closed scope + assert!(lookup.get_variable(6).is_some()); // ConstInventory + assert!(lookup.get_variable(7).is_some()); // Global scope + } +} diff --git a/fuzzamoto-ir/src/mutators/input.rs b/fuzzamoto-ir/src/mutators/input.rs index fd2f0618..28f8fb2e 100644 --- a/fuzzamoto-ir/src/mutators/input.rs +++ b/fuzzamoto-ir/src/mutators/input.rs @@ -1,5 +1,5 @@ use super::{Mutator, MutatorError, MutatorResult}; -use crate::{PerTestcaseMetadata, Program, ProgramBuilder}; +use crate::{PerTestcaseMetadata, Program, VariableLookup}; use rand::{RngCore, seq::IteratorRandom}; @@ -16,7 +16,7 @@ impl Mutator for InputMutator { rng: &mut R, _meta: Option<&PerTestcaseMetadata>, ) -> MutatorResult { - let Some(candidate_instruction) = program + let Some((instr_idx, _)) = program .instructions .iter() .enumerate() @@ -25,34 +25,26 @@ impl Mutator for InputMutator { else { return Err(MutatorError::NoMutationsAvailable); }; - let candidate_instruction = (candidate_instruction.0, candidate_instruction.1.clone()); - let program_upto = Program::unchecked_new( - program.context.clone(), - program.instructions[..candidate_instruction.0].to_vec(), - ); + let lookup = VariableLookup::from_instructions(&program.instructions[..instr_idx]); - let builder = ProgramBuilder::from_program(program_upto) - .expect("Program upto the chosen instruction should always be valid"); - - let candidate_input = candidate_instruction - .1 + let (input_slot, &var_idx) = program.instructions[instr_idx] .inputs .iter() .enumerate() .choose(rng) .expect("Candidates have at least one input"); - let current_variable = builder - .get_variable(*candidate_input.1) - .expect("Candiate variable has to exist"); + let current_variable = lookup + .get_variable(var_idx) + .expect("Candidate variable has to exist"); - if let Some(new_var) = builder.get_random_variable(rng, ¤t_variable.var) { + if let Some(new_var) = lookup.get_random_variable(rng, ¤t_variable.var) { if new_var.index == current_variable.index { return Err(MutatorError::NoMutationsAvailable); } - program.instructions[candidate_instruction.0].inputs[candidate_input.0] = new_var.index; + program.instructions[instr_idx].inputs[input_slot] = new_var.index; } Ok(())