diff --git a/Cargo.lock b/Cargo.lock index 727e2c2..a846cdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1176,9 +1176,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -1535,9 +1535,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2063,9 +2063,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "petgraph" @@ -2860,7 +2860,7 @@ dependencies = [ [[package]] name = "sothis" -version = "0.3.1" +version = "0.3.2" dependencies = [ "clap", "ctrlc", @@ -2872,6 +2872,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "url", ] [[package]] @@ -3316,9 +3317,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", diff --git a/Cargo.toml b/Cargo.toml index 352faa9..9aca8c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sothis" -version = "0.3.1" +version = "0.3.2" edition = "2021" authors = ["makemake "] license = "GPL-3.0-or-later" @@ -23,3 +23,4 @@ rlp = "0.5.2" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" tokio = { version = "1.28.1", features = ["full"] } +url = "2.4.0" diff --git a/README.md b/README.md index 75c4e0b..bcb3caf 100644 --- a/README.md +++ b/README.md @@ -106,10 +106,11 @@ The result is saved to a JSON file that looks like this: - `--source_rpc`: RPC of the node we are getting data from. - `--contract_address`: Address of the contract we are reading storage from. - `--storage_slot`: The storage slot of the contract. +- `--terminal_block`(optional): Final block sothis will track. If not specified, sothis will track until terminated. - `--filename`(optional): Name of our output file. The default filename is formatted as: `address-{}-slot-{}-timestamp-{}.json`. - `--path`(optional): Path to our output file. The default path is the current directory. -Once you are done tracking the slot, terminate the process via a `SIGTERM` or a `SIGINT` (ctrl-c), which will terminate execution and write the file. +Once you are done tracking the slot, terminate the process via a `SIGTERM` or a `SIGINT` (ctrl-c), which will terminate execution and write the file. Keep in mind that sothis will check once per new block if you tried to terminate it. If no new block are produced on the source_rpc, sothis will not terminate and nothing will be written if you force close it. `sothis --mode track --source_rpc http://localhost:8545 --contract_address 0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6 --storage_slot 0 --filename siuuu.json --path ~/Desktop ` diff --git a/dummy_server.py b/dummy_server.py new file mode 100644 index 0000000..8bda9ea --- /dev/null +++ b/dummy_server.py @@ -0,0 +1,48 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer +import json + + +class DummyRPCHandler(BaseHTTPRequestHandler): + def _set_response(self, status_code=200): + self.send_response(status_code) + self.send_header('Content-type', 'application/json') + self.end_headers() + + def do_POST(self): + content_length = int(self.headers['Content-Length']) + request_body = self.rfile.read(content_length) + rpc_request = json.loads(request_body) + + if rpc_request['method'] == 'eth_blockNumber': + response = { + "jsonrpc": "2.0", + "id": rpc_request['id'], + "result": "0x123456" # Dummy block number + } + else: + response = { + "jsonrpc": "2.0", + "id": rpc_request['id'], + "error": { + "code": -32601, + "message": "Method not found" + } + } + + self._set_response() + self.wfile.write(json.dumps(response).encode()) + + def do_GET(self): + self._set_response(404) + self.wfile.write(b'Not found') + + +def run_server(): + server_address = ('localhost', 8000) + httpd = HTTPServer(server_address, DummyRPCHandler) + print('Dummy RPC server is running on http://localhost:8000...') + httpd.serve_forever() + + +if __name__ == '__main__': + run_server() \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 81a67db..02c7455 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,7 @@ lazy_static! { #[tokio::main] async fn main() -> Result<(), Box> { let matches = Command::new("sothis") - .version("0.3.1") + .version("0.3.2") .author("makemake ") .about("Tool for replaying historical transactions. Designed to be used with anvil or hardhat.") .arg(Arg::new("source_rpc") @@ -131,13 +131,13 @@ async fn main() -> Result<(), Box> { "historic" => { println!("Replaying in historic mode..."); - let block: String = matches.get_one::("terminal_block").expect("required").to_string(); - let block = format_number_input(&block); + let terminal_block: String = matches.get_one::("terminal_block").expect("required").to_string(); + let terminal_block = format_number_input(&terminal_block); let replay_rpc: String = matches.get_one::("replay_rpc").expect("required").to_string(); let replay_rpc = RpcConnection::new(replay_rpc); - replay_historic_blocks(source_rpc, replay_rpc, hex_to_decimal(&block)?).await?; + replay_historic_blocks(source_rpc, replay_rpc, hex_to_decimal(&terminal_block)?).await?; }, "live" => { println!("Replaying live blocks..."); @@ -149,16 +149,22 @@ async fn main() -> Result<(), Box> { } "track" => { println!("Tracking state variable..."); - println!("Send SIGTERM or SIGINT to serialize to JSON, write and stop."); + println!("Send SIGTERM or SIGINT (ctrl-c) to serialize to JSON, write and stop."); let contract_address: String = matches.get_one::("contract_address").expect("required").to_string(); let storage_slot: String = matches.get_one::("storage_slot").expect("required").to_string(); let storage_slot = U256::from_dec_str(&storage_slot)?; + + // If terminal_block is set by the user use that, otherwise have it be none + let terminal_block: Option = matches.get_one::("terminal_block").map(|x| x.parse().expect("Invalid terminal block")); + + if terminal_block == None { + println!("No terminal block set, tracking indefinitely."); + } - track_state(source_rpc, storage_slot, contract_address).await?; + track_state(source_rpc, storage_slot, contract_address, terminal_block).await?; } &_ => { - // handle this properly later panic!("Mode does not exist!"); }, } diff --git a/src/replay/replay.rs b/src/replay/replay.rs index 2c1deb5..0e19caa 100644 --- a/src/replay/replay.rs +++ b/src/replay/replay.rs @@ -46,8 +46,8 @@ pub async fn replay_historic_blocks( let app_config = APP_CONFIG.lock()?; replay_delay = app_config.replay_delay; } - - while until > replay_block { + + loop { // we write a bit of illegible code let hex_block = decimal_to_hex(replay_block + 1); // get block from historical node @@ -71,8 +71,10 @@ pub async fn replay_historic_blocks( replay_block = hex_to_decimal(&replay_rpc.block_number().await?)?; - // TODO: For some godforsaken reason i cannot do an infinite loop and break here or else it crashes. - // I feel dirty doing 2 checks for the same thing so you have to wait a bit ig. + if replay_block >= until { + break; + } + sleep(Duration::from_millis(replay_delay)); } println!("Done replaying blocks"); diff --git a/src/replay/send_transaction.rs b/src/replay/send_transaction.rs index c4a975e..c409035 100644 --- a/src/replay/send_transaction.rs +++ b/src/replay/send_transaction.rs @@ -1,51 +1,52 @@ +use crate::rpc::error::RequestError; use crate::RpcConnection; use crate::rpc::types::Transaction; use crate::APP_CONFIG; +// Abstract over the return types of send functions +impl RpcConnection { + async fn send(&self, tx: Transaction, chain_id: u64) -> Result { + if APP_CONFIG.lock().unwrap().send_as_raw { + self.send_raw_transaction(tx, chain_id).await + } else { + self.send_unsigned_transaction(tx, chain_id).await + } + } +} + // Generic function we use to replay all tx in a block. pub async fn send_transactions( replay_rpc: RpcConnection, historical_txs: Vec, chain_id: u64, ) -> Result<(), Box> { - let app_config = APP_CONFIG.lock()?; + let exit_on_tx_fail; + let entropy_threshold; + { + let app_config = APP_CONFIG.lock()?; + exit_on_tx_fail = app_config.exit_on_tx_fail; + entropy_threshold = app_config.entropy_threshold; + } let tx_amount = historical_txs.len() as f32; let mut fail_tx_amount: f32 = 0.0; - // TODO: This is really bad, please reimplement this - - if app_config.send_as_raw { - for tx in historical_txs { - // Gracefully handle errors so execution doesn't halt on error - match replay_rpc.send_raw_transaction(tx, chain_id).await { - Ok(_) => (), - Err(e) => if app_config.exit_on_tx_fail { - return Err(e.into()); - } else { - fail_tx_amount += 1.0; - println!("!!! \x1b[93mError sending transaction:\x1b[0m {} !!!", e) - } - } - } - } else { - for tx in historical_txs { - // Gracefully handle errors so execution doesn't halt on error - match replay_rpc.send_unsigned_transaction(tx, chain_id).await { - Ok(_) => (), - Err(e) => if app_config.exit_on_tx_fail { - return Err(e.into()); - } else { - fail_tx_amount += 1.0; - println!("!!! \x1b[93mError sending transaction:\x1b[0m {} !!!", e) - } + for tx in historical_txs { + // Gracefully handle errors so execution doesn't halt on error + match replay_rpc.send(tx, chain_id).await { + Ok(_) => (), + Err(e) => if exit_on_tx_fail { + return Err(e.into()); + } else { + fail_tx_amount += 1.0; + println!("!!! \x1b[93mError sending transaction:\x1b[0m {} !!!", e) } } } // Calculate the percentage of failed transactions let fail_percent = fail_tx_amount / tx_amount; - if fail_percent > app_config.entropy_threshold { + if fail_percent > entropy_threshold { println!("!!! \x1b[91mHigh entropy detected!\x1b[0m Fail ratio: {}. Consider restarting the fork\x1b[0m !!!", format!("{:.2}%", fail_percent * 100.0)); } diff --git a/src/rpc/rpc.rs b/src/rpc/rpc.rs index 0decfef..483a4ea 100644 --- a/src/rpc/rpc.rs +++ b/src/rpc/rpc.rs @@ -1,4 +1,6 @@ +use url::Url; use std::thread::sleep; +use std::time::Instant; use tokio::time::Duration; use serde::{Deserialize, Serialize}; @@ -27,27 +29,19 @@ struct JsonRpcResponse { id: u32, } +#[derive(Clone)] pub struct RpcConnection { client: Client, url: String, } -impl Clone for RpcConnection { - fn clone(&self) -> Self { - RpcConnection { - client: self.client.clone(), - url: self.url.clone(), - } - } -} - #[allow(dead_code)] impl RpcConnection { // Create client and set url pub fn new(url: String) -> Self { Self { client: Client::new(), - url, + url: Url::parse(&url).expect("REASON").into(), } } @@ -240,9 +234,23 @@ impl RpcConnection { let mut new_blocknumber = blocknumber.clone(); println!("Listening for new blocks from block {}...", hex_to_decimal(&blocknumber).unwrap()); + // Start timer for the *heartbeat* + let mut start_time = Instant::now(); + while blocknumber == new_blocknumber { - // sleep for 1 second + // sleep for set duration sleep(Duration::from_millis(time)); + + // Add this as a *heartbeat* so users are less confused if nothing is happening + let elapsed_time = start_time.elapsed(); + + if elapsed_time >= Duration::from_secs(60) { + println!("!!! \x1b[93mNo new blocks have been detected in 60 seconds! Check your node(s)\x1b[0m !!!"); + println!("Still listening..."); + start_time = Instant::now(); + } + + new_blocknumber = self.block_number().await? } diff --git a/src/rpc/types.rs b/src/rpc/types.rs index 8565972..e991392 100644 --- a/src/rpc/types.rs +++ b/src/rpc/types.rs @@ -76,8 +76,6 @@ impl Transaction { // features set to legacy, this is a legacy tx let mut typed_tx: TypedTransaction = Default::default(); - //todo: fix this - // If to doesnt contain a value, set it match self.to { Some(_) => { let address = H160::from_str(&self.to.clone().expect("Can't read `to` field")); diff --git a/src/tracker/tracker.rs b/src/tracker/tracker.rs index 5636938..0cd8d41 100644 --- a/src/tracker/tracker.rs +++ b/src/tracker/tracker.rs @@ -1,5 +1,6 @@ use crate::APP_CONFIG; use crate::RpcConnection; +use crate::rpc::format::hex_to_decimal; use crate::tracker::types::*; use crate::tracker::time::get_latest_unix_timestamp; @@ -15,6 +16,7 @@ pub async fn track_state( source_rpc: RpcConnection, storage_slot: U256, contract_address: String, + terminal_block: Option, ) -> Result<(), Box> { let interrupted = Arc::new(AtomicBool::new(false)); let interrupted_clone = interrupted.clone(); @@ -42,7 +44,9 @@ pub async fn track_state( let mut block_number = source_rpc.block_number().await?; loop { - if interrupted.load(Ordering::SeqCst) { + // Crazy hamburger check + let has_reached_terminal_block = terminal_block.as_ref().map(|tb| hex_to_decimal(&block_number).unwrap() >= *tb).unwrap_or(false); + if interrupted.load(Ordering::SeqCst) || has_reached_terminal_block { break; } @@ -72,7 +76,7 @@ pub async fn track_state( }; let path = format!("{}/{}", path, filename); - println!("Writing to file: {}", path); + println!("\nWriting to file: {}", path); fs::write(path, json)?; Ok(())