diff --git a/Cargo.lock b/Cargo.lock index 0805870..679bda0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3837,7 +3837,7 @@ dependencies = [ [[package]] name = "ghost-slow-clap" -version = "0.4.11" +version = "0.4.12" dependencies = [ "frame-benchmarking", "frame-support", diff --git a/pallets/slow-clap/Cargo.toml b/pallets/slow-clap/Cargo.toml index 976a34c..9d7d178 100644 --- a/pallets/slow-clap/Cargo.toml +++ b/pallets/slow-clap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-slow-clap" -version = "0.4.11" +version = "0.4.12" description = "Applause protocol for the EVM bridge" license.workspace = true authors.workspace = true diff --git a/pallets/slow-clap/src/lib.rs b/pallets/slow-clap/src/lib.rs index bd03deb..f46c425 100644 --- a/pallets/slow-clap/src/lib.rs +++ b/pallets/slow-clap/src/lib.rs @@ -881,10 +881,10 @@ impl Pallet { Self::prepare_evm_request_body_for_latest_block(network_data) }; - let pending_requests_metadata = + let pending_requests = Self::prepare_pending_evm_requests(&rpc_endpoints, &request_body)?; let parsed_evm_responses = - Self::fetch_multiple_evm_from_remote(pending_requests_metadata) + Self::fetch_multiple_evm_from_remote(pending_requests) .iter() .filter_map(|response_bytes| { let parsed_evm_response = @@ -965,8 +965,8 @@ impl Pallet { fn prepare_pending_evm_requests( rpc_endpoints: &Vec>, request_body: &[u8], - ) -> OffchainResult> { - let mut pending_requests_metadata = Vec::new(); + ) -> OffchainResult> { + let mut pending_requests = Vec::new(); let request_body_str = core::str::from_utf8(request_body) .map_err(|_| OffchainErr::UnparsableRequestBody(request_body.to_vec()))?; @@ -977,7 +977,11 @@ impl Pallet { let rpc_endpoint_str = match core::str::from_utf8(rpc_endpoint) { Ok(rpc_endpoint_str) => rpc_endpoint_str, Err(_) => { - log::info!(target: LOG_TARGET, "👻 Could not get valid utf8 rpc endpoint from bytes: {:?}", rpc_endpoint); + log::info!( + target: LOG_TARGET, + "👻 Could not get valid utf8 rpc endpoint from bytes; skipped \"{:?}\"", + rpc_endpoint, + ); continue; } }; @@ -989,21 +993,30 @@ impl Pallet { .send() { Ok(pending) => { - pending_requests_metadata.push((pending, rpc_endpoint_str.to_string())) + pending_requests.push(pending) } Err(_) => { - log::info!(target: LOG_TARGET, "👻 Request skipped: failed to send request to \"{}\"", rpc_endpoint_str) + log::info!( + target: LOG_TARGET, + "👻 Request skipped: failed to send request to \"{}\"", + rpc_endpoint_str, + ) } } } - if pending_requests_metadata.len() == 0 { + if pending_requests.len() == 0 { return Err(OffchainErr::NoRequestsSent); } - log::info!(target: LOG_TARGET, "👻 Requests sent {} out of {}", pending_requests_metadata.len(), rpc_endpoints.len()); + log::info!( + target: LOG_TARGET, + "👻 Requests sent {} out of {}", + pending_requests.len(), + rpc_endpoints.len(), + ); - Ok(pending_requests_metadata) + Ok(pending_requests) } fn get_balanced_evm_response( @@ -1067,31 +1080,24 @@ impl Pallet { } fn fetch_multiple_evm_from_remote( - pending_requests_metadata: Vec<(PendingRequest, String)>, + pending_requests: Vec, ) -> Vec> { - let pending_requests = pending_requests_metadata - .iter() - .map(|(pending_request, _)| PendingRequest { - id: pending_request.id, - }) - .collect::>(); + let mut requests_failed = 0; + let mut responses_failed = 0; + let mut responses_non_200 = 0; + let pending_requests_len = pending_requests.len(); let deadline = sp_io::offchain::timestamp() .add(rt_offchain::Duration::from_millis(FETCH_TIMEOUT_PERIOD)); - PendingRequest::try_wait_all(pending_requests, Some(deadline)) + let parsed_evm_responses = PendingRequest::try_wait_all(pending_requests, Some(deadline)) .into_iter() - .enumerate() - .filter_map(|(index, pending_request)| { - let url = pending_requests_metadata.get(index) - .map(|(_, url)| url.clone()) - .unwrap_or_default(); - + .filter_map(|pending_request| { // handle request-level errors (transport/connection failures) let request_result = match pending_request { Ok(request) => request, - Err(err) => { - log::info!(target: LOG_TARGET, "👻 Request skipped; request to \"{}\" failed: {:?}", url, err); + Err(_) => { + requests_failed += 1; return None; } }; @@ -1099,20 +1105,32 @@ impl Pallet { // handle response-level errors (HTTP/protocol errors) let response = match request_result { Ok(response) => response, - Err(err) => { - log::info!(target: LOG_TARGET, "👻 Response skipped from \"{}\" error: {:?}", url, err); + Err(_) => { + responses_failed += 1; return None; } }; if response.code != 200 { - log::info!(target: LOG_TARGET, "👻 Response skipped from \"{}\": status {}", url, response.code); + responses_non_200 += 1; return None; } Some(response.body().collect::>()) }) - .collect() + .collect::>>(); + + log::info!( + target: LOG_TARGET, + "👻 Fetched {} of {}: {} failed request, {} failed responses, {} non 200 code", + pending_requests_len, + parsed_evm_responses.len(), + requests_failed, + responses_failed, + responses_non_200, + ); + + parsed_evm_responses } fn prepare_evm_request_body_for_latest_block(network_data: &NetworkData) -> Vec { diff --git a/pallets/slow-clap/src/tests.rs b/pallets/slow-clap/src/tests.rs index 4a57da5..7360b3e 100644 --- a/pallets/slow-clap/src/tests.rs +++ b/pallets/slow-clap/src/tests.rs @@ -3,6 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use super::*; +use crate::evm_types::Log; use crate::mock::*; use frame_support::{assert_err, assert_ok, dispatch}; @@ -293,6 +294,162 @@ fn should_make_http_call_and_parse_logs() { }); } +#[test] +fn should_prepare_correct_evm_request_for_latest_block() { + let (offchain, state) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + evm_block_response(&mut state.write()); + + let _: Result<(), OffchainErr> = t.execute_with(|| { + let rpc_endpoints = get_rpc_endpoints(); + let network_data = prepare_evm_network(Some(1), None); + let request_body = SlowClap::prepare_evm_request_body_for_latest_block(&network_data); + + let pending_requests = + SlowClap::prepare_pending_evm_requests(&rpc_endpoints, &request_body)?; + + assert_eq!(pending_requests.len(), rpc_endpoints.len()); + for (i, pending_metadata) in pending_requests.iter().enumerate() { + assert_eq!(pending_metadata.id.0, i as u16); + } + Ok(()) + }); +} + +#[test] +fn should_prepare_correct_evm_request_for_latest_transfers() { + let (offchain, state) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + evm_logs_response(&mut state.write()); + + let _: Result<(), OffchainErr> = t.execute_with(|| { + let from_block: u64 = 20335770; + let to_block: u64 = 20335858; + let rpc_endpoints = get_rpc_endpoints(); + + let network_data = prepare_evm_network(Some(1), None); + let request_body = SlowClap::prepare_evm_request_body_for_latest_transfers( + from_block, + to_block, + &network_data, + ); + + let pending_requests = + SlowClap::prepare_pending_evm_requests(&rpc_endpoints, &request_body)?; + + assert_eq!(pending_requests.len(), rpc_endpoints.len()); + for (i, pending_metadata) in pending_requests.iter().enumerate() { + assert_eq!(pending_metadata.id.0, i as u16); + } + Ok(()) + }); +} + +#[test] +fn should_throw_error_on_empty_prepared_requests() { + let (offchain, _) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + let _: Result<(), OffchainErr> = t.execute_with(|| { + let rpc_endpoints = vec![]; + let network_data = prepare_evm_network(Some(1), None); + let request_body = SlowClap::prepare_evm_request_body_for_latest_block(&network_data); + + assert_err!( + SlowClap::prepare_pending_evm_requests(&rpc_endpoints, &request_body), + OffchainErr::NoRequestsSent, + ); + + Ok(()) + }); +} + +#[test] +fn should_ignore_unparsable_rpc_ednpoint() { + let (offchain, _) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + let _: Result<(), OffchainErr> = t.execute_with(|| { + let rpc_endpoints = vec![get_rpc_endpoint(), vec![0xFF]]; + let network_data = prepare_evm_network(Some(1), None); + let request_body = SlowClap::prepare_evm_request_body_for_latest_block(&network_data); + + let pending_empty_requests = + SlowClap::prepare_pending_evm_requests(&rpc_endpoints, &request_body)?; + + assert_eq!(pending_empty_requests.len(), 1); + + Ok(()) + }); +} + +#[test] +fn should_fetch_blocks_correctly() { + let (offchain, state) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + evm_block_response(&mut state.write()); + + let _: Result<(), OffchainErr> = t.execute_with(|| { + let rpc_endpoints = get_rpc_endpoints(); + let network_data = prepare_evm_network(Some(1), None); + let request_body = SlowClap::prepare_evm_request_body_for_latest_block(&network_data); + + let pending_requests = + SlowClap::prepare_pending_evm_requests(&rpc_endpoints, &request_body)?; + let fetched_blocks_raw = SlowClap::fetch_multiple_evm_from_remote(pending_requests); + + assert_eq!(fetched_blocks_raw.len(), 2); + + Ok(()) + }); +} + +#[test] +fn should_parse_blocks_correctly() { + let (offchain, state) = TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + evm_block_response(&mut state.write()); + + let _: Result<(), OffchainErr> = t.execute_with(|| { + let rpc_endpoints = get_rpc_endpoints(); + let network_data = prepare_evm_network(Some(1), None); + let request_body = SlowClap::prepare_evm_request_body_for_latest_block(&network_data); + + let pending_requests_metadata = + SlowClap::prepare_pending_evm_requests(&rpc_endpoints, &request_body)?; + let parsed_evm_responses = + SlowClap::fetch_multiple_evm_from_remote(pending_requests_metadata) + .iter() + .filter_map(|response_bytes| { + let parsed_evm_response = SlowClap::parse_evm_response(response_bytes).ok()?; + Some(parsed_evm_response) + }) + .collect::>(); + + assert_eq!(parsed_evm_responses.len(), 2); + assert_eq!( + parsed_evm_responses[0], + EvmResponseType::BlockNumber(20335745) + ); + assert_eq!( + parsed_evm_responses[1], + EvmResponseType::BlockNumber(20335745) + ); + + Ok(()) + }); +} + #[test] fn should_emit_black_swan_if_not_enough_authorities_left() { let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); @@ -1363,11 +1520,135 @@ fn should_split_commit_slash_between_active_validators() { }); } -// TODO: add tests -// 1. prepare_pending_evm_requests fails fully and partially -// 2. fetch_multiple_evm_from_remote when they give different results -// 3. check_evm_responses_correctness -// 4. get_balanced_evm_response +#[test] +fn should_check_responses_correctly() { + new_test_ext().execute_with(|| { + let empty_responses = vec![]; + assert_err!( + SlowClap::check_evm_responses_correctness(&empty_responses), + OffchainErr::EmptyResponses, + ); + + let different_responses_types = vec![ + EvmResponseType::BlockNumber(60), + EvmResponseType::BlockNumber(420), + EvmResponseType::TransactionLogs(Default::default()), + EvmResponseType::BlockNumber(1337), + ]; + assert_err!( + SlowClap::check_evm_responses_correctness(&different_responses_types), + OffchainErr::DifferentEvmResponseTypes, + ); + + let correct_block_responses = vec![ + EvmResponseType::BlockNumber(69), + EvmResponseType::BlockNumber(420), + EvmResponseType::BlockNumber(1337), + EvmResponseType::BlockNumber(20335745), + ]; + assert_ok!(SlowClap::check_evm_responses_correctness( + &correct_block_responses + )); + + let correct_log_responses = vec![ + EvmResponseType::TransactionLogs(Default::default()), + EvmResponseType::TransactionLogs(Default::default()), + EvmResponseType::TransactionLogs(Default::default()), + EvmResponseType::TransactionLogs(Default::default()), + ]; + assert_ok!(SlowClap::check_evm_responses_correctness( + &correct_log_responses + )); + }); +} + +#[test] +fn should_get_balanced_responses_correctly() { + new_test_ext().execute_with(|| { + let empty_responses = vec![]; + assert_err!( + SlowClap::get_balanced_evm_response(&empty_responses), + OffchainErr::EmptyResponses, + ); + + let correct_block_responses = vec![ + EvmResponseType::BlockNumber(69), + EvmResponseType::BlockNumber(420), + EvmResponseType::BlockNumber(422), + EvmResponseType::BlockNumber(1337), + ]; + let median_block = SlowClap::get_balanced_evm_response(&correct_block_responses) + .expect("median block should be extractable; qed"); + assert_eq!(median_block, EvmResponseType::BlockNumber(421)); + + let first_correct_log = Log { + transaction_hash: Some(H256::random()), + block_number: Some(69), + topics: vec![], + removed: false, + }; + let second_correct_log = Log { + transaction_hash: Some(H256::random()), + block_number: Some(420), + topics: vec![], + removed: false, + }; + let first_wrong_block_log = Log { + block_number: Some(1338), + ..first_correct_log.clone() + }; + let first_wrong_transaction_log = Log { + transaction_hash: Some(H256::zero()), + ..first_correct_log.clone() + }; + let second_wrong_block_log = Log { + block_number: Some(1338), + ..second_correct_log.clone() + }; + let second_wrong_transaction_log = Log { + transaction_hash: Some(H256::zero()), + ..second_correct_log.clone() + }; + + let correct_log_responses = vec![ + EvmResponseType::TransactionLogs(vec![ + first_correct_log.clone(), + second_correct_log.clone(), + ]), + EvmResponseType::TransactionLogs(vec![ + first_correct_log.clone(), + second_correct_log.clone(), + ]), + EvmResponseType::TransactionLogs(vec![ + first_correct_log.clone(), + second_wrong_block_log.clone(), + ]), + EvmResponseType::TransactionLogs(vec![ + first_correct_log.clone(), + second_wrong_transaction_log.clone(), + ]), + EvmResponseType::TransactionLogs(vec![ + first_wrong_block_log.clone(), + second_correct_log.clone(), + ]), + EvmResponseType::TransactionLogs(vec![ + first_wrong_transaction_log.clone(), + second_correct_log.clone(), + ]), + EvmResponseType::TransactionLogs(vec![first_wrong_block_log, second_wrong_block_log]), + EvmResponseType::TransactionLogs(vec![ + first_wrong_transaction_log, + second_wrong_transaction_log, + ]), + ]; + let best_logs = SlowClap::get_balanced_evm_response(&correct_log_responses) + .expect("best logs should be extractable; qed"); + assert_eq!( + best_logs, + EvmResponseType::TransactionLogs(vec![first_correct_log, second_correct_log]) + ); + }); +} fn assert_clapped_amount( session_index: &SessionIndex,