diff --git a/Cargo.lock b/Cargo.lock index 6e03f4d..355c810 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4655,6 +4655,7 @@ dependencies = [ "forge-script", "foundry-common", "futures", + "hyper 1.4.1", "op-alloy-rpc-types", "op-test-vectors", "revm 12.1.0", diff --git a/bin/opt8n/Cargo.toml b/bin/opt8n/Cargo.toml index b01cfd5..3d1e2dd 100644 --- a/bin/opt8n/Cargo.toml +++ b/bin/opt8n/Cargo.toml @@ -36,3 +36,4 @@ revm.workspace = true # OP Types op-test-vectors.workspace = true op-alloy-rpc-types.workspace = true +hyper = "1.4.1" diff --git a/bin/opt8n/src/cmd/mod.rs b/bin/opt8n/src/cmd/mod.rs new file mode 100644 index 0000000..016fb70 --- /dev/null +++ b/bin/opt8n/src/cmd/mod.rs @@ -0,0 +1,3 @@ +pub mod repl; +pub mod script; +pub mod server; diff --git a/bin/opt8n/src/cmd/repl.rs b/bin/opt8n/src/cmd/repl.rs new file mode 100644 index 0000000..c939382 --- /dev/null +++ b/bin/opt8n/src/cmd/repl.rs @@ -0,0 +1,111 @@ +use anvil::cmd::NodeArgs; +use clap::{CommandFactory, FromArgMatches, Parser}; +use futures::StreamExt; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncBufReadExt, BufReader}; + +use crate::opt8n::{Opt8n, Opt8nArgs}; + +#[derive(Parser, Clone, Debug)] +pub struct ReplArgs { + #[command(flatten)] + opt8n_args: Opt8nArgs, + #[command(flatten)] + pub node_args: NodeArgs, +} + +impl ReplArgs { + pub async fn run(&self) -> color_eyre::Result<()> { + let mut opt8n = Opt8n::new( + Some(self.node_args.clone()), + self.opt8n_args.output.clone(), + self.opt8n_args.genesis.clone(), + ) + .await?; + + repl(&mut opt8n).await?; + + Ok(()) + } +} + +#[derive(Parser, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[clap(rename_all = "snake_case", infer_subcommands = true, multicall = true)] +pub enum ReplCommand { + #[command(visible_alias = "a")] + Anvil { + #[arg(index = 1, allow_hyphen_values = true)] + args: Vec, + }, + #[command(visible_alias = "c")] + Cast { + #[arg(index = 1, allow_hyphen_values = true)] + args: Vec, + }, + Dump, + RpcEndpoint, + // TODO: implement clear + // TODO: implement reset + #[command(visible_alias = "e")] + Exit, +} + +/// Listens for commands, and new blocks from the block stream. +pub async fn repl(opt8n: &mut Opt8n) -> color_eyre::Result<()> { + let mut new_blocks = opt8n.eth_api.backend.new_block_notifications(); + + loop { + tokio::select! { + command = receive_command() => { + match command { + Ok(ReplCommand::Exit) => break, + Ok(command) => execute(opt8n, command).await?, + Err(e) => eprintln!("Error: {:?}", e), + } + } + + new_block = new_blocks.next() => { + if let Some(new_block) = new_block { + if let Some(block) = opt8n.eth_api.backend.get_block_by_hash(new_block.hash) { + opt8n.generate_execution_fixture(block).await?; + } + } + } + } + } + + Ok(()) +} + +async fn receive_command() -> color_eyre::Result { + let line = BufReader::new(tokio::io::stdin()) + .lines() + .next_line() + .await? + .unwrap(); + let words = shellwords::split(&line)?; + + let matches = ReplCommand::command().try_get_matches_from(words)?; + Ok(ReplCommand::from_arg_matches(&matches)?) +} + +async fn execute(opt8n: &mut Opt8n, command: ReplCommand) -> color_eyre::Result<()> { + match command { + ReplCommand::Dump => { + opt8n.mine_block().await; + } + ReplCommand::Anvil { mut args } => { + args.insert(0, "anvil".to_string()); + let command = NodeArgs::command_for_update(); + let matches = command.try_get_matches_from(args)?; + let node_args = NodeArgs::from_arg_matches(&matches)?; + node_args.run().await?; + } + ReplCommand::Cast { .. } => {} + ReplCommand::RpcEndpoint => { + println!("{}", opt8n.node_handle.http_endpoint()); + } + ReplCommand::Exit => unreachable!(), + } + Ok(()) +} diff --git a/bin/opt8n/src/cmd/script.rs b/bin/opt8n/src/cmd/script.rs new file mode 100644 index 0000000..e75b094 --- /dev/null +++ b/bin/opt8n/src/cmd/script.rs @@ -0,0 +1,126 @@ +use anvil::cmd::NodeArgs; +use clap::Parser; +use color_eyre::eyre::eyre; +use futures::StreamExt; + +use crate::opt8n::{Opt8n, Opt8nArgs}; + +#[derive(Parser, Clone, Debug)] +pub struct ScriptArgs { + #[command(flatten)] + opt8n_args: Opt8nArgs, + #[command(flatten)] + inner: forge_script::ScriptArgs, + #[command(flatten)] + pub node_args: NodeArgs, +} + +impl ScriptArgs { + pub async fn run(mut self) -> color_eyre::Result<()> { + let opt8n = Opt8n::new( + Some(self.node_args.clone()), + self.opt8n_args.output.clone(), + self.opt8n_args.genesis.clone(), + ) + .await?; + + foundry_common::shell::set_shell(foundry_common::shell::Shell::from_args( + self.inner.opts.silent, + self.inner.json, + ))?; + + self.inner.broadcast = true; + self.inner.evm_opts.sender = Some( + opt8n + .node_handle + .genesis_accounts() + .last() + .expect("Could not get genesis account"), + ); + self.inner.unlocked = true; + self.inner.evm_opts.fork_url = Some(opt8n.node_handle.http_endpoint()); + + run_script(opt8n, Box::new(self.inner)).await?; + + Ok(()) + } +} + +/// Run a Forge script with the given arguments, and generate an execution fixture +/// from the broadcasted transactions. +pub async fn run_script( + opt8n: Opt8n, + script_args: Box, +) -> color_eyre::Result<()> { + let mut new_blocks = opt8n.eth_api.backend.new_block_notifications(); + + // Run the forge script and broadcast the transactions to the anvil node + let mut opt8n = broadcast_transactions(opt8n, script_args).await?; + + // Mine the block and generate the execution fixture + opt8n.mine_block().await; + + let block = new_blocks.next().await.ok_or(eyre!("No new block"))?; + if let Some(block) = opt8n.eth_api.backend.get_block_by_hash(block.hash) { + opt8n.generate_execution_fixture(block).await?; + } + + Ok(()) +} + +async fn broadcast_transactions( + opt8n: Opt8n, + script_args: Box, +) -> color_eyre::Result { + // Run the script, compile the transactions and broadcast to the anvil instance + let compiled = script_args.preprocess().await?.compile()?; + + let pre_simulation = compiled + .link() + .await? + .prepare_execution() + .await? + .execute() + .await? + .prepare_simulation() + .await?; + + let bundled = pre_simulation.fill_metadata().await?.bundle().await?; + + let tx_count = bundled + .sequence + .sequences() + .iter() + .fold(0, |sum, sequence| sum + sequence.transactions.len()); + + // TODO: break into function + let broadcast = bundled.broadcast(); + + let pending_transactions = tokio::task::spawn(async move { + loop { + let pending_tx_count = opt8n + .eth_api + .txpool_content() + .await + .expect("Failed to get txpool content") + .pending + .len(); + + if pending_tx_count == tx_count { + return opt8n; + } + } + }); + + let opt8n = tokio::select! { + _ = broadcast => { + // TODO: Gracefully handle this error + return Err(eyre!("Script failed early")); + }, + opt8n = pending_transactions => { + opt8n? + } + }; + + Ok(opt8n) +} diff --git a/bin/opt8n/src/cmd/server.rs b/bin/opt8n/src/cmd/server.rs new file mode 100644 index 0000000..7ec514b --- /dev/null +++ b/bin/opt8n/src/cmd/server.rs @@ -0,0 +1,18 @@ +use anvil::cmd::NodeArgs; +use clap::Parser; + +use crate::opt8n::Opt8nArgs; + +#[derive(Parser, Clone, Debug)] +pub struct ServerArgs { + #[command(flatten)] + pub opt8n_args: Opt8nArgs, + #[command(flatten)] + pub node_args: NodeArgs, +} + +impl ServerArgs { + pub async fn run(&self) -> color_eyre::Result<()> { + unimplemented!() + } +} diff --git a/bin/opt8n/src/main.rs b/bin/opt8n/src/main.rs index 6dea2bf..023f6f4 100644 --- a/bin/opt8n/src/main.rs +++ b/bin/opt8n/src/main.rs @@ -1,102 +1,35 @@ +pub mod cmd; pub mod opt8n; -use std::path::PathBuf; -use anvil::cmd::NodeArgs; +use crate::cmd::script::ScriptArgs; use clap::Parser; +use cmd::repl::ReplArgs; +use cmd::server::ServerArgs; use color_eyre::eyre; -use forge_script::ScriptArgs; -use opt8n::Opt8n; #[derive(Parser, Clone, Debug)] #[command(author, version, about, long_about = None)] pub struct Args { #[command(subcommand)] pub command: Commands, - #[command(flatten)] - pub node_args: NodeArgs, } #[derive(Parser, Clone, Debug)] pub enum Commands { - /// Starts a REPL for running forge, anvil, and cast commands - #[command(visible_alias = "r")] - Repl { - #[command(flatten)] - opt8n_args: Opt8nArgs, - }, - /// Uses a forge script to generate a test vector - #[command(visible_alias = "s")] - Script { - #[command(flatten)] - opt8n_args: Opt8nArgs, - #[command(flatten)] - script_args: Box, - }, -} - -impl Commands { - fn get_opt8n_args(&self) -> &Opt8nArgs { - match self { - Commands::Repl { opt8n_args } => opt8n_args, - Commands::Script { opt8n_args, .. } => opt8n_args, - } - } -} - -#[derive(Parser, Clone, Debug)] -pub struct Opt8nArgs { - #[clap(long, help = "Output file for the execution test fixture")] - pub output: PathBuf, - #[clap(long, help = "Path to genesis state")] - pub genesis: Option, + Repl(ReplArgs), + Script(ScriptArgs), + Server(ServerArgs), } #[tokio::main] async fn main() -> eyre::Result<()> { color_eyre::install()?; - let args = Args::parse(); - let node_args = args.node_args.clone(); - let opt8n_args = args.command.get_opt8n_args(); - - if node_args.evm_opts.fork_url.is_some() || node_args.evm_opts.fork_block_number.is_some() { - return Err(eyre::eyre!( - "Forking is not supported in opt8n, please specify prestate with a genesis file" - )); - } - - let node_config = node_args.clone().into_node_config(); - let mut opt8n = Opt8n::new( - Some(node_config), - opt8n_args.output.clone(), - opt8n_args.genesis.clone(), - ) - .await?; - - match args.command { - Commands::Repl { .. } => { - opt8n.repl().await?; - } - Commands::Script { - mut script_args, .. - } => { - foundry_common::shell::set_shell(foundry_common::shell::Shell::from_args( - script_args.opts.silent, - script_args.json, - ))?; - - script_args.broadcast = true; - script_args.evm_opts.sender = Some( - opt8n - .node_handle - .genesis_accounts() - .last() - .expect("Could not get genesis account"), - ); - script_args.unlocked = true; - script_args.evm_opts.fork_url = Some(opt8n.node_handle.http_endpoint()); + let command = Args::parse().command; - opt8n.run_script(script_args).await?; - } + match command { + Commands::Repl(cmd) => cmd.run().await?, + Commands::Script(cmd) => cmd.run().await?, + Commands::Server(cmd) => cmd.run().await?, } Ok(()) diff --git a/bin/opt8n/src/opt8n.rs b/bin/opt8n/src/opt8n.rs index 4984e94..b41d159 100644 --- a/bin/opt8n/src/opt8n.rs +++ b/bin/opt8n/src/opt8n.rs @@ -7,24 +7,28 @@ use anvil::{cmd::NodeArgs, eth::EthApi, NodeConfig, NodeHandle}; use anvil_core::eth::block::Block; use anvil_core::eth::transaction::PendingTransaction; use cast::traces::{GethTraceBuilder, TracingInspectorConfig}; -use forge_script::ScriptArgs; +use clap::Parser; use std::{ error::Error, fs::{self, File}, path::PathBuf, }; -use clap::{CommandFactory, FromArgMatches, Parser}; use color_eyre::eyre::{ensure, eyre, Result}; -use futures::StreamExt; use op_test_vectors::execution::{ExecutionFixture, ExecutionReceipt, ExecutionResult}; use revm::{ db::{AlloyDB, CacheDB}, primitives::{BlobExcessGasAndPrice, BlockEnv, CfgEnv, Env, SpecId, U256}, Database, DatabaseCommit, DatabaseRef, Evm, EvmBuilder, }; -use serde::{Deserialize, Serialize}; -use tokio::io::{AsyncBufReadExt, BufReader}; + +#[derive(Parser, Clone, Debug)] +pub struct Opt8nArgs { + #[clap(long, help = "Output file for the execution test fixture")] + pub output: PathBuf, + #[clap(long, help = "Path to genesis state")] + pub genesis: Option, +} pub struct Opt8n { pub eth_api: EthApi, @@ -36,10 +40,24 @@ pub struct Opt8n { impl Opt8n { pub async fn new( - node_config: Option, + node_args: Option, output_file: PathBuf, genesis: Option, ) -> Result { + let node_config = if let Some(node_args) = node_args { + if node_args.evm_opts.fork_url.is_some() + || node_args.evm_opts.fork_block_number.is_some() + { + return Err(eyre!( + "Forking is not supported in opt8n, please specify prestate with a genesis file" + )); + } + + Some(node_args.into_node_config()) + } else { + None + }; + let genesis = if let Some(genesis) = genesis.as_ref() { serde_json::from_reader(File::open(genesis)?)? } else { @@ -64,141 +82,6 @@ impl Opt8n { }) } - /// Listens for commands, and new blocks from the block stream. - pub async fn repl(&mut self) -> Result<()> { - let mut new_blocks = self.eth_api.backend.new_block_notifications(); - - loop { - tokio::select! { - command = self.receive_command() => { - match command { - Ok(ReplCommand::Exit) => break, - Ok(command) => self.execute(command).await?, - Err(e) => eprintln!("Error: {:?}", e), - } - } - - new_block = new_blocks.next() => { - if let Some(new_block) = new_block { - if let Some(block) = self.eth_api.backend.get_block_by_hash(new_block.hash) { - self.generate_execution_fixture(block).await?; - } - } - } - } - } - - Ok(()) - } - - /// Run a Forge script with the given arguments, and generate an execution fixture - /// from the broadcasted transactions. - pub async fn run_script(self, script_args: Box) -> Result<()> { - let mut new_blocks = self.eth_api.backend.new_block_notifications(); - - // Run the forge script and broadcast the transactions to the anvil node - let mut opt8n = self.broadcast_transactions(script_args).await?; - - // Mine the block and generate the execution fixture - opt8n.mine_block().await; - - let block = new_blocks.next().await.ok_or(eyre!("No new block"))?; - if let Some(block) = opt8n.eth_api.backend.get_block_by_hash(block.hash) { - opt8n.generate_execution_fixture(block).await?; - } - - Ok(()) - } - - async fn broadcast_transactions(self, script_args: Box) -> Result { - // Run the script, compile the transactions and broadcast to the anvil instance - let compiled = script_args.preprocess().await?.compile()?; - - let pre_simulation = compiled - .link() - .await? - .prepare_execution() - .await? - .execute() - .await? - .prepare_simulation() - .await?; - - let bundled = pre_simulation.fill_metadata().await?.bundle().await?; - - let tx_count = bundled - .sequence - .sequences() - .iter() - .fold(0, |sum, sequence| sum + sequence.transactions.len()); - - // TODO: break into function - let broadcast = bundled.broadcast(); - - let opt8n = self; - - let pending_transactions = tokio::task::spawn(async move { - loop { - let pending_tx_count = opt8n - .eth_api - .txpool_content() - .await - .expect("Failed to get txpool content") - .pending - .len(); - - if pending_tx_count == tx_count { - return opt8n; - } - } - }); - - let opt8n = tokio::select! { - _ = broadcast => { - // TODO: Gracefully handle this error - return Err(eyre!("Script failed early")); - }, - opt8n = pending_transactions => { - opt8n? - } - }; - - Ok(opt8n) - } - - async fn receive_command(&self) -> Result { - let line = BufReader::new(tokio::io::stdin()) - .lines() - .next_line() - .await? - .unwrap(); - let words = shellwords::split(&line)?; - - let matches = ReplCommand::command().try_get_matches_from(words)?; - Ok(ReplCommand::from_arg_matches(&matches)?) - } - - async fn execute(&mut self, command: ReplCommand) -> Result<()> { - match command { - ReplCommand::Dump => { - self.mine_block().await; - } - ReplCommand::Anvil { mut args } => { - args.insert(0, "anvil".to_string()); - let command = NodeArgs::command_for_update(); - let matches = command.try_get_matches_from(args)?; - let node_args = NodeArgs::from_arg_matches(&matches)?; - node_args.run().await?; - } - ReplCommand::Cast { .. } => {} - ReplCommand::RpcEndpoint => { - println!("{}", self.node_handle.http_endpoint()); - } - ReplCommand::Exit => unreachable!(), - } - Ok(()) - } - /// Updates the pre and post state allocations of the [ExecutionFixture] from Revm. pub fn capture_pre_post_alloc(&mut self, block: &Block) -> Result<()> { let revm_db = CacheDB::new( @@ -337,27 +220,6 @@ where evm } -#[derive(Parser, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[clap(rename_all = "snake_case", infer_subcommands = true, multicall = true)] -pub enum ReplCommand { - #[command(visible_alias = "a")] - Anvil { - #[arg(index = 1, allow_hyphen_values = true)] - args: Vec, - }, - #[command(visible_alias = "c")] - Cast { - #[arg(index = 1, allow_hyphen_values = true)] - args: Vec, - }, - Dump, - RpcEndpoint, - // TODO: implement clear - // TODO: implement reset - #[command(visible_alias = "e")] - Exit, -} - #[cfg(test)] mod tests { #[tokio::test] diff --git a/examples/exec-scripts/lib/forge-std b/examples/exec-scripts/lib/forge-std index 2cbff06..bf66061 160000 --- a/examples/exec-scripts/lib/forge-std +++ b/examples/exec-scripts/lib/forge-std @@ -1 +1 @@ -Subproject commit 2cbff0602d340503dba9828ab6981053704d1384 +Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623