WIP: offchain-start #14
@@ -3,6 +3,10 @@ name = "offchain"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "gp_tester"
|
||||||
|
path = "src/bin/gp_tester.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
/// Returns a new `UntypedAst` that is a mutated version of `original`.
|
||||||
// // stub
|
/// We do not modify `original`; we clone or build new pieces as needed.
|
||||||
// (a.clone(), b.clone())
|
///
|
||||||
// }
|
/// - `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) {
|
match original {
|
||||||
// // stub
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-11
@@ -1,13 +1,3 @@
|
|||||||
pub mod population;
|
pub mod population;
|
||||||
pub mod genetic_ops;
|
pub mod genetic_ops;
|
||||||
// pub mod fitness;
|
pub mod generate;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_dummy() {
|
|
||||||
assert_eq!(2+2, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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<Vec<u8>> {
|
||||||
|
// 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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod artifact;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod compiler;
|
||||||
|
pub mod runner;
|
||||||
|
pub mod gp;
|
||||||
|
pub mod helpers;
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
// src/main.rs
|
// src/main.rs
|
||||||
|
|
||||||
mod compiler;
|
|
||||||
mod runner;
|
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
// We’ll use your existing AST definitions for parsing
|
// We’ll use your existing AST definitions for parsing
|
||||||
use compiler::ast::{
|
use offchain::compiler::ast::{
|
||||||
parse_string_to_sexpr,
|
parse_string_to_sexpr,
|
||||||
sexpr_to_untyped,
|
sexpr_to_untyped,
|
||||||
Push3Ast, // The trait
|
Push3Ast, // The trait
|
||||||
UntypedAst, // The untyped AST enum
|
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 anyhow::{anyhow, bail, Result};
|
||||||
use hex;
|
use hex;
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ impl EvmRunner {
|
|||||||
self.evm.context.modify_tx(|tx| {
|
self.evm.context.modify_tx(|tx| {
|
||||||
tx.transact_to = TxKind::Call(self.interpreter_addr);
|
tx.transact_to = TxKind::Call(self.interpreter_addr);
|
||||||
tx.data = Bytes::from(call_data);
|
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
|
// 6) Execute the call
|
||||||
|
|||||||
Reference in New Issue
Block a user