Add revm_runner as module.

This commit is contained in:
zjkree
2025-01-13 10:35:54 +01:00
parent 46683c4e21
commit 856779e0bc
7 changed files with 480 additions and 252 deletions
+1
View File
@@ -1 +1,2 @@
pub mod ast;
pub mod push3_describtor;
+67
View File
@@ -0,0 +1,67 @@
//! src/compiler/push3_descriptor.rs
//!
//! A small module of helper functions to construct or interpret the 256-bit descriptors
//! used by our on-chain `Push3Interpreter`. Each descriptor encodes a tag, an offset,
//! a length, and any leftover bits (e.g., immediate data).
use ethers::types::U256;
/// The 256-bit descriptor layout is as follows:
/// [ 8 bits: tag | 32 bits: offset | 32 bits: length | 184 bits: leftover ]
///
/// We'll define small helper functions to build or parse these.
/// Our `make_sublist_descriptor` is an example for tag=3 (SUBLIST).
/// Tag constants, if you want them:
pub const TAG_SUBLIST: u8 = 3;
pub const TAG_INT_LITERAL: u8 = 2;
pub const TAG_INSTRUCTION: u8 = 1;
pub const TAG_NONE: u8 = 0;
/// A small helper to shift the tag into the top 8 bits.
#[inline]
fn tag_bits(tag: u8) -> U256 {
U256::from(tag) << 248
}
/// Build a descriptor for a SUBLIST with the given offset and length (in bytes),
/// leftover=0.
///
/// # Example
/// ```
/// let desc = make_sublist_descriptor(0, code_bytes.len() as u32);
/// // pass `desc` into the interpreter's exec stack
/// ```
pub fn make_sublist_descriptor(offset: u32, length: u32) -> U256 {
tag_bits(TAG_SUBLIST as u8)
| (U256::from(offset) << 216)
| (U256::from(length) << 184)
| U256::zero() // leftover
}
/// Possibly you want a more general helper that builds any descriptor:
/// If you have other uses for leftover bits, you can pass that in:
pub fn make_descriptor(tag: u8, offset: u32, length: u32, leftover: U256) -> U256 {
tag_bits(tag)
| (U256::from(offset) << 216)
| (U256::from(length) << 184)
| leftover
}
/// If you want to parse a descriptor, you can define getTag, getOffset, getLength, etc:
pub fn get_tag(desc: U256) -> u8 {
// top 8 bits => shift right 248
(desc >> 248).as_u32() as u8
}
pub fn get_offset(desc: U256) -> u32 {
((desc >> 216) & U256::from(u32::MAX)).as_u32()
}
pub fn get_length(desc: U256) -> u32 {
((desc >> 184) & U256::from(u32::MAX)).as_u32()
}
pub fn get_low_184(desc: U256) -> U256 {
desc & ((U256::from(1u64) << 184) - 1)
}
+53 -129
View File
@@ -1,8 +1,11 @@
// src/main.rs
mod compiler;
mod runner;
use std::env;
// We’ll use your existing AST definitions for parsing
use compiler::ast::{
parse_string_to_sexpr,
sexpr_to_untyped,
@@ -10,22 +13,28 @@ use compiler::ast::{
UntypedAst, // The untyped AST enum
};
use runner::revm_runner::{EvmRunner, Push3InterpreterOutputs}; // <--- We import our EvmRunner
use anyhow::{anyhow, bail, Result};
use hex;
use database::CacheDB;
use revm::{
context::Context,
context_interface::result::{ExecutionResult, Output},
database_interface::EmptyDB,
handler::EthHandler,
primitives::{Bytes, TxKind},
EvmCommit, MainEvm,
};
use ethers::abi::{encode, Token};
use ethers::abi::{ParamType, decode};
use ethers::types::U256;
use serde::Deserialize; // <--- NEW: for deserializing from JSON
use serde::Deserialize; // For JSON artifact loading
// If you still need these (not strictly required anymore):
// use database::CacheDB;
// use revm::{
// context::Context,
// context_interface::result::{ExecutionResult, Output},
// database_interface::EmptyDB,
// handler::EthHandler,
// primitives::{Bytes, TxKind},
// EvmCommit, MainEvm,
// };
use ethers::abi::{decode, ParamType};
use ethers::types::U256;
// ----------------------------------------------------------------------
// JSON artifact struct, as before
// ----------------------------------------------------------------------
#[derive(Debug, Deserialize)]
struct BytecodeObject {
object: String,
@@ -36,9 +45,12 @@ struct MyContractArtifact {
bytecode: BytecodeObject,
}
// ----------------------------------------------------------------------
// MAIN
// ----------------------------------------------------------------------
fn main() -> Result<()> {
// 1) Read CLI arguments
let args: Vec<String> = std::env::args().collect();
// 1) Read CLI arguments for the Push3 program
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: cargo run -- '<program>'");
eprintln!("Example: cargo run -- '((3 5 +) DUP MUL)'");
@@ -54,7 +66,7 @@ fn main() -> Result<()> {
let ast: UntypedAst = sexpr_to_untyped(&sexpr)
.map_err(|e| anyhow!("Error converting to UntypedAst: {}", e))?;
// 4) Convert UntypedAst => Push3 bytecode
// 4) Convert UntypedAst => Push3 bytecode (for debugging, so we can see it)
let bytecode = ast.to_bytecode();
// 5) Print the resulting AST and bytecode (in hex)
@@ -62,137 +74,49 @@ fn main() -> Result<()> {
println!("Bytecode (hex): 0x{}", hex::encode(&bytecode));
// ----------------------------------------------------------------------
// NEW: Read the contract creation code (bytecode) from a JSON artifact
// Load the creation code (bytecode) from a JSON artifact
// ----------------------------------------------------------------------
// Example JSON (Forge artifact) might look like:
// {
// "bytecode": "60808060...0033" <--- entire creation code
// ... other fields ...
// }
// Path to your JSON artifact. Adjust as needed:
let creation_hex_filename = "../onchain/out/Push3Interpreter.sol/Push3Interpreter.json";
// Read and parse the JSON artifact
let creation_json = std::fs::read_to_string(creation_hex_filename)
.map_err(|e| anyhow!("Failed to read JSON file {}: {}", creation_hex_filename, e))?;
let contract_artifact: MyContractArtifact = serde_json::from_str(&creation_json)
.map_err(|e| anyhow!("Failed to parse JSON artifact: {}", e))?;
// Extract the "bytecode" field from the JSON
let raw_hex = &contract_artifact.bytecode.object;
let stripped_hex = raw_hex.trim_start_matches("0x");
// ----------------------------------------------------------------------
// The rest of the deployment+call logic remains mostly unchanged
// ----------------------------------------------------------------------
// 2) Convert hex string to raw bytes.
let creation_bytes = match hex::decode(stripped_hex) {
Ok(bytes) => bytes,
Err(e) => bail!("Invalid hex for creation code: {e}"),
};
// 3) Build a new EVM instance with a "Create" transaction:
let mut evm = MainEvm::new(
// a) Prepare context with a chained transaction
Context::builder()
.modify_tx_chained(|tx| {
tx.transact_to = TxKind::Create;
tx.data = Bytes::from(creation_bytes.clone());
})
// b) Provide an in-memory DB for ephemeral state
.with_db(CacheDB::<EmptyDB>::default()),
// c) The "EthHandler" for block/tx environment logic
EthHandler::default(),
println!(
"Loaded creation code ({} bytes) from {}",
creation_bytes.len(),
creation_hex_filename
);
println!("Deploying contract with creation code size: {} bytes", creation_bytes.len());
// ----------------------------------------------------------------------
// 6) Deploy the interpreter in an ephemeral EVM using `EvmRunner`
// ----------------------------------------------------------------------
let mut runner = EvmRunner::new(creation_bytes)?;
println!("Interpreter deployed successfully in ephemeral EVM at: 0x{:x}", runner.interpreter_addr);
// 4) Execute the CREATE transaction
let creation_result = evm.exec_commit()?;
// => an ExecutionResult
// ----------------------------------------------------------------------
// 7) Evaluate the AST in our EVM
// ----------------------------------------------------------------------
// We'll call `runner.run_ast(&ast)`, which:
// - Converts AST => push3 code,
// - Builds a sublist descriptor in the exec stack,
// - Calls `runInterpreter`,
// - Returns final stacks in a `Push3InterpreterOutputs`.
let outputs = runner.run_ast(&ast)?;
println!("Ran the AST successfully!");
// 5) Check success
let ExecutionResult::Success {
output: Output::Create(_, Some(deployed_addr)),
..
} = creation_result
else {
bail!("Deployment failed or didn't return an address: {creation_result:#?}");
};
// 8) Print the final stacks
println!("Final CODE stack: {:?}", outputs.final_code_stack);
println!("Final EXEC stack: {:?}", outputs.final_exec_stack);
println!("Final INT stack: {:?}", outputs.final_int_stack);
println!("Deployed contract at: 0x{:x}", deployed_addr);
// 1) Prepare the function selector for runInterpreter
let func_selector = &ethers::utils::id("runInterpreter(bytes,uint256[],uint256[],int256[])")[0..4];
// 2) Convert your compiled “Push3” bytecode into the `bytes` argument
let code_arg = Token::Bytes(bytecode.clone());
// 3) Build a “SUBLIST descriptor” for the entire bytecode.
let tag_bits = U256::from(3u64) << 248; // CodeTag.SUBLIST = 3
let offset_bits = U256::from(0u64) << 216; // offset=0
let length_bits = U256::from(bytecode.len() as u64) << 184; // length = bytecode.len()
let low_184 = U256::zero();
let sublist_descriptor = tag_bits | offset_bits | length_bits | low_184;
// Now we put that descriptor as the one and only item in `initExecStack`.
let init_exec_stack = Token::Array(vec![
Token::Uint(sublist_descriptor)
]);
// For the other two arrays (codeStack, intStack), we’ll just leave them empty:
let empty_uint256_array = Token::Array(vec![]); // codeStack
let empty_int256_array = Token::Array(vec![]); // intStack
// 4) Encode all arguments in the correct order:
let encoded_args = encode(&[
code_arg,
empty_uint256_array.clone(), // initCodeStack
init_exec_stack, // initExecStack
empty_uint256_array.clone(), // initIntStack
]);
// 5) Concatenate selector + encoded arguments to form the full call data
let mut call_data = Vec::from(func_selector);
call_data.extend_from_slice(&encoded_args);
// 6) Modify the transaction to call `runInterpreter` with the encoded data
evm.context.modify_tx(|tx| {
tx.transact_to = TxKind::Call(deployed_addr);
tx.data = Bytes::from(call_data);
tx.nonce += 1; // increment nonce to avoid reuse
});
// 7) Execute the call.
let call_result = evm.transact()?;
match &call_result.result {
ExecutionResult::Success {
output: Output::Call(return_data),
..
} => {
println!("Call succeeded, returned data: 0x{}", hex::encode(return_data));
// Decode (uint256[], uint256[], int256[])
let param_types = &[
ParamType::Array(Box::new(ParamType::Uint(256))), // finalCodeStack
ParamType::Array(Box::new(ParamType::Uint(256))), // finalExecStack
ParamType::Array(Box::new(ParamType::Int(256))), // finalIntStack
];
let decoded = decode(param_types, return_data)
.map_err(|e| anyhow!("Failed to decode return data: {e}"))?;
println!("Decoded return: {:?}", decoded);
}
ExecutionResult::Revert { gas_used, output } => {
bail!("Call reverted: gas used = {gas_used:?}, output = {:?}", output);
}
other => {
bail!("Call failed: {other:?}");
}
}
Ok(())
}
+232
View File
@@ -0,0 +1,232 @@
//! src/evm_integration/revm_runner.rs
//!
//! This module provides a thin wrapper around REVM to deploy a Push3Interpreter
//! contract, then call its `runInterpreter(...)` function with flexible inputs/outputs,
//! using the same style (Context::builder(), .modify_tx_chained, etc.) you had in your main.
use anyhow::{anyhow, bail, Result};
use ethers::abi::{encode, decode, Token, ParamType};
use ethers::types::U256;
use ethers::utils;
use database::CacheDB;
use revm::{
context::Context,
context::{BlockEnv, TxEnv, CfgEnv},
context_interface::result::{ExecutionResult, Output},
database_interface::EmptyDB,
handler::EthHandler,
primitives::{Bytes, TxKind}, // 4 generics
EvmCommit,
MainEvm,
};
// We import your AST definitions so we can call `ast.to_bytecode()`.
use crate::compiler::ast::{UntypedAst, Push3Ast};
// If you have a descriptor helper (like make_sublist_descriptor), bring it in:
use crate::compiler::push3_describtor::make_sublist_descriptor;
/// The input parameters for `runInterpreter(...)`: four fields (code, codeStack, execStack, intStack).
pub struct Push3InterpreterInputs {
pub code: Vec<u8>,
pub init_code_stack: Vec<U256>,
pub init_exec_stack: Vec<U256>,
pub init_int_stack: Vec<i128>,
}
/// The outputs from `runInterpreter(...)`: three arrays for code/exec/int stacks.
pub struct Push3InterpreterOutputs {
pub final_code_stack: Vec<U256>,
pub final_exec_stack: Vec<U256>,
pub final_int_stack: Vec<i128>,
}
/// A thin wrapper around REVM, parameterized by the 4 generics (DB, BLOCK, TX, CFG).
/// - We store the ephemeral EVM instance,
/// - We store the deployed address of your `Push3Interpreter`,
/// - We provide `run_interpreter` and `run_ast`.
pub struct EvmRunner {
/// The 4-parameter MainEvm:
/// DB = CacheDB<EmptyDB>,
/// BLOCK = BlockEnv,
/// TX = TxEnv,
/// CFG = CfgEnv
pub evm: MainEvm<CacheDB<EmptyDB>, BlockEnv, TxEnv, CfgEnv>,
/// The address where Push3Interpreter was deployed.
pub interpreter_addr: revm::primitives::Address,
}
impl EvmRunner {
/// Deploy a new ephemeral EVM with a "Create" transaction for the given `creation_code`.
///
/// This matches your older style of:
/// `MainEvm::new(Context::builder()... .with_db(...), EthHandler::default())`
pub fn new(creation_code: Vec<u8>) -> Result<Self> {
// 1) Create the EVM using your old style: `Context::builder()...`
// Then pass to MainEvm::new(...).
// The difference is that we explicitly say MainEvm<DB,BLOCK,TX,CFG>.
let mut evm = MainEvm::new(
// a) Prepare context with a chained transaction to CREATE
Context::builder()
.modify_tx_chained(|tx| {
tx.transact_to = TxKind::Create;
tx.data = Bytes::from(creation_code.clone());
})
// b) Provide ephemeral DB
.with_db(CacheDB::<EmptyDB>::default()),
// c) The EthHandler for block/tx environment logic
EthHandler::default(),
);
// 2) Execute the CREATE transaction
let creation_result = evm.exec_commit()?;
let ExecutionResult::Success {
output: Output::Create(_, Some(deployed_addr)),
..
} = creation_result
else {
bail!("Interpreter deployment failed or no address returned: {creation_result:#?}");
};
// 3) Return the EvmRunner
Ok(Self {
evm,
interpreter_addr: deployed_addr,
})
}
/// Call `runInterpreter(bytes,uint256[],uint256[],int256[])` on the deployed contract,
/// returning the final code/exec/int stacks.
pub fn run_interpreter(
&mut self,
inputs: &Push3InterpreterInputs
) -> Result<Push3InterpreterOutputs> {
// 1) Build function selector
let func_selector = &utils::id("runInterpreter(bytes,uint256[],uint256[],int256[])")[0..4];
// 2) Convert each field to `ethers::abi::Token`
let code_token = Token::Bytes(inputs.code.clone());
let init_code_stack = Token::Array(
inputs.init_code_stack
.iter()
.map(|&u| Token::Uint(u))
.collect()
);
let init_exec_stack = Token::Array(
inputs.init_exec_stack
.iter()
.map(|&u| Token::Uint(u))
.collect()
);
let init_int_stack = Token::Array(
inputs.init_int_stack
.iter()
.map(|&val_i128| {
// store i128 as a U256. If you need negative values, do a two’s complement approach.
let as_u128 = val_i128 as u128;
Token::Int(U256::from(as_u128))
})
.collect()
);
// 3) Encode
let encoded_args = encode(&[
code_token,
init_code_stack,
init_exec_stack,
init_int_stack,
]);
// 4) Build final call data
let mut call_data = Vec::from(func_selector);
call_data.extend_from_slice(&encoded_args);
// 5) Modify the transaction to CALL the deployed interpreter
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
});
// 6) Execute the call
let call_result = self.evm.transact()?;
match &call_result.result {
ExecutionResult::Success {
output: Output::Call(return_data),
..
} => {
// 7) Decode (uint256[], uint256[], int256[])
let param_types = &[
ParamType::Array(Box::new(ParamType::Uint(256))), // finalCodeStack
ParamType::Array(Box::new(ParamType::Uint(256))), // finalExecStack
ParamType::Array(Box::new(ParamType::Int(256))), // finalIntStack
];
let decoded = decode(param_types, return_data)
.map_err(|e| anyhow!("Failed to decode return data: {e}"))?;
// parse each array
let final_code_stack = match &decoded[0] {
Token::Array(arr) => arr.iter().filter_map(|t| {
if let Token::Uint(u) = t { Some(*u) } else { None }
}).collect(),
_ => Vec::new(),
};
let final_exec_stack = match &decoded[1] {
Token::Array(arr) => arr.iter().filter_map(|t| {
if let Token::Uint(u) = t { Some(*u) } else { None }
}).collect(),
_ => Vec::new(),
};
let final_int_stack = match &decoded[2] {
Token::Array(arr) => arr.iter().filter_map(|t| {
if let Token::Int(u256_val) = t {
// read only the lower 128 bits => ignoring sign extension
let lo = u256_val.low_u128();
Some(lo as i128)
} else {
None
}
}).collect(),
_ => Vec::new(),
};
Ok(Push3InterpreterOutputs {
final_code_stack,
final_exec_stack,
final_int_stack,
})
}
ExecutionResult::Revert { gas_used, output } => {
bail!("Call reverted: gas used={gas_used:?}, output={output:?}")
}
other => {
bail!("Call failed: {other:?}")
}
}
}
/// A convenience method to run an `UntypedAst`:
/// - Convert AST => push3 code,
/// - Build a sublist descriptor in the exec stack,
/// - Call `run_interpreter`.
pub fn run_ast(&mut self, ast: &UntypedAst) -> Result<Push3InterpreterOutputs> {
// 1) Convert AST => push3 bytecode
let code_bytes = ast.to_bytecode();
let code_len = code_bytes.len() as u32;
// 2) Build a sublist descriptor
let descriptor = make_sublist_descriptor(0, code_len);
// 3) Put code in `code` and sublist descriptor in `execStack`
let inputs = Push3InterpreterInputs {
code: code_bytes,
init_code_stack: Vec::new(),
init_exec_stack: vec![descriptor],
init_int_stack: Vec::new(),
};
// 4) Run interpreter
self.run_interpreter(&inputs)
}
}