From c5538e35bbb0b04c491a9aca3a2983085cf5730c Mon Sep 17 00:00:00 2001 From: zjkree Date: Mon, 13 Jan 2025 11:57:18 +0100 Subject: [PATCH] Add random program generation and mutation. --- offchain/Cargo.toml | 4 ++ offchain/src/bin/gp_tester.rs | 91 ++++++++++++++++++++++++++++++ offchain/src/gp/generate.rs | 79 ++++++++++++++++++++++++++ offchain/src/gp/genetic_ops.rs | 70 ++++++++++++++++++++--- offchain/src/gp/mod.rs | 12 +--- offchain/src/helpers/artifact.rs | 41 ++++++++++++++ offchain/src/helpers/mod.rs | 1 + offchain/src/lib.rs | 4 ++ offchain/src/main.rs | 8 +-- offchain/src/runner/revm_runner.rs | 2 +- 10 files changed, 286 insertions(+), 26 deletions(-) create mode 100644 offchain/src/bin/gp_tester.rs create mode 100644 offchain/src/gp/generate.rs create mode 100644 offchain/src/helpers/artifact.rs create mode 100644 offchain/src/helpers/mod.rs create mode 100644 offchain/src/lib.rs diff --git a/offchain/Cargo.toml b/offchain/Cargo.toml index fffccda..03796d0 100644 --- a/offchain/Cargo.toml +++ b/offchain/Cargo.toml @@ -3,6 +3,10 @@ name = "offchain" version = "0.1.0" edition = "2021" +[[bin]] +name = "gp_tester" +path = "src/bin/gp_tester.rs" + [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/offchain/src/bin/gp_tester.rs b/offchain/src/bin/gp_tester.rs new file mode 100644 index 0000000..aae71ca --- /dev/null +++ b/offchain/src/bin/gp_tester.rs @@ -0,0 +1,91 @@ +// src/bin/gp_tester.rs + +use anyhow::{anyhow, bail, Result}; +use rand::{thread_rng, Rng}; + +// 1) We'll use the helper that reads creation code from a JSON artifact +use offchain::helpers::artifact::get_creation_code; + +// 2) EvmRunner to deploy & run the interpreter +use offchain::runner::revm_runner::EvmRunner; + +// 3) AST generation & mutation +use offchain::gp::generate::random_ast; +use offchain::gp::genetic_ops::mutated_ast; +use offchain::compiler::ast::UntypedAst; + +fn main() -> Result<()> { + // ---------------------------------------------------------------------- + // 1) Read the creation code (Push3Interpreter) using our helper function + // ---------------------------------------------------------------------- + let creation_hex_filename = "../onchain/out/Push3Interpreter.sol/Push3Interpreter.json"; + let creation_bytes = get_creation_code(creation_hex_filename)?; + println!( + "Loaded creation code from {} ({} bytes)", + creation_hex_filename, + creation_bytes.len() + ); + + // 2) Create the ephemeral EVM (deploy the contract) + let mut runner = EvmRunner::new(creation_bytes)?; + println!("Deployed test interpreter at: 0x{:x}\n", runner.interpreter_addr); + + // 3) We'll generate random ASTs & mutate them + let number_of_programs = 3; // how many random ASTs to try + let max_depth = 4; // max AST depth + let mut rng = thread_rng(); + + for i in 0..number_of_programs { + // a) Generate a random AST + let ast: UntypedAst = random_ast(&mut rng, 0, max_depth); + + println!("=== Program {} ===", i); + println!("Random AST:\n{:#?}", ast); + + // b) Run the original AST + match runner.run_ast(&ast) { + Ok(outputs) => { + println!("Original AST ran successfully!"); + println!("Final CODE stack: {:?}", outputs.final_code_stack); + println!("Final EXEC stack: {:?}", outputs.final_exec_stack); + println!("Final INT stack: {:?}", outputs.final_int_stack); + + // c) Mutate it a few times + let mutated1 = mutated_ast(&ast, &mut rng, 0, max_depth); + println!("\n-- MUTATED 1 --\n{:#?}", mutated1); + + match runner.run_ast(&mutated1) { + Ok(m1_out) => { + println!("MUTATED 1 ran successfully!"); + println!("Final INT stack: {:?}", m1_out.final_int_stack); + } + Err(e) => { + println!("MUTATED 1 error: {}", e); + } + } + + let mutated2 = mutated_ast(&ast, &mut rng, 0, max_depth); + println!("\n-- MUTATED 2 --\n{:#?}", mutated2); + + match runner.run_ast(&mutated2) { + Ok(m2_out) => { + println!("MUTATED 2 ran successfully!"); + println!("Final INT stack: {:?}", m2_out.final_int_stack); + } + Err(e) => { + println!("MUTATED 2 error: {}", e); + } + } + } + Err(e) => { + // If the original fails, we won't bother mutating it + println!("Original AST error: {}", e); + } + } + + println!("=== End of Program {} ===\n", i); + } + + println!("Finished running & mutating {} random programs.", number_of_programs); + Ok(()) +} diff --git a/offchain/src/gp/generate.rs b/offchain/src/gp/generate.rs new file mode 100644 index 0000000..11ed512 --- /dev/null +++ b/offchain/src/gp/generate.rs @@ -0,0 +1,79 @@ +use rand::Rng; +use crate::compiler::ast::{UntypedAst, OpCode}; + +pub fn random_sublist_ast(rng: &mut impl Rng, max_depth: usize) -> UntypedAst { + // 1) Choose how many children the top-level `Sublist` will have. + let len = rng.gen_range(1..=3); + + // 2) Build a vector of sub-ASTs by calling `random_ast` for each child + let mut children = Vec::with_capacity(len); + for _ in 0..len { + // We start at `depth=1` because the top-level sublist itself is `depth=0`. + children.push(random_ast(rng, 1, max_depth)); + } + + // 3) Return a `Sublist` as the root node + UntypedAst::Sublist(children) +} + +/// Generate a random `UntypedAst` by recursively building sub-trees, +/// int literals, or instructions. +/// +/// - `depth` tracks how deep we are in the tree. +/// - `max_depth` is the maximum allowed depth to prevent infinite recursion. +pub fn random_ast(rng: &mut impl Rng, depth: usize, max_depth: usize) -> UntypedAst { + if depth >= max_depth { + // Return something "terminal," + // e.g. an IntLiteral or a single Instruction + random_terminal(rng) + } else { + // Weighted choice: + // 0 => IntLiteral, + // 1 => single Instruction, + // 2 => Sublist with children + let choice = rng.gen_range(0..3); + match choice { + 0 => UntypedAst::IntLiteral(rng.gen_range(-10..10)), + 1 => UntypedAst::Instruction(random_opcode(rng)), + 2 => { + // Make a sublist with 1..=3 children + let len = rng.gen_range(1..=3); + let mut children = Vec::with_capacity(len); + for _ in 0..len { + children.push(random_ast(rng, depth + 1, max_depth)); + } + UntypedAst::Sublist(children) + } + _ => unreachable!(), + } + } +} + +/// Generate a random "terminal" node. +/// Typically either an int literal or a single instruction. +fn random_terminal(rng: &mut impl Rng) -> UntypedAst { + // Weighted choice: + // 0 => IntLiteral + // 1 => single Instruction + let choice = rng.gen_range(0..2); + match choice { + 0 => UntypedAst::IntLiteral(rng.gen_range(-10..10)), + 1 => UntypedAst::Instruction(random_opcode(rng)), + _ => unreachable!(), + } +} + +/// Generate a random `OpCode` from your known set of instructions. +pub fn random_opcode(rng: &mut impl Rng) -> OpCode { + let choice = rng.gen_range(0..6); + // because we have 6 variants: Noop, Plus, Minus, Mult, Dup, Pop + match choice { + 0 => OpCode::Noop, + 1 => OpCode::Plus, + 2 => OpCode::Minus, + 3 => OpCode::Mult, + 4 => OpCode::Dup, + 5 => OpCode::Pop, + _ => unreachable!(), + } +} diff --git a/offchain/src/gp/genetic_ops.rs b/offchain/src/gp/genetic_ops.rs index b5fb028..d204b2e 100644 --- a/offchain/src/gp/genetic_ops.rs +++ b/offchain/src/gp/genetic_ops.rs @@ -1,10 +1,64 @@ -// use crate::compiler::ast::{Expr}; +use rand::Rng; +use crate::compiler::ast::{UntypedAst, OpCode}; +use crate::gp::generate::{random_ast, random_opcode}; -// pub fn crossover(a: &Expr, b: &Expr) -> (Expr, Expr) { -// // stub -// (a.clone(), b.clone()) -// } +/// Returns a new `UntypedAst` that is a mutated version of `original`. +/// We do not modify `original`; we clone or build new pieces as needed. +/// +/// - `depth`: current depth of this node, +/// - `max_depth`: max allowed depth (to prevent infinite recursion). +pub fn mutated_ast(original: &UntypedAst, rng: &mut impl Rng, depth: usize, max_depth: usize) -> UntypedAst { + // 10% chance: completely replace this node with a fresh random subtree + if rng.gen_range(0..100) < 10 { + return random_ast(rng, depth, max_depth); + } -// pub fn mutate(expr: &mut Expr) { -// // stub -// } + match original { + UntypedAst::IntLiteral(val) => { + // Another small chance to replace the entire node with random subtree + if rng.gen_range(0..100) < 10 { + return random_ast(rng, depth, max_depth); + } + + // Otherwise, "tweak" the int by a small offset + let delta = rng.gen_range(-2..=2); + let new_val = val.saturating_add(delta); + UntypedAst::IntLiteral(new_val) + } + UntypedAst::Instruction(op) => { + // 10% chance to replace node + if rng.gen_range(0..100) < 10 { + return random_ast(rng, depth, max_depth); + } + + // 50% chance to pick a new opcode + // or else keep the same + if rng.gen_bool(0.5) { + UntypedAst::Instruction(random_opcode(rng)) + } else { + // Just clone the original + UntypedAst::Instruction(op.clone()) + } + } + UntypedAst::Sublist(children) => { + // 10% chance to replace this sublist entirely + if rng.gen_range(0..100) < 10 { + return random_ast(rng, depth, max_depth); + } + + // Otherwise, mutate zero or more children + let mut new_children = children.clone(); + if !new_children.is_empty() { + // Choose how many kids to mutate: + // e.g., 1..=min(2, children.len()) + let mut_count = rng.gen_range(1..=new_children.len().min(2)); + + for _ in 0..mut_count { + let idx = rng.gen_range(0..new_children.len()); + new_children[idx] = mutated_ast(&new_children[idx], rng, depth+1, max_depth); + } + } + UntypedAst::Sublist(new_children) + } + } +} diff --git a/offchain/src/gp/mod.rs b/offchain/src/gp/mod.rs index 12178cb..3681485 100644 --- a/offchain/src/gp/mod.rs +++ b/offchain/src/gp/mod.rs @@ -1,13 +1,3 @@ pub mod population; pub mod genetic_ops; -// pub mod fitness; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dummy() { - assert_eq!(2+2, 4); - } -} +pub mod generate; diff --git a/offchain/src/helpers/artifact.rs b/offchain/src/helpers/artifact.rs new file mode 100644 index 0000000..444710f --- /dev/null +++ b/offchain/src/helpers/artifact.rs @@ -0,0 +1,41 @@ +//! src/helpers/artifact.rs +//! A small helper module to read a Forge artifact JSON and extract the creation code. + +use anyhow::{anyhow, bail, Result}; +use serde::Deserialize; +use std::fs; + +/// This mirrors the JSON structure: "bytecode": { "object": "...hex..." } +#[derive(Debug, Deserialize)] +struct BytecodeObject { + object: String, +} + +/// The top-level artifact with a "bytecode" field. +#[derive(Debug, Deserialize)] +struct MyContractArtifact { + bytecode: BytecodeObject, +} + +/// Reads the given JSON file (a Forge artifact) and returns the raw creation code bytes. +/// +/// * `filename`: path to the artifact JSON (e.g. `../onchain/out/Push3Interpreter.sol/Push3Interpreter.json`) +pub fn get_creation_code(filename: &str) -> Result> { + // 1) Read the JSON file + let creation_json = fs::read_to_string(filename) + .map_err(|e| anyhow!("Failed to read JSON file {}: {}", filename, e))?; + + // 2) Parse + let contract_artifact: MyContractArtifact = serde_json::from_str(&creation_json) + .map_err(|e| anyhow!("Failed to parse JSON artifact: {}", e))?; + + // 3) Extract the hex code + let raw_hex = &contract_artifact.bytecode.object; + let stripped_hex = raw_hex.trim_start_matches("0x"); + + // 4) Convert from hex => bytes + match hex::decode(stripped_hex) { + Ok(bytes) => Ok(bytes), + Err(e) => bail!("Invalid hex for creation code: {e}"), + } +} diff --git a/offchain/src/helpers/mod.rs b/offchain/src/helpers/mod.rs new file mode 100644 index 0000000..3ee85ee --- /dev/null +++ b/offchain/src/helpers/mod.rs @@ -0,0 +1 @@ +pub mod artifact; diff --git a/offchain/src/lib.rs b/offchain/src/lib.rs new file mode 100644 index 0000000..f969342 --- /dev/null +++ b/offchain/src/lib.rs @@ -0,0 +1,4 @@ +pub mod compiler; +pub mod runner; +pub mod gp; +pub mod helpers; diff --git a/offchain/src/main.rs b/offchain/src/main.rs index 90e1460..7c093f6 100644 --- a/offchain/src/main.rs +++ b/offchain/src/main.rs @@ -1,19 +1,15 @@ // src/main.rs - -mod compiler; -mod runner; - use std::env; // We’ll use your existing AST definitions for parsing -use compiler::ast::{ +use offchain::compiler::ast::{ parse_string_to_sexpr, sexpr_to_untyped, Push3Ast, // The trait UntypedAst, // The untyped AST enum }; -use runner::revm_runner::{EvmRunner, Push3InterpreterOutputs}; // <--- We import our EvmRunner +use offchain::runner::revm_runner::{EvmRunner, Push3InterpreterOutputs}; // <--- We import our EvmRunner use anyhow::{anyhow, bail, Result}; use hex; diff --git a/offchain/src/runner/revm_runner.rs b/offchain/src/runner/revm_runner.rs index fe97a21..5047d78 100644 --- a/offchain/src/runner/revm_runner.rs +++ b/offchain/src/runner/revm_runner.rs @@ -146,7 +146,7 @@ impl EvmRunner { self.evm.context.modify_tx(|tx| { tx.transact_to = TxKind::Call(self.interpreter_addr); tx.data = Bytes::from(call_data); - tx.nonce += 1; // increment nonce to avoid reuse + tx.nonce = 1; // increment nonce to avoid reuse }); // 6) Execute the call