diff --git a/network/config/besu/config.toml b/network/config/besu/config.toml index c5af9d42..b3cf5567 100644 --- a/network/config/besu/config.toml +++ b/network/config/besu/config.toml @@ -5,6 +5,7 @@ logging="INFO" data-path="/opt/besu/data" host-allowlist=["*"] min-gas-price=0 +revert-reason-enabled=true # rpc rpc-http-enabled=true diff --git a/vdr/Cargo.lock b/vdr/Cargo.lock index 5cf13669..8fd7e009 100644 --- a/vdr/Cargo.lock +++ b/vdr/Cargo.lock @@ -1157,7 +1157,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.11", "indexmap", "slab", "tokio", @@ -1188,14 +1188,14 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "headers" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.7", "bytes", "headers-core", - "http", + "http 1.1.0", "httpdate", "mime", "sha1", @@ -1203,11 +1203,11 @@ dependencies = [ [[package]] name = "headers-core" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http", + "http 1.1.0", ] [[package]] @@ -1248,6 +1248,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -1255,7 +1266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.11", "pin-project-lite", ] @@ -1288,7 +1299,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.11", "http-body", "httparse", "httpdate", @@ -1343,16 +1354,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "0.5.0" @@ -1429,13 +1430,14 @@ dependencies = [ "futures", "hex", "indy-data-types", + "jsonrpc-core", "log", "log-derive", "mockall", "once_cell", "rand", "rstest", - "secp256k1 0.28.2", + "secp256k1", "serde", "serde_derive", "serde_json", @@ -2221,7 +2223,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.11", "http-body", "hyper", "hyper-tls", @@ -2433,15 +2435,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" -dependencies = [ - "secp256k1-sys 0.8.1", -] - [[package]] name = "secp256k1" version = "0.28.2" @@ -2449,16 +2442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ "rand", - "secp256k1-sys 0.9.2", -] - -[[package]] -name = "secp256k1-sys" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" -dependencies = [ - "cc", + "secp256k1-sys", ] [[package]] @@ -2516,9 +2500,9 @@ dependencies = [ [[package]] name = "serde-wasm-bindgen" -version = "0.5.0" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" dependencies = [ "js-sys", "serde", @@ -3052,7 +3036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna", "percent-encoding", ] @@ -3173,9 +3157,8 @@ dependencies = [ [[package]] name = "web3" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5388522c899d1e1c96a4c307e3797e0f697ba7c77dd8e0e625ecba9dd0342937" +version = "0.20.0" +source = "git+https://github.com/DSRCorporation/rust-web3.git#0d96b9b722ded9d0b56e9d29401a74f905052002" dependencies = [ "arrayvec", "base64 0.21.7", @@ -3188,7 +3171,7 @@ dependencies = [ "getrandom", "headers", "hex", - "idna 0.4.0", + "idna", "js-sys", "jsonrpc-core", "log", @@ -3198,7 +3181,7 @@ dependencies = [ "rand", "reqwest", "rlp", - "secp256k1 0.27.0", + "secp256k1", "serde", "serde-wasm-bindgen", "serde_json", diff --git a/vdr/Cargo.toml b/vdr/Cargo.toml index 5cd23124..1a9cc891 100644 --- a/vdr/Cargo.toml +++ b/vdr/Cargo.toml @@ -42,9 +42,10 @@ serde = "1.0.188" serde_derive = "1.0.188" serde_json = "1.0.107" thiserror = "1.0.49" -web3 = { version = "0.19.0", optional = true } +web3 = { version = "0.20.0", optional = true } web-sys = { version = "0.3.64", optional = true, features = ["Window"] } -web3-wasm = { package = "web3", version = "0.19.0", default-features = false, features = ["wasm", "http", "http-tls"], optional = true } +web3-wasm = { package = "web3", version = "0.20.0", default-features = false, features = ["wasm", "http", "http-tls"], optional = true } +jsonrpc-core = "18.0.0" [dev-dependencies] rstest = "0.18.2" @@ -52,3 +53,6 @@ mockall = "0.12.0" env_logger = "0.10.0" rand = "0.8.5" ed25519-dalek = { version = "2", features = ["rand_core"] } + +[patch.crates-io] +web3 = { git = 'https://github.com/DSRCorporation/rust-web3.git'} diff --git a/vdr/src/client/client.rs b/vdr/src/client/client.rs index 60286160..852303ec 100644 --- a/vdr/src/client/client.rs +++ b/vdr/src/client/client.rs @@ -3,6 +3,7 @@ use std::{ fmt::{Debug, Formatter}, }; +use ethabi::{AbiError, Param, ParamType}; use log::warn; use log_derive::{logfn, logfn_inputs}; @@ -24,6 +25,7 @@ pub struct LedgerClient { chain_id: u64, client: Box, contracts: HashMap>, + errors: HashMap<[u8; 4], AbiError>, network: Option, quorum_handler: Option, } @@ -52,6 +54,7 @@ impl LedgerClient { let client = Box::new(Web3Client::new(rpc_node)?); let contracts = Self::init_contracts(&client, contract_configs)?; + let errors = Self::build_error_map(&contracts)?; let quorum_handler = match quorum_config { Some(quorum_config) => Some(QuorumHandler::new(quorum_config.clone())?), @@ -62,6 +65,7 @@ impl LedgerClient { chain_id, client, contracts, + errors, network: network.map(String::from), quorum_handler, }; @@ -100,13 +104,23 @@ impl LedgerClient { .await } TransactionType::Write => self.client.submit_transaction(&transaction.encode()?).await, - }?; + }; + + let data = match result { + Ok(data) => data, + Err(VdrError::ClientTransactionReverted(revert_reason)) => { + let decoded_reason = self.decode_revert_reason(&revert_reason)?; + + return Err(VdrError::ClientTransactionReverted(decoded_reason)); + } + Err(error) => return Err(error), + }; if let Some(quorum_handler) = &self.quorum_handler { - quorum_handler.check(transaction, &result).await?; + quorum_handler.check(transaction, &data).await?; }; - Ok(result) + Ok(data) } /// Submit prepared events query to the ledger @@ -204,6 +218,106 @@ impl LedgerClient { Ok(contracts) } + fn build_error_map( + contracts: &HashMap>, + ) -> VdrResult> { + let regular_error = AbiError { + name: "Error".to_string(), + inputs: vec![Param { + name: "message".to_string(), + kind: ParamType::String, + internal_type: None, + }], + }; + + let panic_error = AbiError { + name: "Panic".to_string(), + inputs: vec![Param { + name: "code".to_string(), + kind: ParamType::Uint(256), + internal_type: None, + }], + }; + + contracts + .values() + .map(|contract| contract.errors()) + .flatten() + .chain([regular_error, panic_error].iter()) + .map(|error| { + let short_signature: [u8; 4] = + error.signature().as_bytes()[0..4].try_into().map_err(|_| { + VdrError::ClientUnexpectedError( + "Cannot convert a slice into an array of 4 bytes".to_string(), + ) + })?; + + Ok((short_signature, error.clone())) + }) + .collect() + } + + fn decode_revert_reason(&self, revert_reason: &str) -> VdrResult { + let error_data = hex::decode(revert_reason.trim_start_matches("0x")).map_err(|_| { + VdrError::ContractInvalidResponseData( + format!( + "Unable to parse the revert reason '{}': Incorrect hex string", + revert_reason + ) + .to_string(), + ) + })?; + + if error_data.len() < 4 { + return Err(VdrError::ContractInvalidResponseData( + format!( + "Unable to parse the revert reason '{}': Incorrect data", + revert_reason + ) + .to_string(), + )); + } + + let signature: &[u8; 4] = error_data[0..4].try_into().map_err(|_| { + VdrError::ClientUnexpectedError( + "Cannot convert a slice into an array of 4 bytes".to_string(), + ) + })?; + let arguments: &[u8] = &error_data[4..]; + + let error = self.errors.get(signature).ok_or_else( || { + VdrError::ContractInvalidResponseData( + format!( + "Unable to parse the revert reason '{}': Cannot match the error selector with registered errors", + revert_reason + ) + .to_string() + ) + })?; + + let decoded_args = error.decode(&arguments).map_err(|_| { + VdrError::ContractInvalidResponseData( + format!( + "Unable to parse the revert reason '{}': Failed to decode the arguments", + revert_reason + ) + .to_string(), + ) + })?; + + let inputs_str: Vec = error + .inputs + .iter() + .enumerate() + .map(|(i, input)| format!("{}: {}", input.name, decoded_args[i].to_string())) + .collect(); + + let inputs_joined = inputs_str.join(", "); + let decoded_str = format!("{}({})", error.name, inputs_joined); + + Ok(decoded_str) + } + #[logfn(Info)] #[logfn_inputs(Debug)] pub(crate) async fn get_block(&self, block: Option<&Block>) -> VdrResult { @@ -391,7 +505,10 @@ pub mod test { } mod create { - use crate::validator_control::test::VALIDATOR_CONTROL_NAME; + use crate::{ + transaction::test::write_transaction, validator_control::test::VALIDATOR_CONTROL_NAME, + SignatureData, + }; use mockall::predicate::eq; use rstest::rstest; use serde_json::Value; @@ -484,6 +601,74 @@ pub mod test { assert_eq!(error, expected_error); } + #[rstest] + #[case::regular_error( + "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a4e6f7420656e6f7567682045746865722070726f76696465642e000000000000", + VdrError::ClientTransactionReverted("Error(message: Not enough Ether provided.)".to_string()), + )] + #[case::panic_error( + "0x4e487b710000000000000000000000000000000000000000000000000000000000000011", + VdrError::ClientTransactionReverted("Panic(code: 11)".to_string()), + )] + #[case::custom_error( + "0x863b93fe000000000000000000000000f0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5", + VdrError::ClientTransactionReverted("DidNotFound(identity: f0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5)".to_string()), + )] + #[case::error_without_required_argument( + "0x863b93fe", + VdrError::ContractInvalidResponseData("Unable to parse the revert reason '0x863b93fe': Failed to decode the arguments".to_string()), + )] + #[case::error_with_extra_argument( + "0x4e487b71000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000011", + VdrError::ClientTransactionReverted("Panic(code: 11)".to_string()), + )] + #[case::incorrect_error_selector( + "0x9999999e000000000000000000000000f0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5", + VdrError::ContractInvalidResponseData("Unable to parse the revert reason '0x9999999e000000000000000000000000f0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5': Cannot match the error selector with registered errors".to_string()) + )] + #[case::incorrect_hex( + "0xQQ123456e00000000000000000000000f0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5", + VdrError::ContractInvalidResponseData("Unable to parse the revert reason '0xQQ123456e00000000000000000000000f0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5': Incorrect hex string".to_string()) + )] + #[case::empty_data( + "", + VdrError::ContractInvalidResponseData("Unable to parse the revert reason '': Incorrect data".to_string()) + )] + #[case::incorrect_data( + "0x9999", + VdrError::ContractInvalidResponseData("Unable to parse the revert reason '0x9999': Incorrect data".to_string()) + )] + async fn handle_transaction_reverts( + #[case] encoded_error_message: &'static str, + #[case] expected_error: VdrError, + ) { + let mut transaction = Transaction { + to: CONFIG.contracts.ethereum_did_registry.address.clone(), + ..write_transaction() + }; + let fake_signature = SignatureData { + recovery_id: 1, + signature: vec![1; 64], + }; + transaction.set_signature(fake_signature); + + let mut client_mock = MockClient::new(); + client_mock + .expect_submit_transaction() + .with(eq(transaction.encode().unwrap())) + .returning(|_| { + Err(VdrError::ClientTransactionReverted( + encoded_error_message.to_string(), + )) + }); + + let client = mock_custom_client(Box::new(client_mock)); + + let actual_error = client.submit_transaction(&transaction).await.unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + #[async_std::test] async fn get_receipt_invalid_transaction_hash() { let client = client(); diff --git a/vdr/src/client/implementation/web3/client.rs b/vdr/src/client/implementation/web3/client.rs index 02a046db..8a5560df 100644 --- a/vdr/src/client/implementation/web3/client.rs +++ b/vdr/src/client/implementation/web3/client.rs @@ -98,13 +98,21 @@ impl Client for Web3Client { Duration::from_millis(POLL_INTERVAL), NUMBER_TX_CONFIRMATIONS, ) - .await? - .transaction_hash - .0 - .to_vec(); + .await?; + + if receipt.is_txn_reverted() { + if let Some(revert_reason) = receipt.revert_reason { + return Err(VdrError::ClientTransactionReverted(revert_reason)); + } + + return Err(VdrError::ClientTransactionReverted("".to_string())); + } trace!("Web3Client::submit_transaction() -> {:?}", receipt); - Ok(receipt) + + let transaction_hash = receipt.transaction_hash.0.to_vec(); + + Ok(transaction_hash) } async fn call_transaction(&self, to: &str, transaction: &[u8]) -> VdrResult> { @@ -236,7 +244,7 @@ impl Client for Web3Client { }) .map(|receipt| json!(receipt).to_string())?; - trace!("Web3Client::query_events() -> {:?}", receipt); + trace!("Web3Client::get_receipt() -> {:?}", receipt); Ok(receipt) } diff --git a/vdr/src/client/implementation/web3/contract.rs b/vdr/src/client/implementation/web3/contract.rs index 0aef2411..8e573de9 100644 --- a/vdr/src/client/implementation/web3/contract.rs +++ b/vdr/src/client/implementation/web3/contract.rs @@ -6,7 +6,7 @@ use crate::{ }; use std::fmt::{Debug, Formatter}; -use ethabi::Event; +use ethabi::{AbiError, Event}; use log::warn; use log_derive::{logfn, logfn_inputs}; use std::str::FromStr; @@ -96,13 +96,19 @@ impl Contract for Web3Contract { let vdr_error = VdrError::from(err); warn!( - "Error: {:?} during getting smart contract function: {}", + "Error: {:?} during getting smart contract event: {}", vdr_error, name ); vdr_error }) } + + #[logfn(Trace)] + #[logfn_inputs(Trace)] + fn errors(&self) -> Vec<&AbiError> { + self.contract.abi().errors().collect() + } } impl Debug for Web3Contract { diff --git a/vdr/src/client/mod.rs b/vdr/src/client/mod.rs index 027204c0..217d1b32 100644 --- a/vdr/src/client/mod.rs +++ b/vdr/src/client/mod.rs @@ -5,7 +5,7 @@ pub mod quorum; use crate::{error::VdrResult, types::Address, BlockDetails, Transaction}; use async_trait::async_trait; -use ethabi::{Event, Function}; +use ethabi::{AbiError, Event, Function}; use std::fmt::Debug; pub use client::LedgerClient; @@ -105,4 +105,10 @@ pub trait Contract: Sync + Send + Debug { /// # Returns /// Contract event fn event(&self, name: &str) -> VdrResult<&Event>; + + /// Get the contract errors + /// + /// # Returns + /// Contract errors + fn errors(&self) -> Vec<&AbiError>; } diff --git a/vdr/src/error/mod.rs b/vdr/src/error/mod.rs index 4af7ed5b..b9557ae7 100644 --- a/vdr/src/error/mod.rs +++ b/vdr/src/error/mod.rs @@ -1,10 +1,15 @@ +use std::ops::RangeInclusive; + use serde_json::json; +use jsonrpc_core::types::error::{Error as RpcError, ErrorCode}; #[cfg(not(feature = "wasm"))] use web3::{ethabi::Error as Web3EthabiError, Error as Web3Error}; #[cfg(feature = "wasm")] use web3_wasm::{ethabi::Error as Web3EthabiError, Error as Web3Error}; +const RPC_SERVER_ERROR_RANGE: RangeInclusive = -32099..=-32000; + #[derive(thiserror::Error, Debug, PartialEq, Clone)] pub enum VdrError { #[error("Ledger: Quorum not reached: {}", _0)] @@ -16,7 +21,7 @@ pub enum VdrError { #[error("Ledger Client: Invalid transaction: {}", _0)] ClientInvalidTransaction(String), - #[error("Ledger Client: Invalid endorsement dara: {}", _0)] + #[error("Ledger Client: Invalid endorsement data: {}", _0)] ClientInvalidEndorsementData(String), #[error("Ledger Client: Got invalid response: {}", _0)] @@ -75,12 +80,33 @@ impl From for VdrError { match value { Web3Error::Unreachable => VdrError::ClientNodeUnreachable, Web3Error::InvalidResponse(err) => VdrError::ClientInvalidResponse(err), - Web3Error::Rpc(err) => VdrError::ClientTransactionReverted(json!(err).to_string()), + Web3Error::Rpc(err) => err.into(), _ => VdrError::ClientUnexpectedError(value.to_string()), } } } +impl From for VdrError { + fn from(value: RpcError) -> Self { + let create_unexpected_error = || VdrError::ClientUnexpectedError(json!(value).to_string()); + + match value.code { + ErrorCode::ServerError(code) if RPC_SERVER_ERROR_RANGE.contains(&code) => value + .data + .as_ref() + .and_then(|data| data.as_str()) + .map_or_else(create_unexpected_error, |data| { + if data.starts_with("0x") { + VdrError::ClientTransactionReverted(data.to_string()) + } else { + create_unexpected_error() + } + }), + _ => create_unexpected_error(), + } + } +} + impl From for VdrError { fn from(value: Web3EthabiError) -> Self { match value { @@ -100,3 +126,43 @@ impl From for VdrError { } } } + +#[cfg(test)] +pub mod test { + use super::*; + use jsonrpc_core::{ + types::error::{Error as RpcError, ErrorCode}, + Value, + }; + use rstest::rstest; + + #[rstest] + #[case::rpc_error_with_hex_string_data( + RpcError { code: ErrorCode::ServerError(-32000), message: "transaction reverted".to_string(), data: Option::Some(Value::String("0x4e487b710000000000000000000000000000000000000000000000000000000000000011".to_string()))}, + VdrError::ClientTransactionReverted("0x4e487b710000000000000000000000000000000000000000000000000000000000000011".to_string()), + )] + #[case::rpc_error_without_data( + RpcError { code: ErrorCode::ServerError(-32000), message: "transaction reverted".to_string(), data: Option::None}, + VdrError::ClientUnexpectedError("{\"code\":-32000,\"message\":\"transaction reverted\"}".to_string()), + )] + #[case::rpc_error_with_text_data( + RpcError { code: ErrorCode::ServerError(-32000), message: "transaction reverted".to_string(), data: Option::Some(Value::String("Error(message: Not enough Ether provided.)".to_string()))}, + VdrError::ClientUnexpectedError("{\"code\":-32000,\"data\":\"Error(message: Not enough Ether provided.)\",\"message\":\"transaction reverted\"}".to_string()), + )] + #[case::rpc_error_with_boolean_data( + RpcError { code: ErrorCode::ServerError(-32000), message: "Invalid request".to_string(), data: Option::Some(Value::Bool(true))}, + VdrError::ClientUnexpectedError("{\"code\":-32000,\"data\":true,\"message\":\"Invalid request\"}".to_string()), + )] + #[case::rpc_error_with_invalid_request_code( + RpcError { code: ErrorCode::InvalidRequest, message: "Invalid request".to_string(), data: Option::None}, + VdrError::ClientUnexpectedError("{\"code\":-32600,\"message\":\"Invalid request\"}".to_string()), + )] + fn convert_rpc_error_to_vdr_error_test( + #[case] rpc_error: RpcError, + #[case] expected_vdr_error: VdrError, + ) { + let actual_vdr_error: VdrError = rpc_error.into(); + + assert_eq!(actual_vdr_error, expected_vdr_error); + } +}