tests added for new offchain worker functionality

Signed-off-by: Uncle Stinky <uncle.stinky@ghostchain.io>
This commit is contained in:
Uncle Stinky 2026-02-01 15:08:03 +03:00
parent b282ebad62
commit eb9fc16b43
Signed by: st1nky
GPG Key ID: 016064BD97603B40
4 changed files with 336 additions and 37 deletions

2
Cargo.lock generated
View File

@ -3837,7 +3837,7 @@ dependencies = [
[[package]] [[package]]
name = "ghost-slow-clap" name = "ghost-slow-clap"
version = "0.4.11" version = "0.4.12"
dependencies = [ dependencies = [
"frame-benchmarking", "frame-benchmarking",
"frame-support", "frame-support",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ghost-slow-clap" name = "ghost-slow-clap"
version = "0.4.11" version = "0.4.12"
description = "Applause protocol for the EVM bridge" description = "Applause protocol for the EVM bridge"
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true

View File

@ -881,10 +881,10 @@ impl<T: Config> Pallet<T> {
Self::prepare_evm_request_body_for_latest_block(network_data) 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)?; Self::prepare_pending_evm_requests(&rpc_endpoints, &request_body)?;
let parsed_evm_responses = let parsed_evm_responses =
Self::fetch_multiple_evm_from_remote(pending_requests_metadata) Self::fetch_multiple_evm_from_remote(pending_requests)
.iter() .iter()
.filter_map(|response_bytes| { .filter_map(|response_bytes| {
let parsed_evm_response = let parsed_evm_response =
@ -965,8 +965,8 @@ impl<T: Config> Pallet<T> {
fn prepare_pending_evm_requests( fn prepare_pending_evm_requests(
rpc_endpoints: &Vec<Vec<u8>>, rpc_endpoints: &Vec<Vec<u8>>,
request_body: &[u8], request_body: &[u8],
) -> OffchainResult<T, Vec<(PendingRequest, String)>> { ) -> OffchainResult<T, Vec<PendingRequest>> {
let mut pending_requests_metadata = Vec::new(); let mut pending_requests = Vec::new();
let request_body_str = core::str::from_utf8(request_body) let request_body_str = core::str::from_utf8(request_body)
.map_err(|_| OffchainErr::UnparsableRequestBody(request_body.to_vec()))?; .map_err(|_| OffchainErr::UnparsableRequestBody(request_body.to_vec()))?;
@ -977,7 +977,11 @@ impl<T: Config> Pallet<T> {
let rpc_endpoint_str = match core::str::from_utf8(rpc_endpoint) { let rpc_endpoint_str = match core::str::from_utf8(rpc_endpoint) {
Ok(rpc_endpoint_str) => rpc_endpoint_str, Ok(rpc_endpoint_str) => rpc_endpoint_str,
Err(_) => { 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; continue;
} }
}; };
@ -989,21 +993,30 @@ impl<T: Config> Pallet<T> {
.send() .send()
{ {
Ok(pending) => { Ok(pending) => {
pending_requests_metadata.push((pending, rpc_endpoint_str.to_string())) pending_requests.push(pending)
} }
Err(_) => { 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); 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( fn get_balanced_evm_response(
@ -1067,31 +1080,24 @@ impl<T: Config> Pallet<T> {
} }
fn fetch_multiple_evm_from_remote( fn fetch_multiple_evm_from_remote(
pending_requests_metadata: Vec<(PendingRequest, String)>, pending_requests: Vec<PendingRequest>,
) -> Vec<Vec<u8>> { ) -> Vec<Vec<u8>> {
let pending_requests = pending_requests_metadata let mut requests_failed = 0;
.iter() let mut responses_failed = 0;
.map(|(pending_request, _)| PendingRequest { let mut responses_non_200 = 0;
id: pending_request.id,
})
.collect::<Vec<PendingRequest>>();
let pending_requests_len = pending_requests.len();
let deadline = sp_io::offchain::timestamp() let deadline = sp_io::offchain::timestamp()
.add(rt_offchain::Duration::from_millis(FETCH_TIMEOUT_PERIOD)); .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() .into_iter()
.enumerate() .filter_map(|pending_request| {
.filter_map(|(index, pending_request)| {
let url = pending_requests_metadata.get(index)
.map(|(_, url)| url.clone())
.unwrap_or_default();
// handle request-level errors (transport/connection failures) // handle request-level errors (transport/connection failures)
let request_result = match pending_request { let request_result = match pending_request {
Ok(request) => request, Ok(request) => request,
Err(err) => { Err(_) => {
log::info!(target: LOG_TARGET, "👻 Request skipped; request to \"{}\" failed: {:?}", url, err); requests_failed += 1;
return None; return None;
} }
}; };
@ -1099,20 +1105,32 @@ impl<T: Config> Pallet<T> {
// handle response-level errors (HTTP/protocol errors) // handle response-level errors (HTTP/protocol errors)
let response = match request_result { let response = match request_result {
Ok(response) => response, Ok(response) => response,
Err(err) => { Err(_) => {
log::info!(target: LOG_TARGET, "👻 Response skipped from \"{}\" error: {:?}", url, err); responses_failed += 1;
return None; return None;
} }
}; };
if response.code != 200 { if response.code != 200 {
log::info!(target: LOG_TARGET, "👻 Response skipped from \"{}\": status {}", url, response.code); responses_non_200 += 1;
return None; return None;
} }
Some(response.body().collect::<Vec<u8>>()) Some(response.body().collect::<Vec<u8>>())
}) })
.collect() .collect::<Vec<Vec<u8>>>();
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<u8> { fn prepare_evm_request_body_for_latest_block(network_data: &NetworkData) -> Vec<u8> {

View File

@ -3,6 +3,7 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use super::*; use super::*;
use crate::evm_types::Log;
use crate::mock::*; use crate::mock::*;
use frame_support::{assert_err, assert_ok, dispatch}; 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<u32>> = 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<u32>> = 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<u32>> = 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<u32>> = 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<u32>> = 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<u32>> = 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::<Vec<EvmResponseType>>();
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] #[test]
fn should_emit_black_swan_if_not_enough_authorities_left() { fn should_emit_black_swan_if_not_enough_authorities_left() {
let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); 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 #[test]
// 1. prepare_pending_evm_requests fails fully and partially fn should_check_responses_correctly() {
// 2. fetch_multiple_evm_from_remote when they give different results new_test_ext().execute_with(|| {
// 3. check_evm_responses_correctness let empty_responses = vec![];
// 4. get_balanced_evm_response 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( fn assert_clapped_amount(
session_index: &SessionIndex, session_index: &SessionIndex,