├── rakoon ├── Makefile ├── imgs └── image.png ├── .gitignore ├── crates ├── mutator │ ├── Cargo.toml │ └── src │ │ ├── constants.rs │ │ ├── lib.rs │ │ └── operations.rs ├── common │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── errors.rs │ │ ├── types.rs │ │ └── constants.rs ├── app │ ├── Cargo.toml │ └── src │ │ ├── errors.rs │ │ ├── manager.rs │ │ ├── lib.rs │ │ └── handler.rs └── runners │ ├── Cargo.toml │ └── src │ ├── cache.rs │ ├── lib.rs │ ├── logger.rs │ ├── legacy.rs │ ├── al.rs │ ├── eip1559.rs │ ├── eip7702.rs │ ├── blob.rs │ ├── random.rs │ └── builder.rs ├── rustfmt.toml ├── bin ├── Cargo.toml └── cli │ └── main.rs ├── Cargo.toml └── README.md /rakoon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nethoxa/rakoon/HEAD/rakoon -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | cargo build --release 3 | cp target/release/rakoon . -------------------------------------------------------------------------------- /imgs/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nethoxa/rakoon/HEAD/imgs/image.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /reference 3 | /tx-fuzz 4 | Cargo.lock 5 | reports/ 6 | logs/ -------------------------------------------------------------------------------- /crates/mutator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mutator" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | 7 | [dependencies] 8 | rand ={ workspace = true } -------------------------------------------------------------------------------- /crates/common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | 7 | [dependencies] 8 | alloy = { workspace = true } 9 | rand = { workspace = true } -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | use_field_init_shorthand = true 3 | use_small_heuristics = "Max" 4 | array_width = 1 5 | 6 | # Nightly 7 | max_width = 100 8 | comment_width = 100 9 | imports_granularity = "Crate" 10 | wrap_comments = true 11 | format_code_in_doc_comments = true 12 | doc_comment_code_block_width = 100 13 | format_macro_matchers = true 14 | -------------------------------------------------------------------------------- /crates/common/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use alloy::{hex::decode, signers::k256::ecdsa::SigningKey}; 3 | 4 | pub mod constants; 5 | pub mod errors; 6 | pub mod types; 7 | 8 | pub fn parse_sk(sk: &str) -> Result { 9 | let sk = SigningKey::from_slice(&decode(sk).map_err(|_| Error::InvalidKey)?) 10 | .map_err(|_| Error::InvalidKey)?; 11 | Ok(sk) 12 | } 13 | -------------------------------------------------------------------------------- /crates/app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | 7 | [dependencies] 8 | crossterm = { workspace = true } 9 | ratatui = { workspace = true } 10 | tokio = { workspace = true } 11 | colored = { workspace = true } 12 | alloy = { workspace = true } 13 | 14 | common = { path = "../common" } 15 | runners = { path = "../runners" } 16 | -------------------------------------------------------------------------------- /crates/runners/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "runners" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | 7 | [dependencies] 8 | rand = { workspace = true } 9 | tokio = { workspace = true } 10 | alloy = { workspace = true } 11 | alloy-rlp = { workspace = true } 12 | hex = { workspace = true } 13 | chrono = { workspace = true } 14 | 15 | common = { path = "../common" } 16 | mutator = { path = "../mutator" } -------------------------------------------------------------------------------- /bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rakoon" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.85" 6 | 7 | [[bin]] 8 | name = "rakoon" 9 | path = "cli/main.rs" 10 | 11 | [dependencies] 12 | crossterm = { workspace = true } 13 | ratatui = { workspace = true } 14 | tokio = { workspace = true } 15 | clap = { workspace = true } 16 | alloy = { workspace = true } 17 | 18 | app = { path = "../crates/app" } 19 | common = { path = "../crates/common" } 20 | runners = { path = "../crates/runners" } 21 | 22 | -------------------------------------------------------------------------------- /crates/app/src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | #[derive(PartialEq, Debug)] 4 | pub enum AppStatus { 5 | RuntimeError, 6 | Exit, 7 | InvalidBool, 8 | } 9 | 10 | impl Display for AppStatus { 11 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 12 | match self { 13 | AppStatus::RuntimeError => write!(f, "runtime error"), 14 | AppStatus::Exit => write!(f, "exit"), 15 | AppStatus::InvalidBool => write!(f, "invalid bool"), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/common/src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | #[derive(Debug)] 4 | pub enum Error { 5 | InvalidRpcUrl(String), 6 | RuntimeError, 7 | InvalidKey, 8 | RunnerAlreadyRunning, 9 | RunnerAlreadyStopped, 10 | } 11 | 12 | impl Display for Error { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | match self { 15 | Error::InvalidRpcUrl(url) => write!(f, "invalid rpc url: {}", url), 16 | Error::RuntimeError => write!(f, "runtime error"), 17 | Error::InvalidKey => write!(f, "invalid key"), 18 | Error::RunnerAlreadyRunning => write!(f, "runner already running"), 19 | Error::RunnerAlreadyStopped => write!(f, "runner already stopped"), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | version = "0.1.0" 3 | edition = "2024" 4 | rust-version = "1.85" 5 | 6 | [workspace.lints.rust] 7 | async_fn_in_trait = "allow" 8 | 9 | [workspace] 10 | members = [ 11 | "bin", 12 | "crates/mutator", 13 | "crates/common", 14 | "crates/app", "crates/runners"] 15 | resolver = "3" 16 | 17 | [workspace.dependencies] 18 | mutator = { path = "crates/mutator" } 19 | common = { path = "crates/common" } 20 | 21 | crossterm = "0.29.0" 22 | ratatui = "0.29.0" 23 | tokio = { version = "1.44.0", features = ["full"] } 24 | colored = "3.0.0" 25 | alloy = { git = "https://github.com/nethoxa/unsafe-alloy.git", branch = "main" } 26 | rand = "0.9.0" 27 | clap = { version = "4.5.37", features = ["derive"] } 28 | alloy-rlp = "0.3.11" 29 | chrono = "0.4.41" 30 | hex = "0.4.3" 31 | -------------------------------------------------------------------------------- /crates/mutator/src/constants.rs: -------------------------------------------------------------------------------- 1 | /// Interesting values for the mutator. Taken from: 2 | /// - https://github.com/AFLplusplus/AFLplusplus/blob/stable/include/config.h#L359 3 | pub const INTERESTING_8: [u8; 29] = [ 4 | 0, // 00000000 5 | 1, // 00000001 6 | 2, // 00000010 7 | 3, // 00000011 8 | 4, // 00000100 9 | 7, // 00000111 10 | 8, // 00001000 11 | 10, // 00001010 12 | 15, // 00001111 13 | 16, // 00010000 14 | 31, // 00011111 15 | 32, // 00100000 16 | 50, // 00110010 17 | 63, // 00111111 18 | 64, // 01000000 19 | 85, // 01010101 20 | 100, // 01100100 21 | 127, // 01111111 22 | 128, // 10000000 23 | 140, // 10001100 24 | 156, // 10011100 25 | 170, // 10101010 26 | 195, // 11000011 27 | 209, // 11010001 28 | 224, // 11100000 29 | 240, // 11110000 30 | 251, // 11111011 31 | 254, // 11111110 32 | 255, // 11111111 33 | ]; 34 | -------------------------------------------------------------------------------- /crates/common/src/types.rs: -------------------------------------------------------------------------------- 1 | pub type Backend = alloy::providers::fillers::FillProvider< 2 | alloy::providers::fillers::JoinFill< 3 | alloy::providers::fillers::JoinFill< 4 | alloy::providers::Identity, 5 | alloy::providers::fillers::JoinFill< 6 | alloy::providers::fillers::GasFiller, 7 | alloy::providers::fillers::JoinFill< 8 | alloy::providers::fillers::BlobGasFiller, 9 | alloy::providers::fillers::JoinFill< 10 | alloy::providers::fillers::NonceFiller, 11 | alloy::providers::fillers::ChainIdFiller, 12 | >, 13 | >, 14 | >, 15 | >, 16 | alloy::providers::fillers::WalletFiller, 17 | >, 18 | alloy::providers::RootProvider, 19 | >; 20 | 21 | pub type PendingTransaction = alloy::providers::PendingTransactionBuilder; 22 | -------------------------------------------------------------------------------- /crates/runners/src/cache.rs: -------------------------------------------------------------------------------- 1 | use alloy::{ 2 | primitives::{Address, U256}, 3 | providers::Provider, 4 | }; 5 | use common::types::Backend; 6 | 7 | pub struct BuilderCache { 8 | pub gas_price: u128, 9 | pub max_priority_fee: u128, 10 | pub max_fee_per_blob_gas: u128, 11 | pub balance: U256, 12 | pub nonce: u64, 13 | pub chain_id: u64, 14 | } 15 | 16 | impl BuilderCache { 17 | pub async fn update(&mut self, provider: &Backend, address: Address) { 18 | let account = provider.get_account(address).await.unwrap_or_default(); 19 | 20 | self.gas_price = provider.get_gas_price().await.unwrap_or_default(); 21 | self.max_priority_fee = provider.get_max_priority_fee_per_gas().await.unwrap_or_default(); 22 | self.max_fee_per_blob_gas = provider.get_blob_base_fee().await.unwrap_or_default(); 23 | self.balance = account.balance; 24 | self.nonce = account.nonce; 25 | self.chain_id = provider.get_chain_id().await.unwrap_or_default(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bin/cli/main.rs: -------------------------------------------------------------------------------- 1 | use alloy::{hex, signers::k256::ecdsa::SigningKey, transports::http::reqwest::Url}; 2 | use app::App; 3 | use clap::Parser; 4 | 5 | #[derive(Parser)] 6 | #[command(name = "rakoon")] 7 | #[command(about = "Transaction fuzzer for the Ethereum protocol")] 8 | struct Cli { 9 | #[arg(long, help = "RPC URL to send transactions to", default_value = "http://localhost:8545")] 10 | rpc: String, 11 | #[arg( 12 | long, 13 | help = "Faucet key", 14 | //default_value = "0xcdfbe6f7602f67a97602e3e9fc24cde1cdffa88acd47745c0b84c5ff55891e1b" 15 | default_value = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" 16 | )] 17 | sk: String, 18 | #[arg(long, help = "Seed for the random number generator", default_value = "0")] 19 | seed: u64, 20 | #[arg(long, help = "Max operations per mutation", default_value = "1000")] 21 | max_operations_per_mutation: u64, 22 | } 23 | 24 | #[tokio::main] 25 | async fn main() { 26 | let cli = Cli::parse(); 27 | 28 | let rpc_url = cli.rpc.parse::().unwrap(); 29 | let sk = SigningKey::from_slice(hex::decode(cli.sk).unwrap().as_slice()).unwrap(); 30 | let seed = cli.seed; 31 | let max_operations_per_mutation = cli.max_operations_per_mutation; 32 | 33 | let mut app = App::new(rpc_url, sk, seed, max_operations_per_mutation); 34 | let _ = app.run().await.unwrap(); 35 | } 36 | -------------------------------------------------------------------------------- /crates/runners/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Display}, 3 | str::FromStr, 4 | }; 5 | 6 | pub mod al; 7 | pub mod blob; 8 | pub mod builder; 9 | pub mod cache; 10 | pub mod eip1559; 11 | pub mod eip7702; 12 | pub mod legacy; 13 | pub mod logger; 14 | pub mod random; 15 | 16 | #[derive(PartialEq, Eq, Hash, Clone, Copy)] 17 | pub enum Runner { 18 | AL, 19 | Blob, 20 | EIP1559, 21 | EIP7702, 22 | Legacy, 23 | Random, 24 | } 25 | 26 | impl Display for Runner { 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | match self { 29 | Runner::AL => write!(f, "al"), 30 | Runner::Blob => write!(f, "blob"), 31 | Runner::EIP1559 => write!(f, "eip1559"), 32 | Runner::EIP7702 => write!(f, "eip7702"), 33 | Runner::Legacy => write!(f, "legacy"), 34 | Runner::Random => write!(f, "random"), 35 | } 36 | } 37 | } 38 | 39 | impl FromStr for Runner { 40 | type Err = String; 41 | 42 | fn from_str(s: &str) -> Result { 43 | match s { 44 | "al" => Ok(Runner::AL), 45 | "blob" => Ok(Runner::Blob), 46 | "eip1559" => Ok(Runner::EIP1559), 47 | "eip7702" => Ok(Runner::EIP7702), 48 | "legacy" => Ok(Runner::Legacy), 49 | "random" => Ok(Runner::Random), 50 | _ => Err(format!("invalid runner: {}", s)), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/mutator/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rand::{Rng, SeedableRng, rngs::StdRng}; 2 | mod constants; 3 | mod operations; 4 | 5 | use operations::*; 6 | 7 | #[derive(Clone)] 8 | /// Mutator is a struct that contains the operations to mutate the input and the maximum number of 9 | /// operations per mutation. 10 | pub struct Mutator { 11 | /// The operations to mutate the input 12 | operations: Vec, 13 | /// The maximum number of operations per mutation 14 | max_operations_per_mutation: u64, 15 | /// The seed for the random number generator 16 | seed: u64, 17 | } 18 | 19 | impl Mutator { 20 | /// Creates a new `Mutator` with the given maximum number of operations per mutation and seed 21 | /// for the random number generator. 22 | pub fn new(max_operations_per_mutation: u64, seed: u64) -> Self { 23 | Self { 24 | operations: vec![ 25 | flip_bit, 26 | flip_byte, 27 | interesting, 28 | interesting_be, 29 | add, 30 | add_one, 31 | sub, 32 | sub_one, 33 | random_byte, 34 | clone_byte, 35 | swap_byte, 36 | set_zero_byte, 37 | set_one_byte, 38 | set_ff_byte, 39 | shuffle_bytes, 40 | ], 41 | max_operations_per_mutation, 42 | seed, 43 | } 44 | } 45 | 46 | /// Mutate the input 47 | pub fn mutate(&self, input: &mut [u8]) { 48 | let mut rng = StdRng::seed_from_u64(self.seed); 49 | 50 | for _ in 0..rng.random_range(0..self.max_operations_per_mutation) { 51 | self.operations[rng.random_range(0..self.operations.len())](input, &mut rng); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | rakoon 3 |
4 | 5 | - - - 6 | 7 | This is a transaction fuzzer for the Ethereum Protocol, with per-transaction custom mutators, an user interface for seeing live data from the fuzzer and a terminal to ineract with it in real time. Huge thanks to [Marius van der Wijden](https://github.com/MariusVanDerWijden) for building [tx-fuzz](https://github.com/MariusVanDerWijden/tx-fuzz), which I used as reference in many parts of this project, as well as to the [alloy team](https://github.com/alloy-rs), as I leveraged heavily on them to build this. 8 | 9 | ## Usage 10 | 11 | It is as simple as doing 12 | 13 | ```shell 14 | ./rakoon 15 | ``` 16 | 17 | and the user interface will pop-up. If there is no binary in the root of the project (it should be if I'm not stupid), then run 18 | 19 | ```shell 20 | make build 21 | ``` 22 | 23 | and it will be there. 24 | 25 | ### Commands 26 | 27 | The following commands are available in the terminal interface: 28 | 29 | #### Set Configuration 30 | - `set global rpc ` - Set the global RPC URL 31 | - `set global sk ` - Set the global private key 32 | - `set global seed ` - Set the global seed 33 | 34 | - `set rpc ` - Set RPC URL for a specific runner 35 | - `set sk ` - Set private key for a specific runner 36 | - `set seed ` - Set seed for a specific runner 37 | 38 | Where `` can be one of `al`, `blob`, `eip1559`, `eip7702`, `legacy`, `random` 39 | 40 | #### Reset Configuration 41 | - `reset global all` - Reset all global configuration 42 | - `reset global rpc` - Reset global RPC URL 43 | - `reset global sk` - Reset global private key 44 | - `reset global seed` - Reset global seed 45 | 46 | - `reset all` - Reset all configuration for a specific runner 47 | - `reset rpc` - Reset RPC URL for a specific runner 48 | - `reset sk` - Reset private key for a specific runner 49 | - `reset seed` - Reset seed for a specific runner 50 | 51 | #### Runner Control 52 | - `start` - Start all runners 53 | - `start ` - Start a specific runner 54 | - `stop` - Stop all runners 55 | - `stop ` - Stop a specific runner 56 | 57 | #### Other Commands 58 | - `exit` - Exit the application 59 | 60 | ## Hall of fame 🏅 61 | 62 | - Crash in anvil $\Rightarrow$ [link](https://github.com/foundry-rs/foundry/issues/10444) 63 | -------------------------------------------------------------------------------- /crates/mutator/src/operations.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::INTERESTING_8; 2 | use rand::{Rng, rngs::StdRng, seq::SliceRandom}; 3 | 4 | /// Flip a bit in the input 5 | pub fn flip_bit(input: &mut [u8], random: &mut StdRng) { 6 | let bit = random.random_range(0..8); 7 | let byte = random.random_range(0..input.len()); 8 | input[byte] ^= 1 << bit; 9 | } 10 | 11 | /// Flip a byte in the input 12 | pub fn flip_byte(input: &mut [u8], random: &mut StdRng) { 13 | let byte = random.random_range(0..input.len()); 14 | input[byte] ^= 0xff; 15 | } 16 | 17 | /// Replace a random byte in the input with an interesting value 18 | pub fn interesting(input: &mut [u8], random: &mut StdRng) { 19 | let idx = random.random_range(0..INTERESTING_8.len()); 20 | input[random.random_range(0..input.len())] = INTERESTING_8[idx]; 21 | } 22 | 23 | /// Replace a random byte in the input with an interesting value in big endian 24 | pub fn interesting_be(input: &mut [u8], random: &mut StdRng) { 25 | let idx = random.random_range(0..INTERESTING_8.len()); 26 | input[random.random_range(0..input.len())] = INTERESTING_8[idx].reverse_bits(); 27 | } 28 | 29 | /// Add a random byte to a random byte in the input 30 | pub fn add(input: &mut [u8], random: &mut StdRng) { 31 | let num = random.random_range(0..u8::MAX); 32 | let idx = random.random_range(0..input.len()); 33 | input[idx] = input[idx].saturating_add(num); 34 | } 35 | 36 | /// Add 1 to a random byte in the input 37 | pub fn add_one(input: &mut [u8], random: &mut StdRng) { 38 | let idx = random.random_range(0..input.len()); 39 | input[idx] = input[idx].saturating_add(1); 40 | } 41 | 42 | /// Subtract a random byte from a random byte in the input 43 | pub fn sub(input: &mut [u8], random: &mut StdRng) { 44 | let num = random.random_range(0..u8::MAX); 45 | let idx = random.random_range(0..input.len()); 46 | input[idx] = input[idx].saturating_sub(num); 47 | } 48 | 49 | /// Subtract 1 from a random byte in the input 50 | pub fn sub_one(input: &mut [u8], random: &mut StdRng) { 51 | let idx = random.random_range(0..input.len()); 52 | input[idx] = input[idx].saturating_sub(1); 53 | } 54 | 55 | /// Replace a random byte in the input with a random byte 56 | pub fn random_byte(input: &mut [u8], random: &mut StdRng) { 57 | let idx = random.random_range(0..input.len()); 58 | input[idx] ^= random.random_range(0..u8::MAX); 59 | } 60 | 61 | /// Clone a random byte in the input to a random byte in the input 62 | pub fn clone_byte(input: &mut [u8], random: &mut StdRng) { 63 | let src = random.random_range(0..input.len()); 64 | let dst = random.random_range(0..input.len()); 65 | input[dst] = input[src]; 66 | } 67 | 68 | /// Swap a random byte in the input with a random byte in the input 69 | pub fn swap_byte(input: &mut [u8], random: &mut StdRng) { 70 | let src = random.random_range(0..input.len()); 71 | let dst = random.random_range(0..input.len()); 72 | input.swap(src, dst); 73 | } 74 | 75 | /// Set a random byte in the input to 0 76 | pub fn set_zero_byte(input: &mut [u8], random: &mut StdRng) { 77 | let idx = random.random_range(0..input.len()); 78 | input[idx] = 0; 79 | } 80 | 81 | /// Set a random byte in the input to 1 82 | pub fn set_one_byte(input: &mut [u8], random: &mut StdRng) { 83 | let idx = random.random_range(0..input.len()); 84 | input[idx] = 1; 85 | } 86 | 87 | /// Set a random byte in the input to 0xff 88 | pub fn set_ff_byte(input: &mut [u8], random: &mut StdRng) { 89 | let idx = random.random_range(0..input.len()); 90 | input[idx] = 0xff; 91 | } 92 | 93 | /// Shuffle the bytes in the input 94 | pub fn shuffle_bytes(input: &mut [u8], random: &mut StdRng) { 95 | let mut bytes = input.to_vec(); 96 | bytes.shuffle(random); 97 | for (i, byte) in bytes.iter().enumerate() { 98 | input[i] = *byte; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/runners/src/logger.rs: -------------------------------------------------------------------------------- 1 | use alloy::transports::{RpcError, TransportErrorKind}; 2 | use chrono::Local; 3 | use std::{ 4 | fs::{File, OpenOptions}, 5 | io::{self, Write}, 6 | path::Path, 7 | }; 8 | 9 | /// Logger structure for writing logs to a file with timestamp 10 | pub struct Logger { 11 | file: File, 12 | runner_name: String, 13 | } 14 | 15 | impl Logger { 16 | fn ensure_directories(runner_name: &str) -> io::Result<()> { 17 | // Create logs directory if it doesn't exist 18 | std::fs::create_dir_all("logs")?; 19 | 20 | // Create reports directory and runner subdirectory if they don't exist 21 | let reports_dir = format!("reports/{}", runner_name); 22 | std::fs::create_dir_all(&reports_dir)?; 23 | 24 | Ok(()) 25 | } 26 | 27 | /// Creates a new logger instance for the specified runner 28 | /// 29 | /// # Arguments 30 | /// 31 | /// * `runner_name` - Name of the runner to be used in the log file name 32 | /// 33 | /// # Returns 34 | /// 35 | /// A Result containing the Logger instance or an IO error 36 | pub fn new(runner_name: &str) -> io::Result { 37 | // Ensure directories exist 38 | Self::ensure_directories(runner_name)?; 39 | 40 | let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); 41 | let filename = format!("logs/{}_log.log", runner_name); 42 | 43 | let mut file = 44 | OpenOptions::new().create(true).write(true).append(true).open(Path::new(&filename))?; 45 | 46 | let startup_message = format!("[{}] Logger for {} started\n", timestamp, runner_name); 47 | file.write_all(startup_message.as_bytes())?; 48 | file.flush()?; 49 | 50 | Ok(Self { file, runner_name: runner_name.to_string() }) 51 | } 52 | 53 | /// Logs a message with timestamp 54 | /// 55 | /// # Arguments 56 | /// 57 | /// * `message` - The message to log 58 | /// 59 | /// # Returns 60 | /// 61 | /// A Result indicating success or an IO error 62 | pub fn log(&mut self, message: &str) -> io::Result<()> { 63 | let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); 64 | let log_message = format!("[{}] {}\n", timestamp, message); 65 | self.file.write_all(log_message.as_bytes())?; 66 | self.file.flush()?; 67 | Ok(()) 68 | } 69 | 70 | /// Logs an error message with timestamp 71 | /// 72 | /// # Arguments 73 | /// 74 | /// * `error` - The error message to log 75 | /// 76 | /// # Returns 77 | /// 78 | /// A Result indicating success or an IO error 79 | pub fn log_error(&mut self, error: &str) -> io::Result<()> { 80 | let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string(); 81 | let log_line = format!("[{}] ERROR:{}\n", timestamp, error); 82 | 83 | self.file.write_all(log_line.as_bytes())?; 84 | self.file.flush() 85 | } 86 | 87 | pub fn generate_crash_report(&mut self, crash_data: &[u8]) -> io::Result<()> { 88 | let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); 89 | let report_filename = 90 | format!("reports/{}/crash_report_{}.txt", self.runner_name, timestamp); 91 | 92 | let mut report_file = 93 | OpenOptions::new().create(true).write(true).open(Path::new(&report_filename))?; 94 | 95 | let formatted_data = 96 | format!("Transaction that crashed the node (hex): 0x{}\n", hex::encode(crash_data)); 97 | report_file.write_all(formatted_data.as_bytes())?; 98 | 99 | report_file.flush()?; 100 | Ok(()) 101 | } 102 | 103 | pub fn is_connection_refused_error(&self, err: &RpcError) -> bool { 104 | let formatted_err = format!("{:#?}", err); 105 | formatted_err.contains("Connection refused") 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/app/src/manager.rs: -------------------------------------------------------------------------------- 1 | use crate::App; 2 | use common::errors::Error; 3 | use runners::{ 4 | Runner, Runner::*, al::ALTransactionRunner, blob::BlobTransactionRunner, 5 | eip1559::Eip1559TransactionRunner, eip7702::Eip7702TransactionRunner, 6 | legacy::LegacyTransactionRunner, random::RandomTransactionRunner, 7 | }; 8 | 9 | impl App { 10 | /// Starts a runner given its type. This function spawns a thread and 11 | /// stores its `handle` in the `handler` map. That way we can stop it 12 | /// later on by calling `abort` on the `handle`. 13 | /// 14 | /// # Arguments 15 | /// 16 | /// * `runner_type` - The type of the runner to start. 17 | pub async fn start_runner(&mut self, runner_type: Runner) -> Result<(), Error> { 18 | if *self.active_runners.get(&runner_type).unwrap_or(&false) { 19 | return Err(Error::RunnerAlreadyRunning); 20 | } 21 | 22 | let sk = self.runner_sks.get(&runner_type).unwrap_or(&self.sk).clone(); 23 | let seed = *self.runner_seeds.get(&runner_type).unwrap_or(&self.seed); 24 | let rpc = self.runner_rpcs.get(&runner_type).unwrap_or(&self.rpc_url).clone(); 25 | 26 | match runner_type { 27 | AL => { 28 | let handle = tokio::spawn({ 29 | let rpc = rpc.clone(); 30 | let sk = sk.clone(); 31 | let max_operations_per_mutation = self.max_operations_per_mutation; 32 | async move { 33 | let mut runner = 34 | ALTransactionRunner::new(rpc, sk, seed, max_operations_per_mutation) 35 | .await; 36 | runner.run().await; 37 | } 38 | }); 39 | 40 | self.handler.insert(runner_type, handle); 41 | } 42 | Blob => { 43 | let handle = tokio::spawn({ 44 | let rpc = rpc.clone(); 45 | let sk = sk.clone(); 46 | let max_operations_per_mutation = self.max_operations_per_mutation; 47 | async move { 48 | let mut runner = 49 | BlobTransactionRunner::new(rpc, sk, seed, max_operations_per_mutation) 50 | .await; 51 | runner.run().await; 52 | } 53 | }); 54 | 55 | self.handler.insert(runner_type, handle); 56 | } 57 | EIP1559 => { 58 | let handle = tokio::spawn({ 59 | let rpc = rpc.clone(); 60 | let sk = sk.clone(); 61 | let max_operations_per_mutation = self.max_operations_per_mutation; 62 | async move { 63 | let mut runner = Eip1559TransactionRunner::new( 64 | rpc, 65 | sk, 66 | seed, 67 | max_operations_per_mutation, 68 | ) 69 | .await; 70 | runner.run().await; 71 | } 72 | }); 73 | 74 | self.handler.insert(runner_type, handle); 75 | } 76 | EIP7702 => { 77 | let handle = tokio::spawn({ 78 | let rpc = rpc.clone(); 79 | let sk = sk.clone(); 80 | let max_operations_per_mutation = self.max_operations_per_mutation; 81 | async move { 82 | let mut runner = Eip7702TransactionRunner::new( 83 | rpc, 84 | sk, 85 | seed, 86 | max_operations_per_mutation, 87 | ) 88 | .await; 89 | runner.run().await; 90 | } 91 | }); 92 | 93 | self.handler.insert(runner_type, handle); 94 | } 95 | Legacy => { 96 | let handle = tokio::spawn({ 97 | let rpc = rpc.clone(); 98 | let sk = sk.clone(); 99 | let max_operations_per_mutation = self.max_operations_per_mutation; 100 | async move { 101 | let mut runner = LegacyTransactionRunner::new( 102 | rpc, 103 | sk, 104 | seed, 105 | max_operations_per_mutation, 106 | ) 107 | .await; 108 | runner.run().await; 109 | } 110 | }); 111 | 112 | self.handler.insert(runner_type, handle); 113 | } 114 | Random => { 115 | let handle = tokio::spawn({ 116 | let rpc = rpc.clone(); 117 | let sk = sk.clone(); 118 | let max_operations_per_mutation = self.max_operations_per_mutation; 119 | async move { 120 | let mut runner = RandomTransactionRunner::new( 121 | rpc, 122 | sk, 123 | seed, 124 | max_operations_per_mutation, 125 | ) 126 | .await; 127 | runner.run().await; 128 | } 129 | }); 130 | 131 | self.handler.insert(runner_type, handle); 132 | } 133 | } 134 | 135 | self.active_runners.insert(runner_type, true); 136 | if self.active_runners.values().any(|&active| active) { 137 | self.running = true; 138 | } 139 | 140 | Ok(()) 141 | } 142 | 143 | pub async fn stop_runner(&mut self, runner_type: Runner) -> Result<(), Error> { 144 | if !self.active_runners.get(&runner_type).unwrap_or(&false) { 145 | return Err(Error::RunnerAlreadyStopped); 146 | } 147 | 148 | // Get the handle for this runner and abort it 149 | if let Some(handle) = self.handler.remove(&runner_type) { 150 | // Cancel the handle to signal the runner to stop 151 | handle.abort(); 152 | } 153 | 154 | // Remove the runner from the active runners map 155 | self.active_runners.remove(&runner_type); 156 | 157 | // If all runners are stopped, set the running flag to false 158 | if self.active_runners.values().all(|&active| !active) { 159 | self.running = false; 160 | } 161 | 162 | Ok(()) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /crates/runners/src/legacy.rs: -------------------------------------------------------------------------------- 1 | use crate::{builder::Builder, cache::BuilderCache, logger::Logger}; 2 | use alloy::{ 3 | consensus::TxLegacy, 4 | primitives::{Address, TxHash}, 5 | providers::{Provider, ProviderBuilder}, 6 | rpc::types::TransactionRequest, 7 | signers::{k256::ecdsa::SigningKey, local::PrivateKeySigner}, 8 | transports::http::reqwest::Url, 9 | }; 10 | use alloy_rlp::Encodable; 11 | use common::types::Backend; 12 | use mutator::Mutator; 13 | use rand::{SeedableRng, random_bool, rngs::StdRng}; 14 | 15 | pub struct LegacyTransactionRunner { 16 | pub sk: SigningKey, 17 | pub seed: u64, 18 | pub provider: Backend, 19 | pub current_tx: Vec, 20 | pub mutator: Mutator, 21 | pub crash_counter: u64, 22 | pub running: bool, 23 | pub logger: Logger, 24 | pub cache: BuilderCache, 25 | pub sender: Address, 26 | } 27 | 28 | impl Builder for LegacyTransactionRunner { 29 | fn provider(&self) -> &Backend { 30 | &self.provider 31 | } 32 | 33 | fn cache(&self) -> &BuilderCache { 34 | &self.cache 35 | } 36 | 37 | fn cache_mut(&mut self) -> &mut BuilderCache { 38 | &mut self.cache 39 | } 40 | } 41 | 42 | impl LegacyTransactionRunner { 43 | pub async fn new( 44 | rpc_url: Url, 45 | sk: SigningKey, 46 | seed: u64, 47 | max_operations_per_mutation: u64, 48 | ) -> Self { 49 | let provider = ProviderBuilder::new() 50 | .wallet::(sk.clone().into()) 51 | .connect_http(rpc_url); 52 | 53 | let sender = Address::from_private_key(&sk); 54 | let account = provider.get_account(sender).await.unwrap_or_default(); 55 | let cache = BuilderCache { 56 | gas_price: provider.get_gas_price().await.unwrap_or_default(), 57 | max_priority_fee: provider.get_max_priority_fee_per_gas().await.unwrap_or_default(), 58 | max_fee_per_blob_gas: provider.get_blob_base_fee().await.unwrap_or_default(), 59 | balance: account.balance, 60 | nonce: account.nonce, 61 | chain_id: provider.get_chain_id().await.unwrap_or_default(), 62 | }; 63 | 64 | let mutator = Mutator::new(max_operations_per_mutation, seed); 65 | let logger = Logger::new("legacy").unwrap(); 66 | 67 | Self { 68 | sk, 69 | seed, 70 | current_tx: vec![], 71 | provider, 72 | mutator, 73 | crash_counter: 0, 74 | running: false, 75 | logger, 76 | cache, 77 | sender, 78 | } 79 | } 80 | 81 | pub async fn run(&mut self) { 82 | let mut random = StdRng::seed_from_u64(self.seed); 83 | self.running = true; 84 | 85 | loop { 86 | // 10% chance to re-generate the transaction 87 | if random_bool(0.1) || self.current_tx.is_empty() { 88 | // 50% chance to update the cache 89 | // This is to try to get further in the execution by bypassing 90 | // common checks like gas > expected and so on 91 | if random_bool(0.5) { 92 | self.cache.update(&self.provider, self.sender).await; 93 | } 94 | 95 | let (request, tx) = self.create_legacy_transaction(&mut random).await; 96 | tx.encode(&mut self.current_tx); 97 | 98 | if let Err(err) = self.provider.send_transaction_unsafe(request).await { 99 | if self.logger.is_connection_refused_error(&err) { 100 | let current_tx = self.current_tx.clone(); 101 | let _ = self.logger.generate_crash_report(¤t_tx); 102 | 103 | self.crash_counter += 1; 104 | self.running = false; 105 | 106 | break; 107 | } 108 | } 109 | } else { 110 | self.mutator.mutate(&mut self.current_tx); 111 | if let Err(err) = self 112 | .provider 113 | .client() 114 | .request::<_, TxHash>("eth_sendRawTransaction", &self.current_tx) 115 | .await 116 | { 117 | if self.logger.is_connection_refused_error(&err) { 118 | let current_tx = self.current_tx.clone(); 119 | let _ = self.logger.generate_crash_report(¤t_tx); 120 | 121 | self.crash_counter += 1; 122 | self.running = false; 123 | 124 | break; 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | pub async fn create_legacy_transaction( 132 | &self, 133 | random: &mut StdRng, 134 | ) -> (TransactionRequest, TxLegacy) { 135 | // Legacy transaction type 136 | let transaction_type = 0; 137 | 138 | let to = self.to(random); 139 | let gas_price = self.gas_price(random).await; 140 | let gas_limit = self.gas(random); 141 | let value = self.value(random).await; 142 | let input = self.input(random); 143 | let nonce = self.nonce(random).await; 144 | let chain_id = self.chain_id(random).await; 145 | 146 | let request = TransactionRequest { 147 | from: Some(self.sender), 148 | to: Some(to), 149 | gas_price: Some(gas_price), 150 | max_fee_per_gas: None, 151 | max_priority_fee_per_gas: None, 152 | max_fee_per_blob_gas: None, 153 | gas: Some(gas_limit), 154 | value: Some(value), 155 | input: input.clone(), 156 | nonce: Some(nonce), 157 | chain_id: Some(chain_id), 158 | access_list: None, 159 | transaction_type: Some(transaction_type), 160 | blob_versioned_hashes: None, 161 | sidecar: None, 162 | authorization_list: None, 163 | }; 164 | 165 | let tx = TxLegacy { 166 | to, 167 | value, 168 | chain_id: Some(chain_id), 169 | nonce, 170 | gas_price, 171 | gas_limit, 172 | input: input.into_input().unwrap(), 173 | }; 174 | 175 | (request, tx) 176 | } 177 | } 178 | 179 | #[tokio::test] 180 | async fn test_legacy_transaction_runner() { 181 | let mut rng = StdRng::seed_from_u64(1); 182 | let runner = LegacyTransactionRunner::new( 183 | "http://localhost:8545".parse::().unwrap(), 184 | SigningKey::from_slice( 185 | &alloy::hex::decode( 186 | "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", 187 | ) 188 | .unwrap(), 189 | ) 190 | .unwrap(), 191 | 1, 192 | 1000, 193 | ) 194 | .await; 195 | let tx = runner.create_legacy_transaction(&mut rng).await; 196 | println!("tx: {:#?}", &tx); 197 | } 198 | -------------------------------------------------------------------------------- /crates/runners/src/al.rs: -------------------------------------------------------------------------------- 1 | use crate::{builder::Builder, cache::BuilderCache, logger::Logger}; 2 | use alloy::{ 3 | consensus::TxEip2930, 4 | primitives::{Address, TxHash}, 5 | providers::{Provider, ProviderBuilder}, 6 | rpc::types::TransactionRequest, 7 | signers::{k256::ecdsa::SigningKey, local::PrivateKeySigner}, 8 | transports::http::reqwest::Url, 9 | }; 10 | use alloy_rlp::Encodable; 11 | use common::types::Backend; 12 | use mutator::Mutator; 13 | use rand::{SeedableRng, random_bool, rngs::StdRng}; 14 | 15 | pub struct ALTransactionRunner { 16 | pub sk: SigningKey, 17 | pub seed: u64, 18 | pub provider: Backend, 19 | pub current_tx: Vec, 20 | pub mutator: Mutator, 21 | pub crash_counter: u64, 22 | pub running: bool, 23 | pub logger: Logger, 24 | pub cache: BuilderCache, 25 | pub sender: Address, 26 | } 27 | 28 | impl Builder for ALTransactionRunner { 29 | fn provider(&self) -> &Backend { 30 | &self.provider 31 | } 32 | 33 | fn cache(&self) -> &BuilderCache { 34 | &self.cache 35 | } 36 | 37 | fn cache_mut(&mut self) -> &mut BuilderCache { 38 | &mut self.cache 39 | } 40 | } 41 | 42 | impl ALTransactionRunner { 43 | pub async fn new( 44 | rpc_url: Url, 45 | sk: SigningKey, 46 | seed: u64, 47 | max_operations_per_mutation: u64, 48 | ) -> Self { 49 | let provider = ProviderBuilder::new() 50 | .wallet::(sk.clone().into()) 51 | .connect_http(rpc_url); 52 | 53 | let sender = Address::from_private_key(&sk); 54 | let account = provider.get_account(sender).await.unwrap_or_default(); 55 | let cache = BuilderCache { 56 | gas_price: provider.get_gas_price().await.unwrap_or_default(), 57 | max_priority_fee: provider.get_max_priority_fee_per_gas().await.unwrap_or_default(), 58 | max_fee_per_blob_gas: provider.get_blob_base_fee().await.unwrap_or_default(), 59 | balance: account.balance, 60 | nonce: account.nonce, 61 | chain_id: provider.get_chain_id().await.unwrap_or_default(), 62 | }; 63 | 64 | let mutator = Mutator::new(max_operations_per_mutation, seed); 65 | let logger = Logger::new("al").unwrap(); 66 | 67 | Self { 68 | sk, 69 | seed, 70 | current_tx: vec![], 71 | provider, 72 | mutator, 73 | crash_counter: 0, 74 | running: false, 75 | logger, 76 | cache, 77 | sender, 78 | } 79 | } 80 | 81 | pub async fn run(&mut self) { 82 | let mut random = StdRng::seed_from_u64(self.seed); 83 | self.running = true; 84 | 85 | loop { 86 | // 10% chance to re-generate the transaction 87 | if random_bool(0.1) || self.current_tx.is_empty() { 88 | // 50% chance to update the cache 89 | // This is to try to get further in the execution by bypassing 90 | // common checks like gas > expected and so on 91 | if random_bool(0.5) { 92 | self.cache.update(&self.provider, self.sender).await; 93 | } 94 | 95 | let (request, tx) = self.create_access_list_transaction(&mut random).await; 96 | tx.encode(&mut self.current_tx); 97 | 98 | if let Err(err) = self.provider.send_transaction_unsafe(request).await { 99 | if self.logger.is_connection_refused_error(&err) { 100 | let current_tx = self.current_tx.clone(); 101 | let _ = self.logger.generate_crash_report(¤t_tx); 102 | 103 | self.crash_counter += 1; 104 | self.running = false; 105 | 106 | break; 107 | } 108 | } 109 | } else { 110 | self.mutator.mutate(&mut self.current_tx); 111 | if let Err(err) = self 112 | .provider 113 | .client() 114 | .request::<_, TxHash>("eth_sendRawTransaction", &self.current_tx) 115 | .await 116 | { 117 | if self.logger.is_connection_refused_error(&err) { 118 | let current_tx = self.current_tx.clone(); 119 | let _ = self.logger.generate_crash_report(¤t_tx); 120 | 121 | self.crash_counter += 1; 122 | self.running = false; 123 | 124 | break; 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | pub async fn create_access_list_transaction( 132 | &self, 133 | random: &mut StdRng, 134 | ) -> (TransactionRequest, TxEip2930) { 135 | // EIP-2930 transaction type 136 | let transaction_type = 1; 137 | 138 | let to = self.to(random); 139 | let gas_price = self.gas_price(random).await; 140 | let gas_limit = self.gas(random); 141 | let value = self.value(random).await; 142 | let input = self.input(random); 143 | let nonce = self.nonce(random).await; 144 | let chain_id = self.chain_id(random).await; 145 | let access_list = self.access_list(random); 146 | 147 | let request = TransactionRequest { 148 | from: Some(self.sender), 149 | to: Some(to), 150 | gas_price: Some(gas_price), 151 | max_fee_per_gas: None, 152 | max_priority_fee_per_gas: None, 153 | max_fee_per_blob_gas: None, 154 | gas: Some(gas_limit), 155 | value: Some(value), 156 | input: input.clone(), 157 | nonce: Some(nonce), 158 | chain_id: Some(chain_id), 159 | access_list: Some(access_list.clone()), 160 | transaction_type: Some(transaction_type), 161 | blob_versioned_hashes: None, 162 | sidecar: None, 163 | authorization_list: None, 164 | }; 165 | 166 | let tx = TxEip2930 { 167 | to, 168 | gas_price, 169 | gas_limit, 170 | value, 171 | chain_id, 172 | nonce, 173 | access_list, 174 | input: input.into_input().unwrap(), 175 | }; 176 | 177 | (request, tx) 178 | } 179 | } 180 | 181 | #[tokio::test] 182 | async fn test_access_list_transaction_runner() { 183 | let mut rng = StdRng::seed_from_u64(1); 184 | let runner = ALTransactionRunner::new( 185 | "http://localhost:8545".parse::().unwrap(), 186 | SigningKey::from_slice( 187 | &alloy::hex::decode( 188 | "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", 189 | ) 190 | .unwrap(), 191 | ) 192 | .unwrap(), 193 | 1, 194 | 1000, 195 | ) 196 | .await; 197 | let (request, _) = runner.create_access_list_transaction(&mut rng).await; 198 | println!("tx: {:#?}", &request); 199 | } 200 | -------------------------------------------------------------------------------- /crates/runners/src/eip1559.rs: -------------------------------------------------------------------------------- 1 | use crate::{builder::Builder, cache::BuilderCache, logger::Logger}; 2 | use alloy::{ 3 | consensus::TxEip1559, 4 | primitives::{Address, TxHash}, 5 | providers::{Provider, ProviderBuilder}, 6 | rpc::types::TransactionRequest, 7 | signers::{k256::ecdsa::SigningKey, local::PrivateKeySigner}, 8 | transports::http::reqwest::Url, 9 | }; 10 | use alloy_rlp::Encodable; 11 | use common::types::Backend; 12 | use mutator::Mutator; 13 | use rand::{SeedableRng, random_bool, rngs::StdRng}; 14 | 15 | pub struct Eip1559TransactionRunner { 16 | pub sk: SigningKey, 17 | pub seed: u64, 18 | pub provider: Backend, 19 | pub current_tx: Vec, 20 | pub mutator: Mutator, 21 | pub crash_counter: u64, 22 | pub running: bool, 23 | pub logger: Logger, 24 | pub cache: BuilderCache, 25 | pub sender: Address, 26 | } 27 | 28 | impl Builder for Eip1559TransactionRunner { 29 | fn provider(&self) -> &Backend { 30 | &self.provider 31 | } 32 | 33 | fn cache(&self) -> &BuilderCache { 34 | &self.cache 35 | } 36 | 37 | fn cache_mut(&mut self) -> &mut BuilderCache { 38 | &mut self.cache 39 | } 40 | } 41 | 42 | impl Eip1559TransactionRunner { 43 | pub async fn new( 44 | rpc_url: Url, 45 | sk: SigningKey, 46 | seed: u64, 47 | max_operations_per_mutation: u64, 48 | ) -> Self { 49 | let provider = ProviderBuilder::new() 50 | .wallet::(sk.clone().into()) 51 | .connect_http(rpc_url); 52 | 53 | let sender = Address::from_private_key(&sk); 54 | let account = provider.get_account(sender).await.unwrap_or_default(); 55 | let cache = BuilderCache { 56 | gas_price: provider.get_gas_price().await.unwrap_or_default(), 57 | max_priority_fee: provider.get_max_priority_fee_per_gas().await.unwrap_or_default(), 58 | max_fee_per_blob_gas: provider.get_blob_base_fee().await.unwrap_or_default(), 59 | balance: account.balance, 60 | nonce: account.nonce, 61 | chain_id: provider.get_chain_id().await.unwrap_or_default(), 62 | }; 63 | 64 | let mutator = Mutator::new(max_operations_per_mutation, seed); 65 | let logger = Logger::new("eip1559").unwrap(); 66 | 67 | Self { 68 | sk, 69 | seed, 70 | current_tx: vec![], 71 | provider, 72 | mutator, 73 | crash_counter: 0, 74 | running: false, 75 | logger, 76 | cache, 77 | sender, 78 | } 79 | } 80 | 81 | pub async fn run(&mut self) { 82 | let mut random = StdRng::seed_from_u64(self.seed); 83 | self.running = true; 84 | 85 | loop { 86 | // 10% chance to re-generate the transaction 87 | if random_bool(0.1) || self.current_tx.is_empty() { 88 | // 50% chance to update the cache 89 | // This is to try to get further in the execution by bypassing 90 | // common checks like gas > expected and so on 91 | if random_bool(0.5) { 92 | self.cache.update(&self.provider, self.sender).await; 93 | } 94 | 95 | let (request, tx) = self.create_eip1559_transaction(&mut random).await; 96 | tx.encode(&mut self.current_tx); 97 | 98 | if let Err(err) = self.provider.send_transaction_unsafe(request).await { 99 | if self.logger.is_connection_refused_error(&err) { 100 | let current_tx = self.current_tx.clone(); 101 | let _ = self.logger.generate_crash_report(¤t_tx); 102 | 103 | self.crash_counter += 1; 104 | self.running = false; 105 | 106 | break; 107 | } 108 | } 109 | } else { 110 | self.mutator.mutate(&mut self.current_tx); 111 | if let Err(err) = self 112 | .provider 113 | .client() 114 | .request::<_, TxHash>("eth_sendRawTransaction", &self.current_tx) 115 | .await 116 | { 117 | if self.logger.is_connection_refused_error(&err) { 118 | let current_tx = self.current_tx.clone(); 119 | let _ = self.logger.generate_crash_report(¤t_tx); 120 | 121 | self.crash_counter += 1; 122 | self.running = false; 123 | 124 | break; 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | pub async fn create_eip1559_transaction( 132 | &self, 133 | random: &mut StdRng, 134 | ) -> (TransactionRequest, TxEip1559) { 135 | // EIP-1559 transaction type 136 | let transaction_type = 2; 137 | 138 | let to = self.to(random); 139 | let max_fee_per_gas = self.max_fee_per_gas(random); 140 | let max_priority_fee_per_gas = self.max_priority_fee_per_gas(random).await; 141 | let gas_limit = self.gas(random); 142 | let value = self.value(random).await; 143 | let input = self.input(random); 144 | let nonce = self.nonce(random).await; 145 | let chain_id = self.chain_id(random).await; 146 | let access_list = self.access_list(random); 147 | 148 | let request = TransactionRequest { 149 | from: Some(self.sender), 150 | to: Some(to), 151 | gas_price: None, 152 | max_fee_per_gas: Some(max_fee_per_gas), 153 | max_priority_fee_per_gas: Some(max_priority_fee_per_gas), 154 | max_fee_per_blob_gas: None, 155 | gas: Some(gas_limit), 156 | value: Some(value), 157 | input: input.clone(), 158 | nonce: Some(nonce), 159 | chain_id: Some(chain_id), 160 | access_list: Some(access_list.clone()), 161 | transaction_type: Some(transaction_type), 162 | blob_versioned_hashes: None, 163 | sidecar: None, 164 | authorization_list: None, 165 | }; 166 | 167 | let tx = TxEip1559 { 168 | to, 169 | gas_limit, 170 | value, 171 | chain_id, 172 | nonce, 173 | access_list, 174 | input: input.into_input().unwrap(), 175 | max_fee_per_gas, 176 | max_priority_fee_per_gas, 177 | }; 178 | 179 | (request, tx) 180 | } 181 | } 182 | 183 | #[tokio::test] 184 | async fn test_eip1559_transaction_runner() { 185 | let mut rng = StdRng::seed_from_u64(1); 186 | let runner = Eip1559TransactionRunner::new( 187 | "http://localhost:8545".parse::().unwrap(), 188 | SigningKey::from_slice( 189 | &alloy::hex::decode( 190 | "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", 191 | ) 192 | .unwrap(), 193 | ) 194 | .unwrap(), 195 | 1, 196 | 1000, 197 | ) 198 | .await; 199 | let tx = runner.create_eip1559_transaction(&mut rng).await; 200 | println!("tx: {:#?}", &tx); 201 | } 202 | -------------------------------------------------------------------------------- /crates/runners/src/eip7702.rs: -------------------------------------------------------------------------------- 1 | use crate::{builder::Builder, cache::BuilderCache, logger::Logger}; 2 | use alloy::{ 3 | consensus::TxEip7702, 4 | primitives::{Address, TxHash}, 5 | providers::{Provider, ProviderBuilder}, 6 | rpc::types::TransactionRequest, 7 | signers::{k256::ecdsa::SigningKey, local::PrivateKeySigner}, 8 | transports::http::reqwest::Url, 9 | }; 10 | use alloy_rlp::Encodable; 11 | use common::types::Backend; 12 | use mutator::Mutator; 13 | use rand::{SeedableRng, random_bool, rngs::StdRng}; 14 | 15 | pub struct Eip7702TransactionRunner { 16 | pub sk: SigningKey, 17 | pub seed: u64, 18 | pub provider: Backend, 19 | pub current_tx: Vec, 20 | pub mutator: Mutator, 21 | pub crash_counter: u64, 22 | pub running: bool, 23 | pub logger: Logger, 24 | pub cache: BuilderCache, 25 | pub sender: Address, 26 | } 27 | 28 | impl Builder for Eip7702TransactionRunner { 29 | fn provider(&self) -> &Backend { 30 | &self.provider 31 | } 32 | 33 | fn cache(&self) -> &BuilderCache { 34 | &self.cache 35 | } 36 | 37 | fn cache_mut(&mut self) -> &mut BuilderCache { 38 | &mut self.cache 39 | } 40 | } 41 | 42 | impl Eip7702TransactionRunner { 43 | pub async fn new( 44 | rpc_url: Url, 45 | sk: SigningKey, 46 | seed: u64, 47 | max_operations_per_mutation: u64, 48 | ) -> Self { 49 | let provider = ProviderBuilder::new() 50 | .wallet::(sk.clone().into()) 51 | .connect_http(rpc_url); 52 | 53 | let sender = Address::from_private_key(&sk); 54 | let account = provider.get_account(sender).await.unwrap_or_default(); 55 | let cache = BuilderCache { 56 | gas_price: provider.get_gas_price().await.unwrap_or_default(), 57 | max_priority_fee: provider.get_max_priority_fee_per_gas().await.unwrap_or_default(), 58 | max_fee_per_blob_gas: provider.get_blob_base_fee().await.unwrap_or_default(), 59 | balance: account.balance, 60 | nonce: account.nonce, 61 | chain_id: provider.get_chain_id().await.unwrap_or_default(), 62 | }; 63 | 64 | let mutator = Mutator::new(max_operations_per_mutation, seed); 65 | let logger = Logger::new("eip7702").unwrap(); 66 | 67 | Self { 68 | sk, 69 | seed, 70 | current_tx: vec![], 71 | provider, 72 | mutator, 73 | crash_counter: 0, 74 | running: false, 75 | logger, 76 | cache, 77 | sender, 78 | } 79 | } 80 | 81 | pub async fn run(&mut self) { 82 | let mut random = StdRng::seed_from_u64(self.seed); 83 | self.running = true; 84 | 85 | loop { 86 | // 10% chance to re-generate the transaction 87 | if random_bool(0.1) || self.current_tx.is_empty() { 88 | // 50% chance to update the cache 89 | // This is to try to get further in the execution by bypassing 90 | // common checks like gas > expected and so on 91 | if random_bool(0.5) { 92 | self.cache.update(&self.provider, self.sender).await; 93 | } 94 | 95 | let (request, tx) = self.create_eip7702_transaction(&mut random).await; 96 | tx.encode(&mut self.current_tx); 97 | 98 | if let Err(err) = self.provider.send_transaction_unsafe(request).await { 99 | if self.logger.is_connection_refused_error(&err) { 100 | let current_tx = self.current_tx.clone(); 101 | let _ = self.logger.generate_crash_report(¤t_tx); 102 | 103 | self.crash_counter += 1; 104 | self.running = false; 105 | 106 | break; 107 | } 108 | } 109 | } else { 110 | self.mutator.mutate(&mut self.current_tx); 111 | if let Err(err) = self 112 | .provider 113 | .client() 114 | .request::<_, TxHash>("eth_sendRawTransaction", &self.current_tx) 115 | .await 116 | { 117 | if self.logger.is_connection_refused_error(&err) { 118 | let current_tx = self.current_tx.clone(); 119 | let _ = self.logger.generate_crash_report(¤t_tx); 120 | 121 | self.crash_counter += 1; 122 | self.running = false; 123 | 124 | break; 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | pub async fn create_eip7702_transaction( 132 | &self, 133 | random: &mut StdRng, 134 | ) -> (TransactionRequest, TxEip7702) { 135 | // EIP-7702 transaction type 136 | let transaction_type = 4; 137 | 138 | let to = self.to(random); 139 | let max_fee_per_gas = self.max_fee_per_gas(random); 140 | let max_priority_fee_per_gas = self.max_priority_fee_per_gas(random).await; 141 | let gas_limit = self.gas(random); 142 | let value = self.value(random).await; 143 | let input = self.input(random); 144 | let nonce = self.nonce(random).await; 145 | let chain_id = self.chain_id(random).await; 146 | let access_list = self.access_list(random); 147 | let authorization_list = self.authorization_list(random); 148 | 149 | let request = TransactionRequest { 150 | from: Some(self.sender), 151 | to: Some(to), 152 | gas_price: None, 153 | max_fee_per_gas: Some(max_fee_per_gas), 154 | max_priority_fee_per_gas: Some(max_priority_fee_per_gas), 155 | max_fee_per_blob_gas: None, 156 | gas: Some(gas_limit), 157 | value: Some(value), 158 | input: input.clone(), 159 | nonce: Some(nonce), 160 | chain_id: Some(chain_id), 161 | access_list: Some(access_list.clone()), 162 | transaction_type: Some(transaction_type), 163 | blob_versioned_hashes: None, 164 | sidecar: None, 165 | authorization_list: Some(authorization_list.clone()), 166 | }; 167 | 168 | let tx = TxEip7702 { 169 | to: to.into_to().unwrap_or_else(|| Address::ZERO), 170 | gas_limit, 171 | value, 172 | chain_id, 173 | nonce, 174 | max_fee_per_gas, 175 | max_priority_fee_per_gas, 176 | access_list, 177 | authorization_list, 178 | input: input.into_input().unwrap(), 179 | }; 180 | 181 | (request, tx) 182 | } 183 | } 184 | 185 | #[tokio::test] 186 | async fn test_eip7702_transaction_runner() { 187 | let mut rng = StdRng::seed_from_u64(1); 188 | let runner = Eip7702TransactionRunner::new( 189 | "http://localhost:8545".parse::().unwrap(), 190 | SigningKey::from_slice( 191 | &alloy::hex::decode( 192 | "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", 193 | ) 194 | .unwrap(), 195 | ) 196 | .unwrap(), 197 | 1, 198 | 1000, 199 | ) 200 | .await; 201 | let tx = runner.create_eip7702_transaction(&mut rng).await; 202 | println!("tx: {:#?}", &tx); 203 | } 204 | -------------------------------------------------------------------------------- /crates/runners/src/blob.rs: -------------------------------------------------------------------------------- 1 | use crate::{builder::Builder, cache::BuilderCache, logger::Logger}; 2 | use alloy::{ 3 | consensus::{TxEip4844, TxEip4844WithSidecar, transaction::RlpEcdsaEncodableTx}, 4 | primitives::{Address, TxHash}, 5 | providers::{Provider, ProviderBuilder}, 6 | rpc::types::TransactionRequest, 7 | signers::{k256::ecdsa::SigningKey, local::PrivateKeySigner}, 8 | transports::http::reqwest::Url, 9 | }; 10 | use common::types::Backend; 11 | use mutator::Mutator; 12 | use rand::{SeedableRng, random_bool, rngs::StdRng}; 13 | 14 | pub struct BlobTransactionRunner { 15 | pub sk: SigningKey, 16 | pub seed: u64, 17 | pub provider: Backend, 18 | pub current_tx: Vec, 19 | pub mutator: Mutator, 20 | pub crash_counter: u64, 21 | pub running: bool, 22 | pub logger: Logger, 23 | pub cache: BuilderCache, 24 | pub sender: Address, 25 | } 26 | 27 | impl Builder for BlobTransactionRunner { 28 | fn provider(&self) -> &Backend { 29 | &self.provider 30 | } 31 | 32 | fn cache(&self) -> &BuilderCache { 33 | &self.cache 34 | } 35 | 36 | fn cache_mut(&mut self) -> &mut BuilderCache { 37 | &mut self.cache 38 | } 39 | } 40 | 41 | impl BlobTransactionRunner { 42 | pub async fn new( 43 | rpc_url: Url, 44 | sk: SigningKey, 45 | seed: u64, 46 | max_operations_per_mutation: u64, 47 | ) -> Self { 48 | let provider = ProviderBuilder::new() 49 | .wallet::(sk.clone().into()) 50 | .connect_http(rpc_url); 51 | 52 | let sender = Address::from_private_key(&sk); 53 | let account = provider.get_account(sender).await.unwrap_or_default(); 54 | let cache = BuilderCache { 55 | gas_price: provider.get_gas_price().await.unwrap_or_default(), 56 | max_priority_fee: provider.get_max_priority_fee_per_gas().await.unwrap_or_default(), 57 | max_fee_per_blob_gas: provider.get_blob_base_fee().await.unwrap_or_default(), 58 | balance: account.balance, 59 | nonce: account.nonce, 60 | chain_id: provider.get_chain_id().await.unwrap_or_default(), 61 | }; 62 | 63 | let mutator = Mutator::new(max_operations_per_mutation, seed); 64 | let logger = Logger::new("blob").unwrap(); 65 | 66 | Self { 67 | sk, 68 | seed, 69 | current_tx: vec![], 70 | provider, 71 | mutator, 72 | crash_counter: 0, 73 | running: false, 74 | logger, 75 | cache, 76 | sender, 77 | } 78 | } 79 | 80 | pub async fn run(&mut self) { 81 | let mut random = StdRng::seed_from_u64(self.seed); 82 | self.running = true; 83 | 84 | loop { 85 | // 10% chance to re-generate the transaction 86 | if random_bool(0.1) || self.current_tx.is_empty() { 87 | // 50% chance to update the cache 88 | // This is to try to get further in the execution by bypassing 89 | // common checks like gas > expected and so on 90 | if random_bool(0.5) { 91 | self.cache.update(&self.provider, self.sender).await; 92 | } 93 | 94 | let (request, tx) = self.create_blob_transaction(&mut random).await; 95 | tx.rlp_encode(&mut self.current_tx); 96 | 97 | if let Err(err) = self.provider.send_transaction_unsafe(request).await { 98 | if self.logger.is_connection_refused_error(&err) { 99 | let current_tx = self.current_tx.clone(); 100 | let _ = self.logger.generate_crash_report(¤t_tx); 101 | 102 | self.crash_counter += 1; 103 | self.running = false; 104 | 105 | break; 106 | } 107 | } 108 | } else { 109 | self.mutator.mutate(&mut self.current_tx); 110 | if let Err(err) = self 111 | .provider 112 | .client() 113 | .request::<_, TxHash>("eth_sendRawTransaction", &self.current_tx) 114 | .await 115 | { 116 | if self.logger.is_connection_refused_error(&err) { 117 | let current_tx = self.current_tx.clone(); 118 | let _ = self.logger.generate_crash_report(¤t_tx); 119 | 120 | self.crash_counter += 1; 121 | self.running = false; 122 | 123 | break; 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | pub async fn create_blob_transaction( 131 | &self, 132 | random: &mut StdRng, 133 | ) -> (TransactionRequest, TxEip4844WithSidecar) { 134 | // EIP-4844 transaction type 135 | let transaction_type = 3; 136 | 137 | let to = self.to(random); 138 | let max_fee_per_gas = self.max_fee_per_gas(random); 139 | let max_priority_fee_per_gas = self.max_priority_fee_per_gas(random).await; 140 | let max_fee_per_blob_gas = self.max_fee_per_blob_gas(random).await; 141 | let gas_limit = self.gas(random); 142 | let value = self.value(random).await; 143 | let input = self.input(random); 144 | let nonce = self.nonce(random).await; 145 | let chain_id = self.chain_id(random).await; 146 | let access_list = self.access_list(random); 147 | let blob_versioned_hashes = self.blob_versioned_hashes(random); 148 | let sidecar = self.sidecar(random); 149 | 150 | let request = TransactionRequest { 151 | from: Some(self.sender), 152 | to: Some(to), 153 | gas_price: None, 154 | max_fee_per_gas: Some(max_fee_per_gas), 155 | max_priority_fee_per_gas: Some(max_priority_fee_per_gas), 156 | max_fee_per_blob_gas: Some(max_fee_per_blob_gas), 157 | gas: Some(gas_limit), 158 | value: Some(value), 159 | input: input.clone(), 160 | nonce: Some(nonce), 161 | chain_id: Some(chain_id), 162 | access_list: Some(access_list.clone()), 163 | transaction_type: Some(transaction_type), 164 | blob_versioned_hashes: Some(blob_versioned_hashes.clone()), 165 | sidecar: Some(sidecar.clone()), 166 | authorization_list: None, 167 | }; 168 | 169 | let tx = TxEip4844 { 170 | to: to.into_to().unwrap_or_else(|| Address::ZERO), 171 | chain_id, 172 | nonce, 173 | max_fee_per_gas, 174 | max_priority_fee_per_gas, 175 | value, 176 | access_list, 177 | blob_versioned_hashes, 178 | max_fee_per_blob_gas, 179 | input: input.into_input().unwrap(), 180 | gas_limit, 181 | }; 182 | 183 | let tx_with_sidecar = TxEip4844WithSidecar { tx, sidecar }; 184 | 185 | (request, tx_with_sidecar) 186 | } 187 | } 188 | 189 | #[tokio::test] 190 | async fn test_blob_transaction_runner() { 191 | let mut rng = StdRng::seed_from_u64(1); 192 | let runner = BlobTransactionRunner::new( 193 | "http://localhost:8545".parse::().unwrap(), 194 | SigningKey::from_slice( 195 | &alloy::hex::decode( 196 | "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", 197 | ) 198 | .unwrap(), 199 | ) 200 | .unwrap(), 201 | 1, 202 | 1000, 203 | ) 204 | .await; 205 | let tx = runner.create_blob_transaction(&mut rng).await; 206 | println!("tx: {:#?}", &tx); 207 | } 208 | -------------------------------------------------------------------------------- /crates/common/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const STATIC_KEYS: [&str; 100] = [ 2 | "0xaf5ead4413ff4b78bc94191a2926ae9ccbec86ce099d65aaf469e9eb1a0fa87f", 3 | "0xe63135ee5310c0b34c551e4683ad926dce90062b15e43275f9189b0f29bc784c", 4 | "0xc216a7b5048e6ea2437b20bc9c7f9a57cc8aefd5aaf6a991c4db407218ed9e77", 5 | "0xc29d916f5b6ddd0aa2c827ab7333e40e91fda9ca980332b3c60cae5b7263dae7", 6 | "0xde1013a3fcdf8b204f90692478254e78126f9763f475cdaba9fc6a1abfb97db3", 7 | "0xb56fd1fd33b71c508e92bf71fae6e92c76fcd5c0df37a39ff3caa244ddba3c0f", 8 | "0x10ee939f126a0c5fc3c3cc0241cd945aa88f57eef36bde8707db51ceecfd9134", 9 | "0x03210fac527544d5b49b89763121b476c4ab66908b345916d6ad2c7740f23803", 10 | "0x8a6ccbba94844d3951c1e5581df9f8f87de8106b995a3a664d9130a2b72a4b96", 11 | "0x831a53a7994ac56f6c5d99d6371d7a8686f385995da2592aac82dda8b008b454", 12 | "0x5a1f678991d52ca86f2b0403727d19887a261199fd88908bde464f7bf13aa50b", 13 | "0x9823c88ce58b714d576741ae3b97c7f8445209e73596d7588bc5101171e863f4", 14 | "0x0bd4b408eb720ecc526cf72c88180a60102dd7fd049a5ad04e367c0d9bbc843e", 15 | "0x4e5ae855d26b0fdc601c8749ad578ba1ddd714997c40e0483479f0f9ff61c51d", 16 | "0xa0a0419689b82172eb4b9ee2670fef5e1bfa22909b51b7e4f9ab8a9719045a43", 17 | "0x7be763c508ccfd597ab4854614c55162506d454cd41627c46b264483bb8d2138", 18 | "0x5d6f77078b8f61d0258737a29c9d8fe347d70af006c109c5df91ae61a85be52b", 19 | "0x6737e36a661e8de28b5c448063a936bd6c310eab319338f4c503b8f18e935007", 20 | "0xbd5d6497a71ea419b0d0dc538016c238b8a6a630f6cdda299fcc4ce85858f95b", 21 | "0x8848fc11b20202e9f702ea05eed08e509eb8d5e32e61454f114bf2085392df75", 22 | "0x2b7aad673e69b9e4b2d7963ef27e503999f0bd5ff3e63f292e188d2e7df2fe60", 23 | "0xd2254db4e9d74fd2ab7c296f6bc06ab6beb11d8e3f685a582efc2b5e2cc8f86c", 24 | "0x5477ebb68a387dc9d4cf2c2b194bed9a027e7f817bd2cac553aca9fe1ec923ad", 25 | "0xb68a0d9d69df9697ce2c8e4e3a629e7400ddb88a93879a48788c8e8404b2ff90", 26 | "0xd2db07c60da1bf2048b84c1e09fe4d5bb1b6d0b0eb06bef801e1c2bac1c93d76", 27 | "0x9e759f9762cb967f96fe437cfa432e2889b2f972ca9f382756efb4998188be12", 28 | "0x18b886f1e77682ae7a92e9d1c29c13acfb2f493a69723b156510711526654e4f", 29 | "0x907c2e461495607062e0a7ad8bdb29d7129209ba1accbb478dbd3dee7671a1c8", 30 | "0xa63e812d650015a9ec0fc94c09b02cc9425e3e197be4a41f1b44a869dd3adace", 31 | "0xc9a3d46fa54409b795df80a363f43ae31e0c7d15f7d4c5062ce88ae7b78124b8", 32 | "0x6d41eb903d4f5b21e29a8d8558be7dce002e1b23298d2df7cee4dfdaff5b5980", 33 | "0xe969e9be3a7e87dc29699f61d5566c79d95803779575df98d054f4207b363333", 34 | "0xbf82a18972fadc7bf60d8c5bcbd37c8a55fe0cfbb17106e0617ebe6999b2bb61", 35 | "0x011b3a4adb79e6b372972d5a66ea1acdc44b1ca6ea9985af90fda4a12622926b", 36 | "0x43034269f49a0963cd45473c22d18693ebbd924f515a49b6fd9190dc96ec60de", 37 | "0x2a36981f40b25474da25277593836451c8ddc0a4fdd9131cd82dcef003a1c4ce", 38 | "0x59e57a2b3739c119d94e4e2ecf5c0f8430241e59d27f386f17d050d30f1d5d99", 39 | "0x8d6eb80f206eec85c773585295f850e159a27ba148360a34bb3355caab17f1b2", 40 | "0x1491bd992bff53671dd787070fdd54122c395690e248dc4eb32b0fb942a17cc2", 41 | "0xb7f637ebf0faef160984b039994b75fbec1714128eb1feafe92ce3f7e54bbbae", 42 | "0xfd0a5e72904e4c1497e315896fe6918c513f76503377005b70cf30dbd705fc50", 43 | "0x760f74cbbc74c4cdc521abcdbe8ae519091311a1cd3dfd04559848bf94a4af71", 44 | "0x4bc76ef2f36f8988d1c52eec26be1d31f212781bf918e57406a5e8ad14262c36", 45 | "0x716cce68bb7dace09415047aff1cf90af99ac6b81c128eb6c80ad9b739c3fc47", 46 | "0xba9d2c4fed88860a2fb2103590337e71e6720d4323e64dc24f1d9f2f98023c28", 47 | "0xc9eb9929bec8348030b917fca6e0c4a4b08059c831e9c40250eddce6a942d61f", 48 | "0xcbd0db2dabc113c02254bc50efd457bcc446338cde9c7f5c93be4f155541bf8e", 49 | "0x0a6c4e2207afd4c74d78883c4ef8c526dd164b67faf8c8da928d4a7c694fa49c", 50 | "0xde664e3cde706b7dfc33e1c561e9c63f6223c68668507a04804644aa7f56c8ce", 51 | "0xbefc32493440df7b3a3951e824570f69b6fe8947e5714ed4da101ddcf33e0f25", 52 | "0x8971997fb5d1a00599d30af8fe169a47e4d17d3efffefba52839537b89f54bb1", 53 | "0x5011902bd739d52f5d5c0e2bd382fc81350eb2c4f50aa0f1ce973e5fc41378e6", 54 | "0x3d371359b160afaeaba364bdcae5a6639699a513198ebb12fb3ee1f215e34e49", 55 | "0x496a267275fc974aac6d3f46921dd6a08441c9f1dbb9861dc5d8210a1d52053a", 56 | "0xf2e1e9548e5c15a9d21189ee2766770f0245b238499526fe538b505c7a159774", 57 | "0x48f59be041558af55f36dfc7ec5a7950d6482d10f8c0977b71a0b86e9c0a4767", 58 | "0xedeb92913dd239b28906688cae03d3790508c7df9b15fa6ef9abb4042c659985", 59 | "0xb4094d84262c1039240c21f4d8364b7e802940ba734573e9d8e7566573880c41", 60 | "0xdec2b48cfcd0273ad1f23404e0f9e9d3fc4fa1721c317d933484b9015858ba5d", 61 | "0x7dbd4f809ecdaa1ab4dcb40f6a24ddc4561fe264c3fe405e3dfcb7eb44f6275d", 62 | "0xe3cbb18271f2be064b2148bd15449f2cb92169cecee82ace180e38efef35ab99", 63 | "0xbfcf51ca0f25962a5a121234999a96818f94580fc7cfddabc0d0c6dbb98ba8a1", 64 | "0xfb2746579a4129d17c58a796832e30eacc3f329fec12babccc69c49601d93e06", 65 | "0x3ecffcca6aef54316e12532a4284829334a0d0ea98921ece4b8b296133a7d454", 66 | "0x56340e56c551897a8e4f6206b4dd9c581c242b39f3417a0460b6b1389c1db1cc", 67 | "0x2ad703afee0c00cd64f8a309df7caf56db476f50a3a63eb7b645f16afe347670", 68 | "0x499881041292f414309a062c4a0e50a32886a642ca5b18ce2f33892c01c59b6f", 69 | "0x169e05cb1c52f31bce923a62c3a54878e5009664ebdbda7d2c9c2eb8f9ef6c60", 70 | "0xeea4623936f85480e06e6fef37b4b8f6609df6060379c06826b53998c65db8a5", 71 | "0xd190fb5d73d6637e61219b9fec59d262e78ea2425e1b33ac90913d53f4198f57", 72 | "0x9887e54d2d5080cb822d36ba321f2f8e94ab86ec41202b32364330a66f3771f3", 73 | "0x27e029823dc6c29f811586f1c7494da3f98b21b66149b84aef9cecbf1b3b7d84", 74 | "0x1d13d33d4fb6fece8905aa1ca88b12b13d6657c0088e4aef925d9b841c8bd04e", 75 | "0x917b800fe9c34bb01f64466d72d16b17a1a8ee259fe75388728732fbd85efc56", 76 | "0xbdc439d615a794cd56f1e8db57443ecfb7a3aab3007b577c1384c326796e416e", 77 | "0x56f007b4a793aa597f2931918041d15e49c0f0df13d70e943abb1dd55443bdde", 78 | "0x8cbf6dd9f811bd995360e07cd473b03557b26edaa08c74a2b214b51a62a86add", 79 | "0x8d995b221fde9d68f694d67387fa5f7ba96cbf214859afdee35ee1259f40258a", 80 | "0xbfda7549339ec65a6194c590adae05afa8b151ac09e45d47089909baa2e85d0f", 81 | "0x327e90aacbb89dc399c16dda9bf9c678c34706a92a12f916937d51f06f7b77eb", 82 | "0xf209378bcbc64c73e8b7f79b458862fa5352ef98e3be6f35b939da11585f88bd", 83 | "0x48855ae3f55f7541100460044bdffff68d385c037e5d95467ecc7e9bf94717b4", 84 | "0xa8b35150d825158df3582e93b59697267da48e9c346fa0201b73740d494066c1", 85 | "0x39d071a2992e1f9cae76f063f6ac43a8c391f48e6dee0a747cb96fc484c8b1ad", 86 | "0x44f423e403ae230485e9252adad3a7919b15929264e74b153cc71d82e4aa4092", 87 | "0x0baa66376068c94bb0584b6ffd546b920fe3800bdf0738983b4664936bb77ab5", 88 | "0x16accf976a71e9ec5a529d4664adc78a3ddf54b8e5c9515c9b5cae0c510f84c5", 89 | "0x1edc90ec503856ab0ad0d0f94b23f32ed4fd4e0f40b135f742954e045d556cc9", 90 | "0x3349c201895a20ee27559947736cefaff2c8ea4e4f4596af993a32f96d574c7c", 91 | "0xe43760622c3706049c2e5f83286dfba2560f7f435f9a84c4a02580ab74ddcd3b", 92 | "0xcb5b181c33eb5799d13404b3aca7636f4b1754b1edfad6e032031ccef08c0a9b", 93 | "0xb68f242eb49ecb58129a46ce18f118a1da76f75753bf0b3c955dea35ca453b76", 94 | "0x88b6798175f3fdc2f6fb9a14c5d0223ce7f20f63bee3c37bcc3cdb19ef15314c", 95 | "0x8d1859fa479851868ce2c9e364402d393f80f7092aaa56b81669b110052d89c4", 96 | "0xaed286129bfe7b12eab109f95241eb00d869e951481077ff776169f2bdba5826", 97 | "0xd7a0a2519649a7ebd7e343291b245411e1d9280755ca844f80499b02ada3cc8e", 98 | "0xb72018ac17ae122c7af6d27e8bd10646980fa28dc31677de31ad4da676d93590", 99 | "0x6c8bdac67bcefc51948fa2312d0b96d63b50d64a1c8fc86c10524b5b0d0065bc", 100 | "0xe4dac2bcaed15966dade7969b3c846218333ea30aac8b40bb1f9aefaf450bf7f", 101 | "0xa58a4dabe4e61062381f3c5cfbdf9475da4d7753b95c2ceda3146a839317ef22", 102 | ]; 103 | 104 | pub const SK: &str = "0xcdfbe6f7602f67a97602e3e9fc24cde1cdffa88acd47745c0b84c5ff55891e1b"; 105 | 106 | pub const ADDR: &str = "0xb02A2EdA1b317FBd16760128836B0Ac59B560e9D"; 107 | 108 | pub const SK2: &str = "0x8c04e41e317a7cf0cf4c2f7431d0a890a950f352df41ff6d053698df61a73bba"; 109 | 110 | pub const MAX_GAS_LIMIT: u64 = 30000000; 111 | 112 | pub const MAX_INPUT_LENGTH: usize = 1024; 113 | 114 | pub const MAX_ACCESS_LIST_LENGTH: usize = 1024; 115 | 116 | pub const MAX_ACCESSED_KEYS_LENGTH: usize = 1024; 117 | 118 | pub const MAX_TRANSACTION_TYPE: u8 = 5; // one more for invalid testing 119 | 120 | pub const MAX_BLOB_VERSIONED_HASHES_LENGTH: usize = 1024; 121 | 122 | pub const MAX_BLOB_SIDECAR_LENGTH: usize = 1024; 123 | 124 | pub const MAX_AUTHORIZATION_LIST_LENGTH: usize = 1024; 125 | 126 | pub const MAX_TRANSACTION_LENGTH: usize = 1024 * 100; // revisit 127 | -------------------------------------------------------------------------------- /crates/runners/src/random.rs: -------------------------------------------------------------------------------- 1 | use crate::{builder::Builder, cache::BuilderCache, logger::Logger}; 2 | use alloy::{ 3 | consensus::{ 4 | TxEip1559, TxEip2930, TxEip4844, TxEip4844WithSidecar, TxEip7702, TxLegacy, 5 | transaction::RlpEcdsaEncodableTx, 6 | }, 7 | primitives::{Address, TxHash}, 8 | providers::{Provider, ProviderBuilder}, 9 | rpc::types::TransactionRequest, 10 | signers::{k256::ecdsa::SigningKey, local::PrivateKeySigner}, 11 | transports::http::reqwest::Url, 12 | }; 13 | use alloy_rlp::Encodable; 14 | use common::{constants::MAX_TRANSACTION_LENGTH, types::Backend}; 15 | use mutator::Mutator; 16 | use rand::{Rng, SeedableRng, random_bool, rngs::StdRng}; 17 | pub struct RandomTransactionRunner { 18 | pub sk: SigningKey, 19 | pub seed: u64, 20 | pub provider: Backend, 21 | pub current_tx: Vec, 22 | pub mutator: Mutator, 23 | pub running: bool, 24 | pub crash_counter: u64, 25 | pub logger: Logger, 26 | pub cache: BuilderCache, 27 | pub sender: Address, 28 | } 29 | 30 | impl Builder for RandomTransactionRunner { 31 | fn provider(&self) -> &Backend { 32 | &self.provider 33 | } 34 | 35 | fn cache(&self) -> &BuilderCache { 36 | &self.cache 37 | } 38 | 39 | fn cache_mut(&mut self) -> &mut BuilderCache { 40 | &mut self.cache 41 | } 42 | } 43 | 44 | impl RandomTransactionRunner { 45 | pub async fn new( 46 | rpc_url: Url, 47 | sk: SigningKey, 48 | seed: u64, 49 | max_operations_per_mutation: u64, 50 | ) -> Self { 51 | let provider = ProviderBuilder::new() 52 | .wallet::(sk.clone().into()) 53 | .connect_http(rpc_url); 54 | 55 | let sender = Address::from_private_key(&sk); 56 | let account = provider.get_account(sender).await.unwrap_or_default(); 57 | let cache = BuilderCache { 58 | gas_price: provider.get_gas_price().await.unwrap_or_default(), 59 | max_priority_fee: provider.get_max_priority_fee_per_gas().await.unwrap_or_default(), 60 | max_fee_per_blob_gas: provider.get_blob_base_fee().await.unwrap_or_default(), 61 | balance: account.balance, 62 | nonce: account.nonce, 63 | chain_id: provider.get_chain_id().await.unwrap_or_default(), 64 | }; 65 | 66 | let mutator = Mutator::new(max_operations_per_mutation, seed); 67 | let logger = Logger::new("random").unwrap(); 68 | 69 | Self { 70 | sk, 71 | seed, 72 | current_tx: vec![], 73 | provider, 74 | mutator, 75 | running: false, 76 | crash_counter: 0, 77 | logger, 78 | cache, 79 | sender, 80 | } 81 | } 82 | 83 | pub async fn run(&mut self) { 84 | let mut random = StdRng::seed_from_u64(self.seed); 85 | self.running = true; 86 | 87 | loop { 88 | // 10% chance to re-generate the transaction 89 | if random_bool(0.1) || self.current_tx.is_empty() { 90 | // 50% chance to update the cache 91 | // This is to try to get further in the execution by bypassing 92 | // common checks like gas > expected and so on 93 | if random_bool(0.5) { 94 | self.cache.update(&self.provider, self.sender).await; 95 | } 96 | 97 | let (request, tx) = self.create_random_transaction(&mut random).await; 98 | tx.encode(&mut self.current_tx); 99 | 100 | if let Err(err) = self.provider.send_transaction_unsafe(request).await { 101 | if self.logger.is_connection_refused_error(&err) { 102 | let current_tx = self.current_tx.clone(); 103 | let _ = self.logger.generate_crash_report(¤t_tx); 104 | 105 | self.crash_counter += 1; 106 | self.running = false; 107 | 108 | break; 109 | } 110 | } 111 | } else { 112 | self.mutator.mutate(&mut self.current_tx); 113 | if let Err(err) = self 114 | .provider 115 | .client() 116 | .request::<_, TxHash>("eth_sendRawTransaction", &self.current_tx) 117 | .await 118 | { 119 | if self.logger.is_connection_refused_error(&err) { 120 | let current_tx = self.current_tx.clone(); 121 | let _ = self.logger.generate_crash_report(¤t_tx); 122 | 123 | self.crash_counter += 1; 124 | self.running = false; 125 | 126 | break; 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | pub async fn create_random_transaction( 134 | &self, 135 | random: &mut StdRng, 136 | ) -> (TransactionRequest, Vec) { 137 | let to = self.to(random); 138 | let gas_price = self.gas_price(random).await; 139 | let max_fee_per_gas = self.max_fee_per_gas(random); 140 | let max_priority_fee_per_gas = self.max_priority_fee_per_gas(random).await; 141 | let max_fee_per_blob_gas = self.max_fee_per_blob_gas(random).await; 142 | let gas = self.gas(random); 143 | let value = self.value(random).await; 144 | let input = self.input(random); 145 | let nonce = self.nonce(random).await; 146 | let chain_id = self.chain_id(random).await; 147 | let access_list = self.access_list(random); 148 | let transaction_type = self.transaction_type(random); 149 | let blob_versioned_hashes = self.blob_versioned_hashes(random); 150 | let sidecar = self.sidecar(random); 151 | let authorization_list = self.authorization_list(random); 152 | 153 | let request = TransactionRequest { 154 | from: Some(self.sender), 155 | to: Some(to), 156 | gas_price: Some(gas_price), 157 | max_fee_per_gas: Some(max_fee_per_gas), 158 | max_priority_fee_per_gas: Some(max_priority_fee_per_gas), 159 | max_fee_per_blob_gas: Some(max_fee_per_blob_gas), 160 | gas: Some(gas), 161 | value: Some(value), 162 | input: input.clone(), 163 | nonce: Some(nonce), 164 | chain_id: Some(chain_id), 165 | access_list: Some(access_list.clone()), 166 | transaction_type: Some(transaction_type), 167 | blob_versioned_hashes: Some(blob_versioned_hashes.clone()), 168 | sidecar: Some(sidecar.clone()), 169 | authorization_list: Some(authorization_list.clone()), 170 | }; 171 | 172 | let mut encoded = vec![]; 173 | match transaction_type { 174 | 0 => { 175 | let tx = TxLegacy { 176 | to, 177 | value, 178 | chain_id: Some(chain_id), 179 | nonce, 180 | gas_price, 181 | gas_limit: gas, 182 | input: input.into_input().unwrap(), 183 | }; 184 | 185 | tx.encode(&mut encoded); 186 | } 187 | 1 => { 188 | let tx = TxEip2930 { 189 | to, 190 | gas_price, 191 | gas_limit: gas, 192 | value, 193 | chain_id, 194 | nonce, 195 | access_list, 196 | input: input.into_input().unwrap(), 197 | }; 198 | 199 | tx.encode(&mut encoded); 200 | } 201 | 2 => { 202 | let tx = TxEip1559 { 203 | to, 204 | gas_limit: gas, 205 | value, 206 | chain_id, 207 | nonce, 208 | access_list, 209 | input: input.into_input().unwrap(), 210 | max_fee_per_gas, 211 | max_priority_fee_per_gas, 212 | }; 213 | 214 | tx.encode(&mut encoded); 215 | } 216 | 3 => { 217 | let tx = TxEip4844 { 218 | to: to.into_to().unwrap_or_else(|| Address::ZERO), 219 | chain_id, 220 | nonce, 221 | max_fee_per_gas, 222 | max_priority_fee_per_gas, 223 | value, 224 | access_list, 225 | blob_versioned_hashes, 226 | max_fee_per_blob_gas, 227 | input: input.into_input().unwrap(), 228 | gas_limit: gas, 229 | }; 230 | 231 | let tx_with_sidecar = TxEip4844WithSidecar { tx, sidecar }; 232 | 233 | tx_with_sidecar.rlp_encode(&mut encoded); 234 | } 235 | 4 => { 236 | let tx = TxEip7702 { 237 | to: to.into_to().unwrap_or_else(|| Address::ZERO), 238 | gas_limit: gas, 239 | value, 240 | chain_id, 241 | nonce, 242 | max_fee_per_gas, 243 | max_priority_fee_per_gas, 244 | access_list, 245 | authorization_list, 246 | input: input.into_input().unwrap(), 247 | }; 248 | 249 | tx.encode(&mut encoded); 250 | } 251 | _ => { 252 | // Fill with random bytes for any other transaction type 253 | let length = random.random_range(0..=MAX_TRANSACTION_LENGTH); 254 | let random_bytes = (0..length) 255 | .map(|_| random.random_range(0..=u8::MAX) as u8) 256 | .collect::>(); 257 | 258 | encoded.extend_from_slice(&random_bytes); 259 | } 260 | }; 261 | 262 | (request, encoded) 263 | } 264 | } 265 | 266 | #[tokio::test] 267 | async fn test_random_transaction_runner() { 268 | let mut rng = StdRng::seed_from_u64(1); 269 | let runner = RandomTransactionRunner::new( 270 | "http://localhost:8545".parse::().unwrap(), 271 | SigningKey::from_slice( 272 | &alloy::hex::decode( 273 | "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", 274 | ) 275 | .unwrap(), 276 | ) 277 | .unwrap(), 278 | 1, 279 | 1000, 280 | ) 281 | .await; 282 | let tx = runner.create_random_transaction(&mut rng).await; 283 | println!("tx: {:#?}", &tx); 284 | } 285 | -------------------------------------------------------------------------------- /crates/runners/src/builder.rs: -------------------------------------------------------------------------------- 1 | use alloy::{ 2 | consensus::BlobTransactionSidecar, 3 | eips::{eip4844::BYTES_PER_BLOB, eip7702::SignedAuthorization}, 4 | primitives::{Address, Bytes, FixedBytes, TxKind, U256}, 5 | rpc::types::{AccessList, AccessListItem, Authorization, TransactionInput}, 6 | }; 7 | use common::{ 8 | constants::{ 9 | MAX_ACCESS_LIST_LENGTH, MAX_ACCESSED_KEYS_LENGTH, MAX_AUTHORIZATION_LIST_LENGTH, 10 | MAX_BLOB_SIDECAR_LENGTH, MAX_BLOB_VERSIONED_HASHES_LENGTH, MAX_GAS_LIMIT, MAX_INPUT_LENGTH, 11 | MAX_TRANSACTION_TYPE, 12 | }, 13 | types::Backend, 14 | }; 15 | use rand::{Rng, RngCore, random_bool, rngs::StdRng}; 16 | 17 | use crate::cache::BuilderCache; 18 | 19 | pub trait Builder { 20 | fn provider(&self) -> &Backend; 21 | fn cache(&self) -> &BuilderCache; 22 | fn cache_mut(&mut self) -> &mut BuilderCache; 23 | 24 | // ------------------------------------------------------------ 25 | 26 | fn to(&self, random: &mut StdRng) -> TxKind { 27 | if random_bool(0.5) { 28 | TxKind::Create 29 | } else { 30 | TxKind::Call({ 31 | let mut addr = [0u8; 20]; 32 | random.fill(&mut addr); 33 | Address::from(addr) 34 | }) 35 | } 36 | } 37 | 38 | // ------------------------------------------------------------ 39 | 40 | #[allow(async_fn_in_trait)] 41 | async fn gas_price(&self, random: &mut StdRng) -> u128 { 42 | if random_bool(0.85) { self.cache().gas_price } else { random.random::() } 43 | } 44 | 45 | // ------------------------------------------------------------ 46 | 47 | // [nethoxa] this should be better implemented 48 | fn max_fee_per_gas(&self, random: &mut StdRng) -> u128 { 49 | random.random::() 50 | } 51 | 52 | // ------------------------------------------------------------ 53 | 54 | #[allow(async_fn_in_trait)] 55 | async fn max_priority_fee_per_gas(&self, random: &mut StdRng) -> u128 { 56 | if random_bool(0.85) { self.cache().max_priority_fee } else { random.random::() } 57 | } 58 | 59 | // ------------------------------------------------------------ 60 | 61 | #[allow(async_fn_in_trait)] 62 | async fn max_fee_per_blob_gas(&self, random: &mut StdRng) -> u128 { 63 | if random_bool(0.85) { self.cache().max_fee_per_blob_gas } else { random.random::() } 64 | } 65 | 66 | // ------------------------------------------------------------ 67 | 68 | // [nethoxa] should implement a call to gas estimation 69 | fn gas(&self, random: &mut StdRng) -> u64 { 70 | random.random_range(0..=MAX_GAS_LIMIT * 2) 71 | } 72 | 73 | // ------------------------------------------------------------ 74 | 75 | #[allow(async_fn_in_trait)] 76 | async fn value(&self, random: &mut StdRng) -> U256 { 77 | if random_bool(0.85) { 78 | self.cache().balance / U256::from(100_000_000) 79 | } else { 80 | self.random_u256(random) 81 | } 82 | } 83 | 84 | fn random_u256(&self, random: &mut StdRng) -> U256 { 85 | let mut bytes = [0u8; 32]; 86 | random.fill(&mut bytes); 87 | U256::from_be_slice(&bytes) 88 | } 89 | 90 | // ------------------------------------------------------------ 91 | 92 | fn input(&self, random: &mut StdRng) -> TransactionInput { 93 | if random_bool(0.2) { 94 | let length = random.random_range(0..=MAX_INPUT_LENGTH); 95 | TransactionInput::new(self.random_bytes(length, random)) 96 | } else { 97 | TransactionInput::from(vec![]) 98 | } 99 | } 100 | 101 | // ------------------------------------------------------------ 102 | 103 | #[allow(async_fn_in_trait)] 104 | async fn nonce(&self, random: &mut StdRng) -> u64 { 105 | if random_bool(0.90) { self.cache().nonce } else { random.next_u64() } 106 | } 107 | 108 | // ------------------------------------------------------------ 109 | 110 | #[allow(async_fn_in_trait)] 111 | async fn chain_id(&self, random: &mut StdRng) -> u64 { 112 | if random_bool(0.95) { self.cache().chain_id } else { random.next_u64() } 113 | } 114 | 115 | // ------------------------------------------------------------ 116 | 117 | fn access_list(&self, random: &mut StdRng) -> AccessList { 118 | if random_bool(0.2) { self.random_access_list(random) } else { AccessList::from(vec![]) } 119 | } 120 | 121 | fn random_access_list(&self, random: &mut StdRng) -> AccessList { 122 | let length = random.random_range(0..=MAX_ACCESS_LIST_LENGTH); 123 | let mut items = vec![]; 124 | 125 | for _ in 0..length { 126 | let addr = self.random_address(random); 127 | 128 | let keys_length = random.random_range(0..=MAX_ACCESSED_KEYS_LENGTH); 129 | let mut keys: Vec> = vec![]; 130 | 131 | for _ in 0..keys_length { 132 | let bytes = self.random_bytes(32, random); 133 | let mut array: [u8; 32] = [0u8; 32]; 134 | 135 | for i in 0..bytes.len() { 136 | array[i] = bytes[i]; 137 | } 138 | 139 | let key = FixedBytes::new(array); 140 | keys.push(key); 141 | } 142 | 143 | let item = AccessListItem { address: addr, storage_keys: keys }; 144 | items.push(item); 145 | } 146 | 147 | AccessList(items) 148 | } 149 | 150 | // ------------------------------------------------------------ 151 | 152 | fn transaction_type(&self, random: &mut StdRng) -> u8 { 153 | // [nethoxa] should we send tx with wrong transaction type? 154 | random.random_range(0..MAX_TRANSACTION_TYPE) 155 | } 156 | 157 | // ------------------------------------------------------------ 158 | 159 | fn blob_versioned_hashes(&self, random: &mut StdRng) -> Vec> { 160 | if random_bool(0.2) { self.random_blob_versioned_hashes(random) } else { vec![] } 161 | } 162 | 163 | fn random_blob_versioned_hashes(&self, random: &mut StdRng) -> Vec> { 164 | let length = random.random_range(0..=MAX_BLOB_VERSIONED_HASHES_LENGTH); 165 | let mut hashes = vec![]; 166 | 167 | for _ in 0..length { 168 | let bytes = self.random_bytes(32, random); 169 | let mut array: [u8; 32] = [0u8; 32]; 170 | 171 | for i in 0..bytes.len() { 172 | array[i] = bytes[i]; 173 | } 174 | 175 | let hash = FixedBytes::new(array); 176 | hashes.push(hash); 177 | } 178 | 179 | hashes 180 | } 181 | 182 | // ------------------------------------------------------------ 183 | 184 | fn sidecar(&self, random: &mut StdRng) -> BlobTransactionSidecar { 185 | if random_bool(0.2) { 186 | self.random_sidecar(random) 187 | } else { 188 | BlobTransactionSidecar::new(vec![], vec![], vec![]) 189 | } 190 | } 191 | 192 | fn random_sidecar(&self, random: &mut StdRng) -> BlobTransactionSidecar { 193 | let same_length = random_bool(0.75); 194 | if same_length { 195 | let length = random.random_range(0..MAX_BLOB_SIDECAR_LENGTH); 196 | let mut blobs = vec![]; 197 | let mut commitments = vec![]; 198 | let mut proofs = vec![]; 199 | 200 | for _ in 0..length { 201 | let bytes = self.random_bytes(BYTES_PER_BLOB, random); 202 | let mut array: [u8; BYTES_PER_BLOB] = [0u8; BYTES_PER_BLOB]; 203 | 204 | for i in 0..bytes.len() { 205 | array[i] = bytes[i]; 206 | } 207 | 208 | let blob = FixedBytes::new(array); 209 | blobs.push(blob); 210 | 211 | let bytes = self.random_bytes(48, random); 212 | let mut array: [u8; 48] = [0u8; 48]; 213 | 214 | for i in 0..bytes.len() { 215 | array[i] = bytes[i]; 216 | } 217 | 218 | let commitment = FixedBytes::new(array); 219 | commitments.push(commitment); 220 | 221 | let bytes = self.random_bytes(48, random); 222 | let mut array: [u8; 48] = [0u8; 48]; 223 | 224 | for i in 0..bytes.len() { 225 | array[i] = bytes[i]; 226 | } 227 | 228 | let proof = FixedBytes::new(array); 229 | proofs.push(proof); 230 | } 231 | 232 | BlobTransactionSidecar { blobs, commitments, proofs } 233 | } else { 234 | let blobs_length = random.random_range(0..MAX_BLOB_SIDECAR_LENGTH); 235 | let commitments_length = random.random_range(0..MAX_BLOB_SIDECAR_LENGTH); 236 | let proofs_length = random.random_range(0..MAX_BLOB_SIDECAR_LENGTH); 237 | 238 | let mut blobs = vec![]; 239 | for _ in 0..blobs_length { 240 | let bytes = self.random_bytes(BYTES_PER_BLOB, random); 241 | let mut array: [u8; BYTES_PER_BLOB] = [0u8; BYTES_PER_BLOB]; 242 | 243 | for i in 0..bytes.len() { 244 | array[i] = bytes[i]; 245 | } 246 | 247 | let blob = FixedBytes::new(array); 248 | blobs.push(blob); 249 | } 250 | 251 | let mut commitments = vec![]; 252 | for _ in 0..commitments_length { 253 | let bytes = self.random_bytes(48, random); 254 | let mut array: [u8; 48] = [0u8; 48]; 255 | 256 | for i in 0..bytes.len() { 257 | array[i] = bytes[i]; 258 | } 259 | 260 | let commitment = FixedBytes::new(array); 261 | commitments.push(commitment); 262 | } 263 | 264 | let mut proofs = vec![]; 265 | for _ in 0..proofs_length { 266 | let bytes = self.random_bytes(48, random); 267 | let mut array: [u8; 48] = [0u8; 48]; 268 | 269 | for i in 0..bytes.len() { 270 | array[i] = bytes[i]; 271 | } 272 | 273 | let proof = FixedBytes::new(array); 274 | proofs.push(proof); 275 | } 276 | 277 | BlobTransactionSidecar { blobs, commitments, proofs } 278 | } 279 | } 280 | 281 | // ------------------------------------------------------------ 282 | 283 | fn authorization_list(&self, random: &mut StdRng) -> Vec { 284 | if random_bool(0.2) { self.random_authorization_list(random) } else { vec![] } 285 | } 286 | 287 | fn random_authorization_list(&self, random: &mut StdRng) -> Vec { 288 | let length = random.random_range(0..=MAX_AUTHORIZATION_LIST_LENGTH); 289 | let mut authorizations = vec![]; 290 | 291 | for _ in 0..length { 292 | let chain_id = self.random_u256(random); 293 | let addr = self.random_address(random); 294 | let nonce = random.next_u64(); 295 | 296 | let auth = Authorization { chain_id, address: addr, nonce }; 297 | 298 | let y_parity = random.random::(); 299 | let r = self.random_u256(random); 300 | let s = self.random_u256(random); 301 | 302 | let signed = SignedAuthorization::new_unchecked(auth, y_parity, r, s); 303 | 304 | authorizations.push(signed); 305 | } 306 | 307 | authorizations 308 | } 309 | 310 | // ------------------------------------------------------------ 311 | 312 | fn random_bytes(&self, length: usize, random: &mut StdRng) -> Bytes { 313 | let mut bytes = vec![0u8; length]; 314 | random.fill(&mut bytes[..]); 315 | bytes.into() 316 | } 317 | 318 | fn random_address(&self, random: &mut StdRng) -> Address { 319 | let mut addr = [0u8; 20]; 320 | random.fill(&mut addr); 321 | Address::from(addr) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /crates/app/src/lib.rs: -------------------------------------------------------------------------------- 1 | use alloy::{ 2 | hex, primitives::Address, signers::k256::ecdsa::SigningKey, transports::http::reqwest::Url, 3 | }; 4 | use crossterm::{ 5 | event::{self, Event, KeyCode}, 6 | execute, 7 | terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, 8 | }; 9 | use errors::AppStatus; 10 | use ratatui::{ 11 | Frame, Terminal, 12 | backend::CrosstermBackend, 13 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 14 | style::{Color, Style}, 15 | text::{Line, Span, Text}, 16 | widgets::{ 17 | Block, Borders, List, ListItem, ListState, Paragraph, Scrollbar, ScrollbarState, Wrap, 18 | }, 19 | }; 20 | use runners::{Runner, Runner::*}; 21 | use std::{collections::HashMap, io, time::Duration}; 22 | use tokio::task::JoinHandle; 23 | pub mod errors; 24 | pub mod handler; 25 | pub mod manager; 26 | 27 | pub struct App { 28 | // Whether the app is running or not. It is displayed in the 29 | // UI to know if there are any `Runners` running. 30 | running: bool, 31 | 32 | // The global seed all runners will use. If per-runner seeds 33 | // are used, this won't be used and will dissapear from the UI. 34 | seed: u64, 35 | 36 | // The private key of the account that will be sending the 37 | // transactions. The same as with `seed`, if per-runner sk 38 | // are used, this won't be used and will dissapear from the UI. 39 | sk: SigningKey, 40 | 41 | // The RPC URL. The same as with `seed` and `sk`, if per-runner 42 | // RPC URLs are used, this won't be used and will dissapear from 43 | // the UI. 44 | rpc_url: Url, 45 | 46 | // The maximum number of operations per mutation. 47 | max_operations_per_mutation: u64, 48 | 49 | // The output buffer. This is used to store the output of the 50 | // command that is being executed. 51 | output: String, 52 | 53 | // The history of commands that have been executed. 54 | command_history: Vec, 55 | 56 | // The history of outputs that have been produced. 57 | output_history: Vec, 58 | 59 | // The history of errors that have been produced. This is used 60 | // to determine the symbol and color to display in the output 61 | // window. 62 | error_history: Vec, 63 | 64 | // The scrollbar widget state. 65 | history_scroll_state: ScrollbarState, 66 | 67 | // The state of the history list. 68 | history_list_state: ListState, 69 | 70 | // The scroll offset of the history list. This is used to keep 71 | // the bottom of the output window always where the last command 72 | // was executed. 73 | scroll_offset: usize, 74 | 75 | // The width of the left panel. This is used to widden the output 76 | // window to show larger information in a more readable way. 77 | left_panel_width: u16, 78 | 79 | // The height of the input area. This is used as a constant to 80 | // keep the input area at a fixed height. 81 | input_height: u16, 82 | 83 | // The handler for each runner. This is used to abort the 84 | // runner when the user wants to stop the fuzzing process of 85 | // either a specific runner or all runners. 86 | handler: HashMap>, 87 | 88 | // The active runners. This is used to know which runners are 89 | // currently running and update the information in the UI 90 | // accordingly. 91 | active_runners: HashMap, 92 | 93 | // The seeds for each runner. This is to have more granular control 94 | // over the runners. 95 | runner_seeds: HashMap, 96 | 97 | // The private keys for each runner. The same as with `runner_seeds`. 98 | runner_sks: HashMap, 99 | 100 | // The RPC URLs for each runner. The same as with `runner_seeds`. 101 | runner_rpcs: HashMap, 102 | } 103 | 104 | impl App { 105 | /// Creates a new `App` instance. 106 | /// 107 | /// # Arguments 108 | /// 109 | /// * `rpc_url` - The URL of the RPC endpoint. 110 | /// * `sk` - The private key of the account that will be sending the transactions. 111 | /// * `seed` - The seed to use for the runners. 112 | pub fn new(rpc_url: Url, sk: SigningKey, seed: u64, max_operations_per_mutation: u64) -> Self { 113 | let mut list_state = ListState::default(); 114 | list_state.select(Some(0)); 115 | 116 | App { 117 | running: false, 118 | seed, 119 | sk: sk.clone(), 120 | rpc_url: rpc_url.clone(), 121 | max_operations_per_mutation, 122 | output: String::new(), 123 | command_history: Vec::new(), 124 | output_history: Vec::new(), 125 | error_history: Vec::new(), 126 | history_scroll_state: ScrollbarState::default(), 127 | history_list_state: list_state, 128 | scroll_offset: 0, 129 | left_panel_width: 70, 130 | input_height: 3, 131 | handler: HashMap::new(), 132 | active_runners: HashMap::new(), 133 | runner_seeds: HashMap::new(), 134 | runner_sks: HashMap::new(), 135 | runner_rpcs: HashMap::new(), 136 | } 137 | } 138 | 139 | /// This is the entry point for the app. It will start the UI and 140 | /// handle the input from the user. 141 | pub async fn run(&mut self) -> Result<(), io::Error> { 142 | // This is done to make it possible for the TUI to disable by-default 143 | // behaviors of the terminal. That way, we can build ours from the 144 | // ground up. 145 | enable_raw_mode()?; 146 | let mut stdout = io::stdout(); 147 | execute!(stdout, EnterAlternateScreen)?; 148 | let backend = CrosstermBackend::new(stdout); 149 | let mut terminal = Terminal::new(backend)?; 150 | 151 | let mut input = String::new(); 152 | loop { 153 | // Update the status of all runners 154 | self.update_runners_status(); 155 | 156 | terminal.draw(|f| self.ui(f, &input))?; 157 | 158 | // We check if there is an event in the queue. If there is, 159 | // we read it and handle it. 160 | if event::poll(Duration::from_millis(100))? { 161 | // Pressed key event 162 | if let Event::Key(key) = event::read()? { 163 | match key.code { 164 | KeyCode::Char(c) => { 165 | input.push(c); 166 | } 167 | 168 | // Backspace key event, we remove the last character from the input. 169 | KeyCode::Backspace => { 170 | input.pop(); 171 | } 172 | 173 | // Enter key event, we handle all the stuff the user has typed in. 174 | KeyCode::Enter => { 175 | let command = input.clone(); 176 | input.clear(); 177 | 178 | // Add command to history 179 | if !command.trim().is_empty() { 180 | self.command_history.push(command.clone()); 181 | } 182 | 183 | // Handle the command 184 | let result = self.handle_command(command).await; 185 | 186 | // Add output to history 187 | if !self.output.is_empty() { 188 | self.output_history.push(self.output.clone()); 189 | } 190 | 191 | // Update list state to scroll to the bottom 192 | let total_items = self.command_history.len() 193 | + self.output_history.iter().filter(|o| !o.is_empty()).count(); 194 | if total_items > 0 { 195 | self.history_list_state.select(Some(total_items - 1)); 196 | // Ensure we're scrolled to the bottom 197 | self.scroll_offset = total_items.saturating_sub(1); 198 | } 199 | 200 | if result.is_err() { 201 | // This is a hack to exit the app if the command is `exit`. 202 | if result.unwrap_err() == AppStatus::Exit { 203 | break; 204 | } 205 | 206 | // Add the error to the history, so that it is displayed as [-] 207 | self.error_history.push(true); 208 | } else { 209 | // Add the success to the history, so that it is displayed as [+] 210 | self.error_history.push(false); 211 | } 212 | } 213 | 214 | // Escape key event, we exit the app. 215 | KeyCode::Esc => { 216 | break; 217 | } 218 | 219 | // Up key event, we scroll up the history by one line. 220 | KeyCode::Up => { 221 | // Scroll history up 222 | if self.scroll_offset > 0 { 223 | self.scroll_offset -= 1; 224 | if let Some(selected) = self.history_list_state.selected() { 225 | if selected > 0 { 226 | self.history_list_state.select(Some(selected - 1)); 227 | } 228 | } 229 | } 230 | } 231 | 232 | // Down key event, we scroll down the history by one line. 233 | KeyCode::Down => { 234 | // Scroll history down 235 | let total_items = self.command_history.len() 236 | + self.output_history.iter().filter(|o| !o.is_empty()).count(); 237 | if self.scroll_offset < total_items.saturating_sub(1) { 238 | self.scroll_offset += 1; 239 | if let Some(selected) = self.history_list_state.selected() { 240 | if selected < total_items.saturating_sub(1) { 241 | self.history_list_state.select(Some(selected + 1)); 242 | } 243 | } 244 | } 245 | } 246 | 247 | // Left key event, we decrease the left panel width by 5. 248 | KeyCode::Left => { 249 | // Decrease left panel width (minimum 20%) 250 | if self.left_panel_width > 20 { 251 | self.left_panel_width -= 5; 252 | } 253 | } 254 | 255 | // Right key event, we increase the left panel width by 5. 256 | KeyCode::Right => { 257 | // Increase left panel width (maximum 70%) 258 | if self.left_panel_width < 70 { 259 | self.left_panel_width += 5; 260 | } 261 | } 262 | 263 | // Page up key event, or scroll up from the touchpad, we scroll up the 264 | // history by 5 lines. [nethoxa] check this 265 | KeyCode::PageUp => { 266 | // Scroll history up by multiple lines 267 | if self.scroll_offset > 0 { 268 | self.scroll_offset = self.scroll_offset.saturating_sub(5); 269 | self.history_list_state.select(Some(self.scroll_offset)); 270 | } 271 | } 272 | 273 | // Page down key event, or scroll down from the touchpad, we scroll down the 274 | // history by 5 lines. 275 | KeyCode::PageDown => { 276 | // Scroll history down by multiple lines 277 | let total_items = self.command_history.len() 278 | + self.output_history.iter().filter(|o| !o.is_empty()).count(); 279 | if self.scroll_offset < total_items.saturating_sub(1) { 280 | self.scroll_offset = 281 | (self.scroll_offset + 5).min(total_items.saturating_sub(1)); 282 | self.history_list_state.select(Some(self.scroll_offset)); 283 | } 284 | } 285 | _ => {} 286 | } 287 | } 288 | } 289 | } 290 | 291 | disable_raw_mode()?; 292 | execute!(terminal.backend_mut(), LeaveAlternateScreen)?; 293 | terminal.show_cursor()?; 294 | 295 | Ok(()) 296 | } 297 | 298 | /// Updates the status of all runners by checking if they are running 299 | fn update_runners_status(&mut self) { 300 | // Check if any runner is still running by checking their task handles 301 | for (runner_type, handle) in &self.handler { 302 | if !handle.is_finished() { 303 | self.active_runners.insert(*runner_type, true); 304 | } else { 305 | self.active_runners.remove(runner_type); 306 | } 307 | } 308 | 309 | // Update the global running status based on whether any runner is active 310 | self.running = !self.active_runners.is_empty(); 311 | } 312 | 313 | /// This is the function that renders the UI. 314 | /// 315 | /// # Arguments 316 | /// 317 | /// * `f` - The frame to render the UI on. 318 | /// * `input` - The input from the user. 319 | fn ui(&mut self, f: &mut Frame, input: &String) { 320 | // First split the screen into left and right panels using the stored width. 321 | let horizontal_chunks = Layout::default() 322 | .direction(Direction::Horizontal) 323 | .constraints( 324 | [ 325 | Constraint::Percentage(self.left_panel_width), 326 | Constraint::Percentage(100 - self.left_panel_width), 327 | ] 328 | .as_ref(), 329 | ) 330 | .split(f.area()); 331 | 332 | // Split the left panel into stats and command input 333 | let left_chunks = Layout::default() 334 | .direction(Direction::Vertical) 335 | .constraints( 336 | [ 337 | Constraint::Min(0), 338 | Constraint::Length(self.input_height), // Dynamic height for input window 339 | ] 340 | .as_ref(), 341 | ) 342 | .split(horizontal_chunks[0]); 343 | 344 | // Stats panel - use the entire available space 345 | let stats_area = left_chunks[0]; 346 | 347 | // Set status color based on status value 348 | let status_color = if self.running { Color::Green } else { Color::Red }; 349 | 350 | // Pretty print status 351 | let status = if self.running { "Running" } else { "Stopped" }; 352 | 353 | // This is the info that will be displayed in the stats panel. 354 | let stats_lines = vec![ 355 | Line::from(vec![ 356 | Span::styled("Status: ", Style::default().fg(Color::Yellow)), 357 | Span::styled(status, Style::default().fg(status_color)), 358 | ]), 359 | Line::from(vec![ 360 | Span::styled("Seed: ", Style::default().fg(Color::Yellow)), 361 | Span::styled(self.seed.to_string(), Style::default().fg(Color::Green)), 362 | ]), 363 | Line::from(vec![ 364 | Span::styled("RPC: ", Style::default().fg(Color::Yellow)), 365 | Span::styled(self.rpc_url.to_string(), Style::default().fg(Color::Green)), 366 | ]), 367 | Line::from(vec![ 368 | Span::styled("Signer: ", Style::default().fg(Color::Yellow)), 369 | Span::styled( 370 | format!("0x{}", hex::encode(self.sk.to_bytes())), 371 | Style::default().fg(Color::Green), 372 | ), 373 | ]), 374 | ]; 375 | 376 | let mut runners = vec![ 377 | AL, Blob, EIP1559, EIP7702, Legacy, Random, 378 | ]; 379 | 380 | // Add active runners information 381 | let mut active_runners = Vec::new(); 382 | for (runner, active) in &self.active_runners { 383 | if *active { 384 | // Here, if there is no per-runner `seed` or `sk`, we use the `global` seed and 385 | // `sk`. 386 | let seed = self.runner_seeds.get(runner).unwrap_or(&self.seed); 387 | let address = 388 | Address::from_private_key(self.runner_sks.get(runner).unwrap_or(&self.sk)); 389 | let rpc = self.runner_rpcs.get(runner).unwrap_or(&self.rpc_url); 390 | 391 | active_runners.push(Line::from(vec![ 392 | Span::styled(format!("{}: ", runner), Style::default().fg(Color::Yellow)), 393 | Span::styled( 394 | format!( 395 | "seed={}, signer={}, rpc={}, ops={}", 396 | seed, address, rpc, self.max_operations_per_mutation 397 | ), 398 | Style::default().fg(Color::Green), 399 | ), 400 | ])); 401 | 402 | runners.remove(runners.iter().position(|r| r == runner).unwrap()); 403 | } 404 | } 405 | 406 | let mut available_runners = String::new(); 407 | for runner in runners { 408 | let line = Line::from(vec![ 409 | Span::styled(format!("{}", runner), Style::default().fg(Color::Yellow)), 410 | ]); 411 | if !active_runners.contains(&line) { 412 | available_runners.push_str(&format!("{} ", runner)); 413 | } 414 | } 415 | 416 | // Build all the lines to be displayed in the stats panel. 417 | let mut all_lines = stats_lines; 418 | all_lines.push(Line::from("")); 419 | all_lines.push(Line::from(vec![ 420 | Span::styled("Active Runners:", Style::default().fg(Color::Yellow)), 421 | ])); 422 | all_lines.extend(active_runners); 423 | all_lines.push(Line::from("")); 424 | all_lines.push(Line::from(vec![ 425 | Span::styled("Available Runners:", Style::default().fg(Color::Yellow)), 426 | ])); 427 | all_lines.push(Line::from(vec![ 428 | Span::styled(available_runners, Style::default().fg(Color::DarkGray)), 429 | ])); 430 | 431 | let stats_text = Text::from(all_lines); 432 | 433 | // Calculate the height of the stats text to center it vertically 434 | let stats_height = stats_text.height() as u16; 435 | let vertical_padding = (stats_area.height.saturating_sub(2 + stats_height)) / 2; // -2 for borders 436 | 437 | // Create a block with empty lines before the content to center it vertically 438 | let mut centered_lines = Vec::new(); 439 | for _ in 0..vertical_padding { 440 | centered_lines.push(Line::from("")); 441 | } 442 | centered_lines.extend(stats_text.lines.clone()); 443 | 444 | // Create a paragraph with the centered lines. 445 | let stats_paragraph = Paragraph::new(Text::from(centered_lines)) 446 | .block(Block::default().borders(Borders::ALL).title("Fuzzer Stats")) 447 | .alignment(Alignment::Center) // Center horizontally 448 | .wrap(Wrap { trim: true }); 449 | f.render_widget(stats_paragraph, stats_area); 450 | 451 | // Command panel 452 | let input_text = { 453 | Text::from(vec![ 454 | Line::from(vec![ 455 | Span::styled("> ", Style::default().fg(Color::Yellow)), 456 | Span::raw(input.clone()), 457 | ]), 458 | ]) 459 | }; 460 | 461 | let input_paragraph = Paragraph::new(input_text) 462 | .block(Block::default().borders(Borders::ALL).title("Command Input")); 463 | f.render_widget(&input_paragraph, left_chunks[1]); 464 | 465 | // History panel on the right 466 | let history_block = Block::default().borders(Borders::ALL).title("Command History"); 467 | 468 | let history_area = horizontal_chunks[1]; 469 | f.render_widget(history_block, history_area); 470 | 471 | // Create inner area for the history content 472 | let history_inner_area = Rect { 473 | x: history_area.x + 1, 474 | y: history_area.y + 1, 475 | width: history_area.width - 2, 476 | height: history_area.height - 2, 477 | }; 478 | 479 | if !self.command_history.is_empty() { 480 | // Create list items for history 481 | let mut history_items = Vec::new(); 482 | for (i, cmd) in self.command_history.iter().enumerate() { 483 | // Create a wrapped command line with proper indentation 484 | let available_width = history_inner_area.width.saturating_sub(3); // Subtract prefix width "> " 485 | let mut cmd_lines = Vec::new(); 486 | let mut remaining = cmd.as_str(); 487 | 488 | // First line with the command prefix 489 | let first_line_len = available_width.min(remaining.len() as u16); 490 | let (first_part, rest) = remaining.split_at(first_line_len as usize); 491 | cmd_lines.push(Line::from(vec![ 492 | Span::styled(format!("> {}", first_part), Style::default().fg(Color::Yellow)), 493 | ])); 494 | remaining = rest; 495 | 496 | // Subsequent lines with proper indentation if command is long 497 | while !remaining.is_empty() { 498 | let line_len = available_width.min(remaining.len() as u16); 499 | let (part, rest) = remaining.split_at(line_len as usize); 500 | cmd_lines.push(Line::from(vec![ 501 | Span::styled(format!(" {}", part), Style::default().fg(Color::Yellow)), 502 | ])); 503 | remaining = rest; 504 | } 505 | 506 | // Add all command lines to history items 507 | for line in cmd_lines { 508 | history_items.push(ListItem::new(line)); 509 | } 510 | 511 | if i < self.output_history.len() && !self.output_history[i].is_empty() { 512 | let output_text = &self.output_history[i]; 513 | // Use the error_history to determine if this is an error 514 | let is_error = self.error_history[i]; 515 | 516 | // Create styled icon 517 | let icon_span = if is_error { 518 | Span::styled("-", Style::default().fg(Color::Red)) 519 | } else { 520 | Span::styled("+", Style::default().fg(Color::Green)) 521 | }; 522 | 523 | // Wrap output text with proper indentation 524 | let output_color = if is_error { Color::Red } else { Color::White }; 525 | let mut output_remaining = output_text.as_str(); 526 | 527 | // First line of output with icon 528 | let first_output_len = 529 | available_width.saturating_sub(5).min(output_remaining.len() as u16); // [+] prefix 530 | let (first_output, rest_output) = 531 | output_remaining.split_at(first_output_len as usize); 532 | history_items.push(ListItem::new(Line::from(vec![ 533 | Span::raw(" ["), 534 | icon_span.clone(), 535 | Span::raw("] "), 536 | Span::styled(first_output, Style::default().fg(output_color)), 537 | ]))); 538 | output_remaining = rest_output; 539 | 540 | // Subsequent lines of output with proper indentation 541 | while !output_remaining.is_empty() { 542 | let line_len = 543 | available_width.saturating_sub(5).min(output_remaining.len() as u16); 544 | let (part, rest) = output_remaining.split_at(line_len as usize); 545 | history_items.push(ListItem::new(Line::from(vec![ 546 | Span::raw(" "), // Align with text after icon 547 | Span::styled(part, Style::default().fg(output_color)), 548 | ]))); 549 | output_remaining = rest; 550 | } 551 | } 552 | } 553 | 554 | // Create and render the list with auto-scroll 555 | let history_list = 556 | List::new(history_items).highlight_style(Style::default().bg(Color::DarkGray)); 557 | 558 | // Update scrollbar state 559 | let total_items = self.command_history.len() 560 | + self.output_history.iter().filter(|o| !o.is_empty()).count(); 561 | 562 | // Make sure we have a valid selection 563 | if self.history_list_state.selected().is_none() && total_items > 0 { 564 | self.history_list_state.select(Some(self.scroll_offset)); 565 | } 566 | 567 | // Update scrollbar state with current position 568 | self.history_scroll_state = 569 | ScrollbarState::default().content_length(total_items).position(self.scroll_offset); 570 | 571 | f.render_stateful_widget( 572 | history_list, 573 | history_inner_area, 574 | &mut self.history_list_state, 575 | ); 576 | 577 | // Render scrollbar 578 | let scrollbar = Scrollbar::default() 579 | .orientation(ratatui::widgets::ScrollbarOrientation::VerticalRight) 580 | .begin_symbol(Some("↑")) 581 | .end_symbol(Some("↓")); 582 | 583 | f.render_stateful_widget(scrollbar, history_inner_area, &mut self.history_scroll_state); 584 | } 585 | } 586 | 587 | fn print(&mut self, output: &str) { 588 | self.output = output.to_string(); 589 | } 590 | } 591 | -------------------------------------------------------------------------------- /crates/app/src/handler.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::{App, errors::AppStatus}; 4 | use alloy::{hex, signers::k256::ecdsa::SigningKey, transports::http::reqwest::Url}; 5 | use common::{constants::SK, parse_sk}; 6 | use runners::Runner::{self, *}; 7 | 8 | impl App { 9 | // Helper function to check if a runner type is valid 10 | fn is_valid_runner(&self, runner: &str) -> bool { 11 | [ 12 | "al", "blob", "eip1559", "eip7702", "legacy", "random", 13 | ] 14 | .contains(&runner) 15 | } 16 | 17 | // Helper function to check if a scope is valid 18 | fn is_valid_scope(&self, scope: &str) -> bool { 19 | [ 20 | "global", "al", "blob", "eip1559", "eip7702", "legacy", "random", 21 | ] 22 | .contains(&scope) 23 | } 24 | 25 | // Helper function to check if a parameter is valid 26 | fn is_valid_param(&self, param: &str) -> bool { 27 | [ 28 | "rpc", "sk", "seed", "chainid", 29 | ] 30 | .contains(¶m) 31 | } 32 | 33 | // Helper function to handle setting globalconfig values 34 | fn handle_global_set(&mut self, param: &str, value: &str) -> Result<(), AppStatus> { 35 | match param { 36 | "rpc" => { 37 | if let Ok(url) = Url::parse(value) { 38 | if self.rpc_url == url { 39 | self.print(&format!("global rpc url already set to that value")); 40 | return Err(AppStatus::RuntimeError); 41 | } 42 | self.rpc_url = url; 43 | self.print(&format!("global rpc url set to {}", value)); 44 | } else { 45 | self.print(&format!("invalid rpc url: {}", value)); 46 | return Err(AppStatus::RuntimeError); 47 | } 48 | } 49 | "sk" => { 50 | if let Ok(sk) = parse_sk(value) { 51 | if self.sk == sk { 52 | self.print(&format!("global sk already set to that value")); 53 | return Err(AppStatus::RuntimeError); 54 | } 55 | self.sk = sk; 56 | self.print(&format!("global sk set to {}", value)); 57 | } else { 58 | self.print(&format!("invalid sk: {}", value)); 59 | return Err(AppStatus::RuntimeError); 60 | } 61 | } 62 | "seed" => { 63 | if let Ok(seed) = value.parse::() { 64 | if self.seed == seed { 65 | self.print(&format!("global seed already set to that value")); 66 | return Err(AppStatus::RuntimeError); 67 | } 68 | self.seed = seed; 69 | self.print(&format!("global seed set to {}", value)); 70 | } else { 71 | self.print(&format!("invalid seed: {}", value)); 72 | return Err(AppStatus::RuntimeError); 73 | } 74 | } 75 | _ => unreachable!(), 76 | } 77 | 78 | Ok(()) 79 | } 80 | 81 | // Helper function to handle setting per-runner config values 82 | fn handle_runner_set( 83 | &mut self, 84 | runner: &str, 85 | param: &str, 86 | value: &str, 87 | ) -> Result<(), AppStatus> { 88 | match runner { 89 | "al" => match param { 90 | "rpc" => { 91 | if let Ok(url) = Url::parse(value) { 92 | if self.runner_rpcs.get(&AL) == Some(&url) { 93 | self.print(&format!("{} rpc url already set to that value", runner)); 94 | return Err(AppStatus::RuntimeError); 95 | } 96 | self.runner_rpcs.insert(AL, url); 97 | self.print(&format!("{} rpc url set to {}", runner, value)); 98 | } else { 99 | self.print(&format!("invalid rpc url: {}", value)); 100 | return Err(AppStatus::RuntimeError); 101 | } 102 | } 103 | "sk" => { 104 | if let Ok(sk) = parse_sk(value) { 105 | if self.runner_sks.get(&AL) == Some(&sk) { 106 | self.print(&format!("{} sk already set to that value", runner)); 107 | return Err(AppStatus::RuntimeError); 108 | } 109 | self.runner_sks.insert(AL, sk); 110 | self.print(&format!("{} sk set to {}", runner, value)); 111 | } else { 112 | self.print(&format!("invalid sk: {}", value)); 113 | return Err(AppStatus::RuntimeError); 114 | } 115 | } 116 | "seed" => { 117 | if let Ok(seed) = value.parse::() { 118 | if self.runner_seeds.get(&AL) == Some(&seed) { 119 | self.print(&format!("{} seed already set to that value", runner)); 120 | return Err(AppStatus::RuntimeError); 121 | } 122 | self.runner_seeds.insert(AL, seed); 123 | self.print(&format!("{} seed set to {}", runner, value)); 124 | } else { 125 | self.print(&format!("invalid seed: {}", value)); 126 | return Err(AppStatus::RuntimeError); 127 | } 128 | } 129 | _ => unreachable!(), 130 | }, 131 | "blob" => match param { 132 | "rpc" => { 133 | if let Ok(url) = Url::parse(value) { 134 | if self.runner_rpcs.get(&Blob) == Some(&url) { 135 | self.print(&format!("{} rpc url already set to that value", runner)); 136 | return Err(AppStatus::RuntimeError); 137 | } 138 | self.runner_rpcs.insert(Blob, url); 139 | self.print(&format!("{} rpc url set to {}", runner, value)); 140 | } else { 141 | self.print(&format!("invalid rpc url: {}", value)); 142 | return Err(AppStatus::RuntimeError); 143 | } 144 | } 145 | "sk" => { 146 | if let Ok(sk) = parse_sk(value) { 147 | if self.runner_sks.get(&Blob) == Some(&sk) { 148 | self.print(&format!("{} sk already set to that value", runner)); 149 | return Err(AppStatus::RuntimeError); 150 | } 151 | self.runner_sks.insert(Blob, sk); 152 | self.print(&format!("{} sk set to {}", runner, value)); 153 | } else { 154 | self.print(&format!("invalid sk: {}", value)); 155 | return Err(AppStatus::RuntimeError); 156 | } 157 | } 158 | "seed" => { 159 | if let Ok(seed) = value.parse::() { 160 | if self.runner_seeds.get(&Blob) == Some(&seed) { 161 | self.print(&format!("{} seed already set to that value", runner)); 162 | return Err(AppStatus::RuntimeError); 163 | } 164 | self.runner_seeds.insert(Blob, seed); 165 | self.print(&format!("{} seed set to {}", runner, value)); 166 | } else { 167 | self.print(&format!("invalid seed: {}", value)); 168 | return Err(AppStatus::RuntimeError); 169 | } 170 | } 171 | _ => unreachable!(), 172 | }, 173 | "eip1559" => match param { 174 | "rpc" => { 175 | if let Ok(url) = Url::parse(value) { 176 | if self.runner_rpcs.get(&EIP1559) == Some(&url) { 177 | self.print(&format!("{} rpc url already set to that value", runner)); 178 | return Err(AppStatus::RuntimeError); 179 | } 180 | self.runner_rpcs.insert(EIP1559, url); 181 | self.print(&format!("{} rpc url set to {}", runner, value)); 182 | } else { 183 | self.print(&format!("invalid rpc url: {}", value)); 184 | return Err(AppStatus::RuntimeError); 185 | } 186 | } 187 | "sk" => { 188 | if let Ok(sk) = parse_sk(value) { 189 | if self.runner_sks.get(&EIP1559) == Some(&sk) { 190 | self.print(&format!("{} sk already set to that value", runner)); 191 | return Err(AppStatus::RuntimeError); 192 | } 193 | self.runner_sks.insert(EIP1559, sk); 194 | self.print(&format!("{} sk set to {}", runner, value)); 195 | } else { 196 | self.print(&format!("invalid sk: {}", value)); 197 | return Err(AppStatus::RuntimeError); 198 | } 199 | } 200 | "seed" => { 201 | if let Ok(seed) = value.parse::() { 202 | if self.runner_seeds.get(&EIP1559) == Some(&seed) { 203 | self.print(&format!("{} seed already set to that value", runner)); 204 | return Err(AppStatus::RuntimeError); 205 | } 206 | self.runner_seeds.insert(EIP1559, seed); 207 | self.print(&format!("{} seed set to {}", runner, value)); 208 | } else { 209 | self.print(&format!("invalid seed: {}", value)); 210 | return Err(AppStatus::RuntimeError); 211 | } 212 | } 213 | _ => unreachable!(), 214 | }, 215 | "eip7702" => match param { 216 | "rpc" => { 217 | if let Ok(url) = Url::parse(value) { 218 | if self.runner_rpcs.get(&EIP7702) == Some(&url) { 219 | self.print(&format!("{} rpc url already set to that value", runner)); 220 | return Err(AppStatus::RuntimeError); 221 | } 222 | self.runner_rpcs.insert(EIP7702, url); 223 | self.print(&format!("{} rpc url set to {}", runner, value)); 224 | } else { 225 | self.print(&format!("invalid rpc url: {}", value)); 226 | return Err(AppStatus::RuntimeError); 227 | } 228 | } 229 | "sk" => { 230 | if let Ok(sk) = parse_sk(value) { 231 | if self.runner_sks.get(&EIP7702) == Some(&sk) { 232 | self.print(&format!("{} sk already set to that value", runner)); 233 | return Err(AppStatus::RuntimeError); 234 | } 235 | self.runner_sks.insert(EIP7702, sk); 236 | self.print(&format!("{} sk set to {}", runner, value)); 237 | } else { 238 | self.print(&format!("invalid sk: {}", value)); 239 | return Err(AppStatus::RuntimeError); 240 | } 241 | } 242 | "seed" => { 243 | if let Ok(seed) = value.parse::() { 244 | if self.runner_seeds.get(&EIP7702) == Some(&seed) { 245 | self.print(&format!("{} seed already set to that value", runner)); 246 | return Err(AppStatus::RuntimeError); 247 | } 248 | self.runner_seeds.insert(EIP7702, seed); 249 | self.print(&format!("{} seed set to {}", runner, value)); 250 | } else { 251 | self.print(&format!("invalid seed: {}", value)); 252 | return Err(AppStatus::RuntimeError); 253 | } 254 | } 255 | _ => unreachable!(), 256 | }, 257 | "legacy" => match param { 258 | "rpc" => { 259 | if let Ok(url) = Url::parse(value) { 260 | if self.runner_rpcs.get(&Legacy) == Some(&url) { 261 | self.print(&format!("{} rpc url already set to that value", runner)); 262 | return Err(AppStatus::RuntimeError); 263 | } 264 | self.runner_rpcs.insert(Legacy, url); 265 | self.print(&format!("{} rpc url set to {}", runner, value)); 266 | } else { 267 | self.print(&format!("invalid rpc url: {}", value)); 268 | return Err(AppStatus::RuntimeError); 269 | } 270 | } 271 | "sk" => { 272 | if let Ok(sk) = parse_sk(value) { 273 | if self.runner_sks.get(&Legacy) == Some(&sk) { 274 | self.print(&format!("{} sk already set to that value", runner)); 275 | return Err(AppStatus::RuntimeError); 276 | } 277 | self.runner_sks.insert(Legacy, sk); 278 | self.print(&format!("{} sk set to {}", runner, value)); 279 | } else { 280 | self.print(&format!("invalid sk: {}", value)); 281 | return Err(AppStatus::RuntimeError); 282 | } 283 | } 284 | "seed" => { 285 | if let Ok(seed) = value.parse::() { 286 | if self.runner_seeds.get(&Legacy) == Some(&seed) { 287 | self.print(&format!("{} seed already set to that value", runner)); 288 | return Err(AppStatus::RuntimeError); 289 | } 290 | self.runner_seeds.insert(Legacy, seed); 291 | self.print(&format!("{} seed set to {}", runner, value)); 292 | } else { 293 | self.print(&format!("invalid seed: {}", value)); 294 | return Err(AppStatus::RuntimeError); 295 | } 296 | } 297 | _ => unreachable!(), 298 | }, 299 | "random" => match param { 300 | "rpc" => { 301 | if let Ok(url) = Url::parse(value) { 302 | if self.runner_rpcs.get(&Random) == Some(&url) { 303 | self.print(&format!("{} rpc url already set to that value", runner)); 304 | return Err(AppStatus::RuntimeError); 305 | } 306 | self.runner_rpcs.insert(Random, url); 307 | self.print(&format!("{} rpc url set to {}", runner, value)); 308 | } else { 309 | self.print(&format!("invalid rpc url: {}", value)); 310 | return Err(AppStatus::RuntimeError); 311 | } 312 | } 313 | "sk" => { 314 | if let Ok(sk) = parse_sk(value) { 315 | if self.runner_sks.get(&Random) == Some(&sk) { 316 | self.print(&format!("{} sk already set to that value", runner)); 317 | return Err(AppStatus::RuntimeError); 318 | } 319 | self.runner_sks.insert(Random, sk); 320 | self.print(&format!("{} sk set to {}", runner, value)); 321 | } else { 322 | self.print(&format!("invalid sk: {}", value)); 323 | return Err(AppStatus::RuntimeError); 324 | } 325 | } 326 | "seed" => { 327 | if let Ok(seed) = value.parse::() { 328 | if self.runner_seeds.get(&Random) == Some(&seed) { 329 | self.print(&format!("{} seed already set to that value", runner)); 330 | return Err(AppStatus::RuntimeError); 331 | } 332 | self.runner_seeds.insert(Random, seed); 333 | self.print(&format!("{} seed set to {}", runner, value)); 334 | } else { 335 | self.print(&format!("invalid seed: {}", value)); 336 | return Err(AppStatus::RuntimeError); 337 | } 338 | } 339 | _ => unreachable!(), 340 | }, 341 | _ => unreachable!(), 342 | } 343 | 344 | Ok(()) 345 | } 346 | 347 | // Helper function to handle resetting global config 348 | fn handle_global_reset(&mut self, param: &str) -> Result<(), AppStatus> { 349 | match param { 350 | "all" => { 351 | let sk = SigningKey::from_slice(hex::decode(SK).unwrap().as_slice()).unwrap(); 352 | let url = Url::parse("http://localhost:8545").unwrap(); 353 | if self.rpc_url == url && self.seed == 0 && self.sk == sk { 354 | self.print("global config is already reset"); 355 | return Err(AppStatus::RuntimeError); 356 | } 357 | self.rpc_url = url; 358 | self.seed = 0; 359 | self.sk = sk; 360 | } 361 | "rpc" => { 362 | let url = Url::parse("http://localhost:8545").unwrap(); 363 | if self.rpc_url == url { 364 | self.print("global rpc url is already reset"); 365 | return Err(AppStatus::RuntimeError); 366 | } 367 | self.rpc_url = url; 368 | } 369 | "sk" => { 370 | let sk = SigningKey::from_slice(hex::decode(SK).unwrap().as_slice()).unwrap(); 371 | if self.sk == sk { 372 | self.print("global sk is already reset"); 373 | return Err(AppStatus::RuntimeError); 374 | } 375 | self.sk = sk; 376 | } 377 | "seed" => { 378 | if self.seed == 0 { 379 | self.print("global seed is already reset"); 380 | return Err(AppStatus::RuntimeError); 381 | } 382 | self.seed = 0; 383 | } 384 | _ => unreachable!(), 385 | } 386 | 387 | Ok(()) 388 | } 389 | 390 | fn handle_runner_reset(&mut self, runner: &str, param: &str) -> Result<(), AppStatus> { 391 | match runner { 392 | "al" => match param { 393 | "all" => { 394 | if self.runner_rpcs.get(&AL) == None 395 | && self.runner_sks.get(&AL) == None 396 | && self.runner_seeds.get(&AL) == None 397 | { 398 | self.print(&format!("{} runner is already reset", runner)); 399 | return Err(AppStatus::RuntimeError); 400 | } 401 | self.runner_rpcs.remove(&AL); 402 | self.runner_sks.remove(&AL); 403 | self.runner_seeds.remove(&AL); 404 | } 405 | "rpc" => { 406 | if self.runner_rpcs.get(&AL) == None { 407 | self.print(&format!("{} runner rpc is already reset", runner)); 408 | return Err(AppStatus::RuntimeError); 409 | } 410 | self.runner_rpcs.remove(&AL); 411 | } 412 | "sk" => { 413 | if self.runner_sks.get(&AL) == None { 414 | self.print(&format!("{} runner sk is already reset", runner)); 415 | return Err(AppStatus::RuntimeError); 416 | } 417 | self.runner_sks.remove(&AL); 418 | } 419 | "seed" => { 420 | if self.runner_seeds.get(&AL) == None { 421 | self.print(&format!("{} runner seed is already reset", runner)); 422 | return Err(AppStatus::RuntimeError); 423 | } 424 | self.runner_seeds.remove(&AL); 425 | } 426 | _ => unreachable!(), 427 | }, 428 | "blob" => match param { 429 | "all" => { 430 | if self.runner_rpcs.get(&Blob) == None 431 | && self.runner_sks.get(&Blob) == None 432 | && self.runner_seeds.get(&Blob) == None 433 | { 434 | self.print(&format!("{} runner is already reset", runner)); 435 | return Err(AppStatus::RuntimeError); 436 | } 437 | self.runner_rpcs.remove(&Blob); 438 | self.runner_sks.remove(&Blob); 439 | self.runner_seeds.remove(&Blob); 440 | } 441 | "rpc" => { 442 | if self.runner_rpcs.get(&Blob) == None { 443 | self.print(&format!("{} runner rpc is already reset", runner)); 444 | return Err(AppStatus::RuntimeError); 445 | } 446 | self.runner_rpcs.remove(&Blob); 447 | } 448 | "sk" => { 449 | if self.runner_sks.get(&Blob) == None { 450 | self.print(&format!("{} runner sk is already reset", runner)); 451 | return Err(AppStatus::RuntimeError); 452 | } 453 | self.runner_sks.remove(&Blob); 454 | } 455 | "seed" => { 456 | if self.runner_seeds.get(&Blob) == None { 457 | self.print(&format!("{} runner seed is already reset", runner)); 458 | return Err(AppStatus::RuntimeError); 459 | } 460 | self.runner_seeds.remove(&Blob); 461 | } 462 | _ => unreachable!(), 463 | }, 464 | "eip1559" => match param { 465 | "all" => { 466 | if self.runner_rpcs.get(&EIP1559) == None 467 | && self.runner_sks.get(&EIP1559) == None 468 | && self.runner_seeds.get(&EIP1559) == None 469 | { 470 | self.print(&format!("{} runner is already reset", runner)); 471 | return Err(AppStatus::RuntimeError); 472 | } 473 | self.runner_rpcs.remove(&EIP1559); 474 | self.runner_sks.remove(&EIP1559); 475 | self.runner_seeds.remove(&EIP1559); 476 | } 477 | "rpc" => { 478 | if self.runner_rpcs.get(&EIP1559) == None { 479 | self.print(&format!("{} runner rpc is already reset", runner)); 480 | return Err(AppStatus::RuntimeError); 481 | } 482 | self.runner_rpcs.remove(&EIP1559); 483 | } 484 | "sk" => { 485 | if self.runner_sks.get(&EIP1559) == None { 486 | self.print(&format!("{} runner sk is already reset", runner)); 487 | return Err(AppStatus::RuntimeError); 488 | } 489 | self.runner_sks.remove(&EIP1559); 490 | } 491 | "seed" => { 492 | if self.runner_seeds.get(&EIP1559) == None { 493 | self.print(&format!("{} runner seed is already reset", runner)); 494 | return Err(AppStatus::RuntimeError); 495 | } 496 | self.runner_seeds.remove(&EIP1559); 497 | } 498 | _ => unreachable!(), 499 | }, 500 | "eip7702" => match param { 501 | "all" => { 502 | if self.runner_rpcs.get(&EIP7702) == None 503 | && self.runner_sks.get(&EIP7702) == None 504 | && self.runner_seeds.get(&EIP7702) == None 505 | { 506 | self.print(&format!("{} runner is already reset", runner)); 507 | return Err(AppStatus::RuntimeError); 508 | } 509 | self.runner_rpcs.remove(&EIP7702); 510 | self.runner_sks.remove(&EIP7702); 511 | self.runner_seeds.remove(&EIP7702); 512 | } 513 | "rpc" => { 514 | if self.runner_rpcs.get(&EIP7702) == None { 515 | self.print(&format!("{} runner rpc is already reset", runner)); 516 | return Err(AppStatus::RuntimeError); 517 | } 518 | self.runner_rpcs.remove(&EIP7702); 519 | } 520 | "sk" => { 521 | if self.runner_sks.get(&EIP7702) == None { 522 | self.print(&format!("{} runner sk is already reset", runner)); 523 | return Err(AppStatus::RuntimeError); 524 | } 525 | self.runner_sks.remove(&EIP7702); 526 | } 527 | "seed" => { 528 | if self.runner_seeds.get(&EIP7702) == None { 529 | self.print(&format!("{} runner seed is already reset", runner)); 530 | return Err(AppStatus::RuntimeError); 531 | } 532 | self.runner_seeds.remove(&EIP7702); 533 | } 534 | _ => unreachable!(), 535 | }, 536 | "legacy" => match param { 537 | "all" => { 538 | if self.runner_rpcs.get(&Legacy) == None 539 | && self.runner_sks.get(&Legacy) == None 540 | && self.runner_seeds.get(&Legacy) == None 541 | { 542 | self.print(&format!("{} runner is already reset", runner)); 543 | return Err(AppStatus::RuntimeError); 544 | } 545 | self.runner_rpcs.remove(&Legacy); 546 | self.runner_sks.remove(&Legacy); 547 | self.runner_seeds.remove(&Legacy); 548 | } 549 | "rpc" => { 550 | if self.runner_rpcs.get(&Legacy) == None { 551 | self.print(&format!("{} runner rpc is already reset", runner)); 552 | return Err(AppStatus::RuntimeError); 553 | } 554 | self.runner_rpcs.remove(&Legacy); 555 | } 556 | "sk" => { 557 | if self.runner_sks.get(&Legacy) == None { 558 | self.print(&format!("{} runner sk is already reset", runner)); 559 | return Err(AppStatus::RuntimeError); 560 | } 561 | self.runner_sks.remove(&Legacy); 562 | } 563 | "seed" => { 564 | if self.runner_seeds.get(&Legacy) == None { 565 | self.print(&format!("{} runner seed is already reset", runner)); 566 | return Err(AppStatus::RuntimeError); 567 | } 568 | self.runner_seeds.remove(&Legacy); 569 | } 570 | _ => unreachable!(), 571 | }, 572 | "random" => match param { 573 | "all" => { 574 | if self.runner_rpcs.get(&Random) == None 575 | && self.runner_sks.get(&Random) == None 576 | && self.runner_seeds.get(&Random) == None 577 | { 578 | self.print(&format!("{} runner is already reset", runner)); 579 | return Err(AppStatus::RuntimeError); 580 | } 581 | self.runner_rpcs.remove(&Random); 582 | self.runner_sks.remove(&Random); 583 | self.runner_seeds.remove(&Random); 584 | } 585 | "rpc" => { 586 | if self.runner_rpcs.get(&Random) == None { 587 | self.print(&format!("{} runner rpc is already reset", runner)); 588 | return Err(AppStatus::RuntimeError); 589 | } 590 | self.runner_rpcs.remove(&Random); 591 | } 592 | "sk" => { 593 | if self.runner_sks.get(&Random) == None { 594 | self.print(&format!("{} runner sk is already reset", runner)); 595 | return Err(AppStatus::RuntimeError); 596 | } 597 | self.runner_sks.remove(&Random); 598 | } 599 | "seed" => { 600 | if self.runner_seeds.get(&Random) == None { 601 | self.print(&format!("{} runner seed is already reset", runner)); 602 | return Err(AppStatus::RuntimeError); 603 | } 604 | self.runner_seeds.remove(&Random); 605 | } 606 | _ => unreachable!(), 607 | }, 608 | _ => unreachable!(), 609 | } 610 | 611 | Ok(()) 612 | } 613 | 614 | // Helper function to handle commands 615 | pub async fn handle_command(&mut self, command: String) -> Result<(), AppStatus> { 616 | let command = command.trim(); 617 | 618 | // Handle set command 619 | if command.starts_with("set ") { 620 | let parts: Vec<&str> = command.splitn(4, ' ').collect(); 621 | if parts.len() == 4 { 622 | let scope = parts[1]; 623 | let param = parts[2]; 624 | let value = parts[3]; 625 | 626 | if !self.is_valid_scope(scope) { 627 | self.print(&format!("invalid scope: {}", scope)); 628 | return Err(AppStatus::RuntimeError); 629 | } 630 | 631 | if !self.is_valid_param(param) { 632 | self.print(&format!("invalid parameter: {}", param)); 633 | return Err(AppStatus::RuntimeError); 634 | } 635 | 636 | match scope { 637 | "global" => { 638 | return self.handle_global_set(param, value); 639 | } 640 | _ => { 641 | return self.handle_runner_set(scope, param, value); 642 | } 643 | } 644 | } 645 | } 646 | 647 | // Handle reset command 648 | if command.starts_with("reset ") { 649 | let parts: Vec<&str> = command.splitn(3, ' ').collect(); 650 | if parts.len() == 3 { 651 | let scope = parts[1]; 652 | let param = parts[2]; 653 | 654 | if !self.is_valid_scope(scope) { 655 | self.print(&format!("invalid scope: {}", scope)); 656 | return Err(AppStatus::RuntimeError); 657 | } 658 | 659 | if !self.is_valid_param(param) && param != "all" { 660 | self.print(&format!("invalid parameter: {}", param)); 661 | return Err(AppStatus::RuntimeError); 662 | } 663 | 664 | if scope == "global" { 665 | return self.handle_global_reset(param); 666 | } else if self.is_valid_runner(scope) { 667 | return self.handle_runner_reset(scope, param); 668 | } else { 669 | self.print(&format!("invalid scope: {}", scope)); 670 | return Err(AppStatus::RuntimeError); 671 | } 672 | } else { 673 | self.print( 674 | "invalid reset command format. Use: reset ", 675 | ); 676 | return Err(AppStatus::RuntimeError); 677 | } 678 | } 679 | 680 | if command == "stop" { 681 | if self.active_runners.is_empty() { 682 | self.print("no runners to stop"); 683 | return Err(AppStatus::RuntimeError); 684 | } 685 | 686 | let runners: Vec<_> = self.active_runners.keys().cloned().collect(); 687 | for runner in runners { 688 | if let Err(e) = self.stop_runner(runner).await { 689 | self.print(&format!("error stopping runner: {}", e)); 690 | return Err(AppStatus::RuntimeError); 691 | } 692 | } 693 | self.print("all runners stopped"); 694 | return Ok(()); 695 | } 696 | 697 | if command.starts_with("stop ") { 698 | let parts: Vec<&str> = command.splitn(2, ' ').collect(); 699 | if parts.len() == 2 { 700 | let runner = parts[1]; 701 | if !self.is_valid_runner(runner) { 702 | self.print(&format!("invalid runner: {}", runner)); 703 | return Err(AppStatus::RuntimeError); 704 | } 705 | 706 | if !self.active_runners.contains_key(&Runner::from_str(runner).unwrap()) { 707 | self.print(&format!("{} runner is not active", runner)); 708 | return Err(AppStatus::RuntimeError); 709 | } 710 | 711 | if let Err(e) = self.stop_runner(Runner::from_str(runner).unwrap()).await { 712 | self.print(&format!("error stopping runner: {}", e)); 713 | return Err(AppStatus::RuntimeError); 714 | } 715 | self.print(&format!("{} runner stopped", runner)); 716 | return Ok(()); 717 | } else { 718 | self.print("invalid stop command format. Use: stop "); 719 | return Err(AppStatus::RuntimeError); 720 | } 721 | } 722 | 723 | if command == "exit" { 724 | // Stop all runners before exiting 725 | for runner in [ 726 | AL, Blob, EIP1559, EIP7702, Legacy, Random, 727 | ] { 728 | let _ = self.stop_runner(runner).await; 729 | } 730 | return Err(AppStatus::Exit); 731 | } 732 | 733 | if command == "start" { 734 | for runner in [ 735 | AL, Blob, EIP1559, EIP7702, Legacy, Random, 736 | ] { 737 | let _ = self.start_runner(runner).await; 738 | } 739 | self.print("all runners started"); 740 | return Ok(()); 741 | } 742 | 743 | if command.starts_with("start ") { 744 | let parts: Vec<&str> = command.splitn(2, ' ').collect(); 745 | if parts.len() == 2 { 746 | let runner = parts[1]; 747 | if !self.is_valid_runner(runner) { 748 | self.print(&format!("invalid runner: {}", runner)); 749 | return Err(AppStatus::RuntimeError); 750 | } 751 | 752 | if self.active_runners.contains_key(&Runner::from_str(runner).unwrap()) { 753 | self.print(&format!("{} runner is already active", runner)); 754 | return Err(AppStatus::RuntimeError); 755 | } 756 | 757 | if let Err(e) = self.start_runner(Runner::from_str(runner).unwrap()).await { 758 | self.print(&format!("error starting runner: {}", e)); 759 | return Err(AppStatus::RuntimeError); 760 | } 761 | self.print(&format!("{} runner started", runner)); 762 | return Ok(()); 763 | } else { 764 | self.print("invalid start command format. Use: start "); 765 | return Err(AppStatus::RuntimeError); 766 | } 767 | } 768 | 769 | self.print("invalid command"); 770 | Err(AppStatus::RuntimeError) 771 | } 772 | } 773 | --------------------------------------------------------------------------------