From 6c75b264b35f3b3c017af5fb3ac107534c64ee59 Mon Sep 17 00:00:00 2001 From: Louise Poole Date: Wed, 16 Oct 2024 19:27:35 +0200 Subject: [PATCH] feat: implement maybe_coerce_error utils function --- Cargo.lock | 214 ++++++++++++++++++++++++++++++-------- Cargo.toml | 1 + src/protocol/vm/utils.rs | 215 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 386 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45bbe51c..2534cf05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,7 +327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a43b18702501396fa9bcdeecd533bc85fac75150d308fc0f6800a01e6234a003" dependencies = [ "alloy-rlp-derive", - "arrayvec", + "arrayvec 0.7.2", "bytes", ] @@ -934,6 +934,12 @@ dependencies = [ "rand", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.2" @@ -1423,6 +1429,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ + "block-padding", "generic-array", ] @@ -1435,6 +1442,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "blockifier" version = "0.2.0-rc1" @@ -1463,7 +1476,7 @@ dependencies = [ "phf", "serde", "serde_json", - "sha3", + "sha3 0.10.8", "starknet-crypto 0.5.1", "starknet_api", "strum 0.24.1", @@ -1537,6 +1550,12 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "byte-slice-cast" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0a5e3906bcbf133e33c1d4d95afc664ad37fbdb9f6568d8043e7ea8c27d93d3" + [[package]] name = "byte-slice-cast" version = "1.2.2" @@ -1674,7 +1693,7 @@ dependencies = [ "indoc 2.0.4", "num-bigint", "num-traits 0.2.17", - "parity-scale-codec", + "parity-scale-codec 3.6.4", "parity-scale-codec-derive", "schemars", "serde", @@ -1926,7 +1945,7 @@ dependencies = [ "regex", "salsa", "serde", - "sha3", + "sha3 0.10.8", "smol_str", "thiserror", ] @@ -2044,7 +2063,7 @@ dependencies = [ "once_cell", "serde", "serde_json", - "sha3", + "sha3 0.10.8", "smol_str", "thiserror", ] @@ -2085,7 +2104,7 @@ dependencies = [ "itertools 0.11.0", "num-bigint", "num-traits 0.2.17", - "parity-scale-codec", + "parity-scale-codec 3.6.4", "schemars", "serde", ] @@ -2119,7 +2138,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.8", - "sha3", + "sha3 0.10.8", "starknet-crypto 0.5.1", "thiserror-no-std", ] @@ -2346,7 +2365,7 @@ dependencies = [ "serde", "serde_derive", "sha2 0.10.8", - "sha3", + "sha3 0.10.8", "thiserror", ] @@ -2365,7 +2384,7 @@ dependencies = [ "ripemd", "serde", "sha2 0.10.8", - "sha3", + "sha3 0.10.8", "thiserror", ] @@ -3014,7 +3033,7 @@ dependencies = [ "rand", "rlp", "serde", - "sha3", + "sha3 0.10.8", "zeroize", ] @@ -3092,28 +3111,57 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.8", - "sha3", + "sha3 0.10.8", "thiserror", "uuid 0.8.2", ] +[[package]] +name = "ethabi" +version = "13.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d4e679d6864bc26210feb5cf044e245741cd9d7701b35c00440a6e84d61399" +dependencies = [ + "anyhow", + "ethereum-types 0.10.0", + "hex", + "serde", + "serde_json", + "sha3 0.9.1", + "thiserror", + "uint", +] + [[package]] name = "ethabi" version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" dependencies = [ - "ethereum-types", + "ethereum-types 0.14.1", "hex", "once_cell", "regex", "serde", "serde_json", - "sha3", + "sha3 0.10.8", "thiserror", "uint", ] +[[package]] +name = "ethbloom" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a621dcebea74f2a6f2002d0a885c81ccf6cbdf86760183316a7722b5707ca4" +dependencies = [ + "crunchy", + "fixed-hash 0.7.0", + "impl-rlp", + "impl-serde 0.3.2", + "tiny-keccak", +] + [[package]] name = "ethbloom" version = "0.13.0" @@ -3121,26 +3169,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" dependencies = [ "crunchy", - "fixed-hash", - "impl-codec", + "fixed-hash 0.8.0", + "impl-codec 0.6.0", "impl-rlp", - "impl-serde", + "impl-serde 0.4.0", "scale-info", "tiny-keccak", ] +[[package]] +name = "ethereum-types" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05dc5f0df4915fa6dff7f975a8366ecfaaa8959c74235469495153e7bb1b280e" +dependencies = [ + "ethbloom 0.10.0", + "fixed-hash 0.7.0", + "impl-rlp", + "impl-serde 0.3.2", + "primitive-types 0.8.0", + "uint", +] + [[package]] name = "ethereum-types" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" dependencies = [ - "ethbloom", - "fixed-hash", - "impl-codec", + "ethbloom 0.13.0", + "fixed-hash 0.8.0", + "impl-codec 0.6.0", "impl-rlp", - "impl-serde", - "primitive-types", + "impl-serde 0.4.0", + "primitive-types 0.12.1", "scale-info", "uint", ] @@ -3151,7 +3213,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d3627f83d8b87b432a5fad9934b4565260722a141a2c40f371f8080adec9425" dependencies = [ - "ethereum-types", + "ethereum-types 0.14.1", "itertools 0.10.5", "smallvec", ] @@ -3249,13 +3311,13 @@ version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82d80cc6ad30b14a48ab786523af33b37f28a8623fc06afd55324816ef18fb1f" dependencies = [ - "arrayvec", + "arrayvec 0.7.2", "bytes", "cargo_metadata 0.18.1", "chrono", "const-hex", "elliptic-curve", - "ethabi", + "ethabi 18.0.0", "generic-array", "k256", "num_enum", @@ -3427,7 +3489,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" dependencies = [ - "arrayvec", + "arrayvec 0.7.2", "auto_impl", "bytes", ] @@ -3457,6 +3519,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -4682,13 +4756,22 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "impl-codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be51a921b067b0eaca2fad532d9400041561aa922221cc65f95a85641c6bf53" +dependencies = [ + "parity-scale-codec 1.3.7", +] + [[package]] name = "impl-codec" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" dependencies = [ - "parity-scale-codec", + "parity-scale-codec 3.6.4", ] [[package]] @@ -4700,6 +4783,15 @@ dependencies = [ "rlp", ] +[[package]] +name = "impl-serde" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" +dependencies = [ + "serde", +] + [[package]] name = "impl-serde" version = "0.4.0" @@ -5395,7 +5487,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" dependencies = [ - "arrayvec", + "arrayvec 0.7.2", "itoa", ] @@ -5548,10 +5640,10 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" dependencies = [ - "arrayvec", + "arrayvec 0.7.2", "auto_impl", "bytes", - "ethereum-types", + "ethereum-types 0.14.1", "open-fastrlp-derive", ] @@ -5644,15 +5736,27 @@ dependencies = [ "group", ] +[[package]] +name = "parity-scale-codec" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b26b16c7687c3075982af47719e481815df30bc544f7a6690763a25ca16e9d" +dependencies = [ + "arrayvec 0.5.2", + "bitvec 0.17.4", + "byte-slice-cast 0.3.5", + "serde", +] + [[package]] name = "parity-scale-codec" version = "3.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8e946cc0cc711189c0b0249fb8b599cbeeab9784d83c415719368bb8d4ac64" dependencies = [ - "arrayvec", + "arrayvec 0.7.2", "bitvec 1.0.1", - "byte-slice-cast", + "byte-slice-cast 1.2.2", "impl-trait-for-tuples", "parity-scale-codec-derive", "serde", @@ -6005,16 +6109,29 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "primitive-types" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3824ae2c5e27160113b9e029a10ec9e3f0237bad8029f69c7724393c9fdefd8" +dependencies = [ + "fixed-hash 0.7.0", + "impl-codec 0.4.2", + "impl-rlp", + "impl-serde 0.3.2", + "uint", +] + [[package]] name = "primitive-types" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" dependencies = [ - "fixed-hash", - "impl-codec", + "fixed-hash 0.8.0", + "impl-codec 0.6.0", "impl-rlp", - "impl-serde", + "impl-serde 0.4.0", "scale-info", "uint", ] @@ -6136,6 +6253,7 @@ dependencies = [ "chrono", "dotenv", "enum_dispatch", + "ethabi 13.0.0", "ethers", "foundry-config", "foundry-evm", @@ -6835,8 +6953,8 @@ dependencies = [ "fastrlp", "num-bigint", "num-traits 0.2.17", - "parity-scale-codec", - "primitive-types", + "parity-scale-codec 3.6.4", + "primitive-types 0.12.1", "proptest", "rand", "rlp", @@ -7106,7 +7224,7 @@ checksum = "0cfdffd972d76b22f3d7f81c8be34b2296afd3a25e0a547bd9abe340a4dbbe97" dependencies = [ "cfg-if", "derive_more", - "parity-scale-codec", + "parity-scale-codec 3.6.4", "scale-info-derive", ] @@ -7486,6 +7604,18 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", +] + [[package]] name = "sha3" version = "0.10.8" @@ -7727,7 +7857,7 @@ dependencies = [ "serde_json", "serde_json_pythonic", "serde_with 2.3.3", - "sha3", + "sha3 0.10.8", "starknet-crypto 0.6.1", "starknet-ff", ] @@ -7745,7 +7875,7 @@ dependencies = [ "serde_json", "serde_json_pythonic", "serde_with 2.3.3", - "sha3", + "sha3 0.10.8", "starknet-crypto 0.6.1", "starknet-ff", ] @@ -7852,7 +7982,7 @@ checksum = "dbbfccb46a8969fb3ac803718d9d8270cff4eed5b7f6b9ba234875ad2cc997c5" dependencies = [ "async-trait", "auto_impl", - "ethereum-types", + "ethereum-types 0.14.1", "flate2", "log", "reqwest 0.11.22", @@ -7891,7 +8021,7 @@ dependencies = [ "hex", "indexmap 1.9.3", "once_cell", - "primitive-types", + "primitive-types 0.12.1", "serde", "serde_json", "starknet-crypto 0.5.1", @@ -7924,7 +8054,7 @@ dependencies = [ "serde", "serde_json", "serde_json_pythonic", - "sha3", + "sha3 0.10.8", "starknet", "starknet-crypto 0.5.1", "starknet_api", diff --git a/Cargo.toml b/Cargo.toml index 8e693313..e6a28048 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" members = ["protosim_py"] [dependencies] +ethabi = "13.0" ethers = "2.0.13" enum_dispatch = "0.3.8" serde_json = "1.0.105" diff --git a/src/protocol/vm/utils.rs b/src/protocol/vm/utils.rs index 47ebae47..374f2284 100644 --- a/src/protocol/vm/utils.rs +++ b/src/protocol/vm/utils.rs @@ -1,10 +1,12 @@ // TODO: remove skip for clippy dead_code check #![allow(dead_code)] - +use ethabi::{self, decode, ParamType}; +use hex::FromHex; use mini_moka::sync::Cache; use reqwest::{blocking::Client, StatusCode}; use serde_json::json; use std::{ + collections::HashMap, env, fs::File, io::Read, @@ -12,12 +14,129 @@ use std::{ sync::{Arc, LazyLock}, time::Duration, }; +use thiserror::Error; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum RpcError { + #[error("HTTP Error: {0}")] Http(reqwest::Error), + #[error("RPC Error: {0}. Status code: {1}")] Rpc(String, StatusCode), + #[error("Invalid Response: {0}")] InvalidResponse(String), + #[error("Out of Gas: {0}. Pool state: {1}")] + OutOfGas(String, String), +} + +pub fn maybe_coerce_error( + err: RpcError, + pool_state: &str, + gas_limit: Option, + gas_used: Option, +) -> Result<(), RpcError> { + match err { + // Check for revert situation (if error message starts with "0x") + RpcError::InvalidResponse(ref details) if details.starts_with("0x") => { + let reason = parse_solidity_error_message(details); + let err = RpcError::InvalidResponse(format!("Revert! Reason: {}", reason)); + + // Check if we are running out of gas + if let (Some(gas_limit), Some(gas_used)) = (gas_limit, gas_used) { + // if we used up 97% or more issue a OutOfGas error. + let usage = gas_used as f64 / gas_limit as f64; + if usage >= 0.97 { + return Err(RpcError::OutOfGas( + format!( + "SimulationError: Likely out-of-gas. Used {:.2}% of gas limit. Original error: {}", + usage * 100.0, + err + ), + pool_state.to_string(), + )); + } + } + Err(err) + } + + // Check if "OutOfGas" is part of the error message + RpcError::InvalidResponse(ref details) if details.contains("OutOfGas") => { + let usage_msg = if let (Some(gas_limit), Some(gas_used)) = (gas_limit, gas_used) { + let usage = gas_used as f64 / gas_limit as f64; + format!("Used: {:.2}% of gas limit. ", usage * 100.0) + } else { + String::new() + }; + + Err(RpcError::OutOfGas( + format!("SimulationError: out-of-gas. {}Original error: {}", usage_msg, details), + pool_state.to_string(), + )) + } + + // Otherwise return the original error + _ => Err(err), + } +} + +fn parse_solidity_error_message(data: &str) -> String { + let data_bytes = match Vec::from_hex(&data[2..]) { + Ok(bytes) => bytes, + Err(_) => return format!("Failed to decode: {}", data), + }; + + // Check for specific error selectors: + // Solidity Error(string) signature: 0x08c379a0 + if data_bytes.starts_with(&[0x08, 0xc3, 0x79, 0xa0]) { + if let Ok(decoded) = decode(&[ParamType::String], &data_bytes[4..]) { + if let Some(ethabi::Token::String(error_string)) = decoded.first() { + return error_string.clone(); + } + } + + // Solidity Panic(uint256) signature: 0x4e487b71 + } else if data_bytes.starts_with(&[0x4e, 0x48, 0x7b, 0x71]) { + if let Ok(decoded) = decode(&[ParamType::Uint(256)], &data_bytes[4..]) { + if let Some(ethabi::Token::Uint(error_code)) = decoded.first() { + let panic_codes = get_solidity_panic_codes(); + return panic_codes + .get(&error_code.as_u64()) + .cloned() + .unwrap_or_else(|| format!("Panic({})", error_code)); + } + } + } + + // Try decoding as a string (old Solidity revert case) + if let Ok(decoded) = decode(&[ParamType::String], &data_bytes) { + if let Some(ethabi::Token::String(error_string)) = decoded.first() { + return error_string.clone(); + } + } + + // Custom error, try to decode string again with offset + if let Ok(decoded) = decode(&[ParamType::String], &data_bytes[4..]) { + if let Some(ethabi::Token::String(error_string)) = decoded.first() { + return error_string.clone(); + } + } + + // Fallback if no decoding succeeded + format!("Failed to decode: {}", data) +} + +fn get_solidity_panic_codes() -> HashMap { + let mut panic_codes = HashMap::new(); + panic_codes.insert(0, "GenericCompilerPanic".to_string()); + panic_codes.insert(1, "AssertionError".to_string()); + panic_codes.insert(17, "ArithmeticOver/Underflow".to_string()); + panic_codes.insert(18, "ZeroDivisionError".to_string()); + panic_codes.insert(33, "UnknownEnumMember".to_string()); + panic_codes.insert(34, "BadStorageByteArrayEncoding".to_string()); + panic_codes.insert(51, "EmptyArray".to_string()); + panic_codes.insert(0x32, "OutOfBounds".to_string()); + panic_codes.insert(0x41, "OutOfMemory".to_string()); + panic_codes.insert(0x51, "BadFunctionPointer".to_string()); + panic_codes } fn exec_rpc_method( @@ -153,6 +272,98 @@ mod tests { } } + #[test] + fn test_maybe_coerce_error_revert_no_gas_info() { + let err = RpcError::InvalidResponse("0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000011496e76616c6964206f7065726174696f6e000000000000000000000000000000".to_string()); + + let result = maybe_coerce_error(err, "test_pool", None, None); + + assert!(result.is_err()); + if let Err(RpcError::InvalidResponse(message)) = result { + assert!(message.contains("Revert! Reason: Invalid operation")); + } else { + panic!("Expected InvalidResponse error"); + } + } + + #[test] + fn test_maybe_coerce_error_out_of_gas() { + // Test out-of-gas situation with gas limit and gas used provided + let err = RpcError::InvalidResponse("OutOfGas".to_string()); + + let result = maybe_coerce_error(err, "test_pool", Some(1000), Some(980)); + + assert!(result.is_err()); + if let Err(RpcError::OutOfGas(message, pool_state)) = result { + assert!(message.contains("Used: 98.00% of gas limit.")); + assert_eq!(pool_state, "test_pool"); + } else { + panic!("Expected OutOfGas error"); + } + } + + #[test] + fn test_maybe_coerce_error_no_gas_limit_info() { + // Test out-of-gas situation without gas limit info + let err = RpcError::InvalidResponse("OutOfGas".to_string()); + + let result = maybe_coerce_error(err, "test_pool", None, None); + + assert!(result.is_err()); + if let Err(RpcError::OutOfGas(message, pool_state)) = result { + assert!(message.contains("Original error: OutOfGas")); + assert_eq!(pool_state, "test_pool"); + } else { + panic!("Expected OutOfGas error"); + } + } + + #[test] + fn test_maybe_coerce_error_no_match() { + // Test for non-revert, non-out-of-gas errors + let err = RpcError::Rpc("Some other error".to_string(), StatusCode::BAD_REQUEST); + + let result = maybe_coerce_error(err, "test_pool", None, None); + + assert!(result.is_err()); + if let Err(RpcError::Rpc(message, status)) = result { + assert_eq!(message, "Some other error"); + assert_eq!(status, StatusCode::BAD_REQUEST); + } else { + panic!("Expected Rpc error"); + } + } + + #[test] + fn test_parse_solidity_error_message_error_string() { + // Test parsing Solidity Error(string) message + let data = "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e416d6f756e7420746f6f206c6f77000000000000000000000000000000000000"; + + let result = parse_solidity_error_message(data); + + assert_eq!(result, "Amount too low"); + } + + #[test] + fn test_parse_solidity_error_message_panic_code() { + // Test parsing Solidity Panic(uint256) message + let data = "0x4e487b710000000000000000000000000000000000000000000000000000000000000001"; + + let result = parse_solidity_error_message(data); + + assert_eq!(result, "AssertionError"); + } + + #[test] + fn test_parse_solidity_error_message_failed_to_decode() { + // Test failed decoding with invalid data + let data = "0x1234567890"; + + let result = parse_solidity_error_message(data); + + assert!(result.contains("Failed to decode")); + } + #[test] fn test_get_contract_bytecode() { // Create a temporary file with some test data