ghost-node/pallets/weaver/src/tests.rs
Uncle Stretch d83ee59508
let the EXODUS begin...
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-08-29 14:48:50 +03:00

1380 lines
54 KiB
Rust

#![cfg(test)]
use crate::sr25519::AuthorityPair;
use crate::mock::*;
use crate::pallet::*;
use crate::types::*;
use frame_support::traits::Hooks;
use ghost_helpers::networks::{NetworkDataBuilder, NetworkType};
use codec::Decode;
use sp_runtime::codec::Encode;
use sp_core::{H256, crypto::Pair};
use sp_runtime::{
traits::{Dispatchable, ValidateUnsigned},
transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError},
};
use sp_core::offchain::{
testing::{PendingRequest, TestOffchainExt, TestTransactionPoolExt},
OffchainDbExt, OffchainWorkerExt, TransactionPoolExt,
};
use parking_lot::RwLock;
use sp_core::offchain::testing::PoolState;
use std::sync::Arc;
fn create_block_numbers(values: &[u64]) -> Vec<crate::ExternalBlockNumber> {
values.iter().map(|&v| v.into()).collect()
}
fn apply_and_clear_pool_transaction(pool_state: &Arc<RwLock<PoolState>>, tx_index: usize) {
let raw_extrinsic = {
let state = pool_state.read();
assert!(
state.transactions.len() > tx_index,
"No transaction found at index {}",
tx_index
);
state.transactions[tx_index].clone()
};
let ext_decoded =
Extrinsic::decode(&mut &raw_extrinsic[..]).expect("Failed to decode extrinsic from pool");
let runtime_call = ext_decoded.call;
let RuntimeCall::GhostWeaver(ref local_call) = runtime_call else {
panic!("Expected a transaction for GhostWeaver pallet, but found another one");
};
let validity = GhostWeaver::validate_unsigned(TransactionSource::External, local_call);
assert!(validity.is_ok());
let dispatch_result = runtime_call.dispatch(RuntimeOrigin::none());
assert!(dispatch_result.is_ok());
}
#[test]
fn should_successfully_compute_median_and_start_weaving_round() {
let pair1 = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
let pair2 = AuthorityPair::from_string("//Bob", None).expect("Should be valid seed");
let pair3 = AuthorityPair::from_string("//Carol", None).expect("Should be valid seed");
let network_id = 1;
let network_data = NetworkDataBuilder::default().build();
let authorities = vec![pair1.clone(), pair2.clone(), pair3.clone()];
let networks = vec![(network_id, network_data, Default::default())];
new_test_ext(authorities, networks).execute_with(|| {
System::set_block_number(2);
let context = AttestationContext::default()
.with_current_block(1)
.with_network_id(network_id);
let att1 = context.with_authority_index(0).build_block_attestation(100);
let att2 = context.with_authority_index(1).build_block_attestation(102);
let att3 = context.with_authority_index(2).build_block_attestation(101);
let sig1 = pair1.sign(&att1.encode());
let sig2 = pair2.sign(&att2.encode());
let sig3 = pair3.sign(&att3.encode());
assert!(GhostWeaver::extend_warp(RuntimeOrigin::none(), att1, sig1).is_ok());
assert!(GhostWeaver::extend_warp(RuntimeOrigin::none(), att2, sig2).is_ok());
assert!(GhostWeaver::extend_warp(RuntimeOrigin::none(), att3, sig3).is_ok());
assert!(GhostWeaver::tapestry_drafts(network_id).is_none());
System::set_block_number(3);
GhostWeaver::on_initialize(3);
let round = GhostWeaver::tapestry_drafts(network_id);
assert!(round.is_some());
assert_eq!(round.unwrap().until, 3 + WeavingDelay::get());
});
}
#[test]
fn should_apply_cooldown_delay_on_consensus_failure() {
let pair1 = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
let pair2 = AuthorityPair::from_string("//Bob", None).expect("Should be valid seed");
let pair3 = AuthorityPair::from_string("//Carol", None).expect("Should be valid seed");
let network_id = 1;
let network_data = NetworkDataBuilder::default()
.with_block_deviation(5)
.build();
let authorities = vec![pair1.clone(), pair2.clone(), pair3.clone()];
let networks = vec![(network_id, network_data, Default::default())];
new_test_ext(authorities, networks).execute_with(|| {
System::set_block_number(10);
let context = AttestationContext::default()
.with_current_block(9)
.with_network_id(network_id);
let att1 = context.with_authority_index(0).build_block_attestation(100);
let att2 = context.with_authority_index(1).build_block_attestation(500);
let att3 = context.with_authority_index(2).build_block_attestation(900);
let sig1 = pair1.sign(&att1.encode());
let sig2 = pair2.sign(&att2.encode());
let sig3 = pair3.sign(&att3.encode());
assert!(GhostWeaver::extend_warp(RuntimeOrigin::none(), att1, sig1).is_ok());
assert!(GhostWeaver::extend_warp(RuntimeOrigin::none(), att2, sig2).is_ok());
assert!(GhostWeaver::extend_warp(RuntimeOrigin::none(), att3, sig3).is_ok());
System::set_block_number(11);
GhostWeaver::on_initialize(11);
let cooldown_block = GhostWeaver::next_consensus_attempt_blocks(network_id);
assert_eq!(cooldown_block, 14);
TapestryDrafts::<TestRuntime>::remove(network_id);
System::set_block_number(12);
GhostWeaver::on_initialize(12);
assert!(TapestryDrafts::<TestRuntime>::get(network_id).is_none());
System::set_block_number(14);
GhostWeaver::on_initialize(14);
assert!(TapestryDrafts::<TestRuntime>::get(network_id).is_some());
});
}
#[test]
fn validate_unsigned_should_prevent_mempool_dos_via_tags() {
let pair = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
let wrong_pair = AuthorityPair::from_string("//Eve", None).expect("Should be valid seed");
let network_id = 1;
let network_data = NetworkDataBuilder::default().build();
let authorities = vec![pair.clone()];
let networks = vec![(network_id, network_data, Default::default())];
new_test_ext(authorities, networks).execute_with(|| {
System::set_block_number(2);
let att = AttestationContext::default()
.with_current_block(1)
.with_network_id(network_id)
.with_authority_index(0)
.build_block_attestation(100);
let valid_sig = pair.sign(&att.encode());
let invalid_sig = wrong_pair.sign(&att.encode());
let call = Call::extend_warp {
block_attestation: att.clone(),
signature: valid_sig,
};
let validity = GhostWeaver::validate_unsigned(TransactionSource::External, &call);
assert!(validity.is_ok());
let valid_info = validity.unwrap();
let auth_index: crate::AuthIndex = 0u8.into();
let weaver_context_bytes: Vec<u8> = (auth_index, network_id).encode();
let mut expected_tag = "WeaverBlock".encode();
expected_tag.extend(weaver_context_bytes.encode());
assert!(valid_info.provides.contains(&expected_tag));
let spam_call = Call::extend_warp {
block_attestation: att,
signature: invalid_sig,
};
let spam_validity = GhostWeaver::validate_unsigned(TransactionSource::External, &spam_call);
assert_eq!(
spam_validity,
Err(TransactionValidityError::Invalid(InvalidTransaction::BadSigner).into())
);
});
}
#[test]
fn offchain_worker_should_fetch_block_and_submit_unsigned_tx() {
let evm_network_id = 1u32;
let utxo_network_id = 69u32;
let pair = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
let new_evm_network_data = NetworkDataBuilder::default()
.with_network_type(NetworkType::Evm)
.with_default_endpoints(vec![b"https://super-bad-public-rpc-endpoint.com".to_vec()])
.build();
let new_utxo_network_data = NetworkDataBuilder::default()
.with_network_type(NetworkType::Utxo)
.with_default_endpoints(vec![b"https://publicbitcoin.io".to_vec()])
.build();
let authority_index = 0 as crate::AuthIndex;
let authorities = vec![pair.clone()];
let networks = vec![
(evm_network_id, new_evm_network_data, 0u64),
(utxo_network_id, new_utxo_network_data, 0u64),
];
let mut ext = new_test_ext(authorities, networks);
let (offchain, offchain_state) = TestOffchainExt::new();
let (pool, pool_state) = TestTransactionPoolExt::new();
ext.register_extension(OffchainWorkerExt::new(offchain.clone()));
ext.register_extension(OffchainDbExt::new(offchain));
ext.register_extension(TransactionPoolExt::new(pool));
ext.execute_with(|| {
let network_id_encoded = evm_network_id.encode();
let mut endpoint_key = b"ghost-weaver::".to_vec();
endpoint_key.extend_from_slice(b"endpoint-");
endpoint_key.extend_from_slice(&network_id_encoded);
let mut distance_key = b"ghost-weaver::".to_vec();
distance_key.extend_from_slice(b"distance-");
distance_key.extend_from_slice(&network_id_encoded);
let target_endpoint = b"http://localhost:8545".to_vec();
let rpc_endpoints: Vec<Vec<u8>> = vec![target_endpoint.clone()];
sp_io::offchain::local_storage_set(
sp_core::offchain::StorageKind::PERSISTENT,
&endpoint_key,
&rpc_endpoints.encode(),
);
let custom_headers: Vec<(Vec<u8>, Vec<u8>)> = vec![
(b"X-Custom-Provider".to_vec(), b"GhostWeaverNode".to_vec()),
(
b"Authorization".to_vec(),
b"Bearer new-api-token-123".to_vec(),
),
];
sp_io::offchain::local_storage_set(
sp_core::offchain::StorageKind::PERSISTENT,
&target_endpoint,
&custom_headers.encode(),
);
sp_io::crypto::sr25519_generate(
sp_core::crypto::KeyTypeId(*b"weav"),
Some(b"//Alice".to_vec()),
);
});
{
let mut state = offchain_state.write();
state.expect_request(PendingRequest {
method: "POST".into(),
uri: "https://publicbitcoin.io".into(),
headers: vec![
("Accept".into(), "application/json".into()),
("Content-Type".into(), "application/json".into()),
],
body: b"{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"getblockcount\"}".to_vec(),
response: Some(b"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":960127}".to_vec()),
sent: true,
..Default::default()
});
state.expect_request(PendingRequest {
method: "POST".into(),
uri: "http://localhost:8545".into(),
headers: vec![
("Accept".into(), "application/json".into()),
("Content-Type".into(), "application/json".into()),
("X-Custom-Provider".into(), "GhostWeaverNode".into()),
("Authorization".into(), "Bearer new-api-token-123".into()),
],
body: b"{\"id\":2,\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\"}".to_vec(),
response: Some(b"{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":\"0x1873a84\"}".to_vec()),
sent: true,
..Default::default()
});
}
ext.execute_with(|| {
System::set_block_number(1u64);
GhostWeaver::offchain_worker(1u64);
System::set_block_number(2u64);
let state = pool_state.read();
assert_eq!(state.transactions.len(), 1);
let prev_utxo_block = NetworkAttestations::<TestRuntime>::get(utxo_network_id)
.get(authority_index as usize)
.and_then(|opt| *opt)
.unwrap_or(0);
apply_and_clear_pool_transaction(&pool_state, 0);
let curr_utxo_block = NetworkAttestations::<TestRuntime>::get(utxo_network_id)
.get(authority_index as usize)
.and_then(|opt| *opt)
.unwrap_or(0);
assert_eq!(prev_utxo_block, 0);
assert_eq!(curr_utxo_block, 960127);
assert!(TapestryDrafts::<TestRuntime>::get(&utxo_network_id).is_none());
GhostWeaver::on_initialize(3u64);
assert!(TapestryDrafts::<TestRuntime>::get(&utxo_network_id).is_some());
});
ext.execute_with(|| {
System::set_block_number(2u64);
GhostWeaver::offchain_worker(2u64);
System::set_block_number(3u64);
let state = pool_state.read();
assert_eq!(state.transactions.len(), 2);
let prev_evm_block = NetworkAttestations::<TestRuntime>::get(evm_network_id)
.get(authority_index as usize)
.and_then(|opt| *opt)
.unwrap_or(0);
apply_and_clear_pool_transaction(&pool_state, 1);
let curr_evm_block = NetworkAttestations::<TestRuntime>::get(evm_network_id)
.get(authority_index as usize)
.and_then(|opt| *opt)
.unwrap_or(0);
assert_eq!(prev_evm_block, 0);
assert_eq!(curr_evm_block, 25639556);
assert!(TapestryDrafts::<TestRuntime>::get(&evm_network_id).is_none());
GhostWeaver::on_initialize(4u64);
assert!(TapestryDrafts::<TestRuntime>::get(&evm_network_id).is_some());
});
}
#[test]
fn offchain_worker_should_fetch_hash_and_submit_unsigned_tx() {
let evm_network_id = 1u32;
let utxo_network_id = 69u32;
let pair = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
let new_evm_network_data = NetworkDataBuilder::default()
.with_network_type(NetworkType::Evm)
.with_gatekeeper(
hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
.expect("Invalid public key"),
)
.with_default_endpoints(vec![b"https://super-bad-public-rpc-endpoint.com".to_vec()])
.build();
let new_utxo_network_data = NetworkDataBuilder::default()
.with_network_type(NetworkType::Utxo)
.with_default_endpoints(vec![b"https://publicbitcoin.io".to_vec()])
.build();
let evm_session = 69;
let utxo_session = 420;
let authorities = vec![pair.clone()];
let networks = vec![
(evm_network_id, new_evm_network_data, 0u64),
(utxo_network_id, new_utxo_network_data, 0u64),
];
let mut ext = new_test_ext(authorities, networks);
let (offchain, offchain_state) = TestOffchainExt::new();
let (pool, pool_state) = TestTransactionPoolExt::new();
ext.register_extension(OffchainWorkerExt::new(offchain.clone()));
ext.register_extension(OffchainDbExt::new(offchain));
ext.register_extension(TransactionPoolExt::new(pool));
ext.execute_with(|| {
let tapestry_draft = TapestryDraftBuilder::<crate::ExternalBlockNumber, u32>::default();
let evm_tapestry_draft = tapestry_draft.with_median_external(25639556).build();
let utxo_tapestry_draft = tapestry_draft.with_median_external(960129).build();
CurrentWeavingSession::<TestRuntime>::insert(&evm_network_id, evm_session);
CurrentWeavingSession::<TestRuntime>::insert(&utxo_network_id, utxo_session);
TapestryDrafts::<TestRuntime>::insert(&evm_network_id, evm_tapestry_draft);
TapestryDrafts::<TestRuntime>::insert(&utxo_network_id, utxo_tapestry_draft);
let network_id_encoded = evm_network_id.encode();
let mut endpoint_key = b"ghost-weaver::".to_vec();
endpoint_key.extend_from_slice(b"endpoint-");
endpoint_key.extend_from_slice(&network_id_encoded);
let mut distance_key = b"ghost-weaver::".to_vec();
distance_key.extend_from_slice(b"distance-");
distance_key.extend_from_slice(&network_id_encoded);
let target_endpoint = b"http://localhost:8545".to_vec();
let rpc_endpoints: Vec<Vec<u8>> = vec![target_endpoint.clone()];
sp_io::offchain::local_storage_set(
sp_core::offchain::StorageKind::PERSISTENT,
&endpoint_key,
&rpc_endpoints.encode(),
);
let custom_headers: Vec<(Vec<u8>, Vec<u8>)> = vec![
(b"X-Custom-Provider".to_vec(), b"GhostWeaverNode".to_vec()),
(
b"Authorization".to_vec(),
b"Bearer new-api-token-123".to_vec(),
),
];
sp_io::offchain::local_storage_set(
sp_core::offchain::StorageKind::PERSISTENT,
&target_endpoint,
&custom_headers.encode(),
);
sp_io::crypto::sr25519_generate(
sp_core::crypto::KeyTypeId(*b"weav"),
Some(b"//Alice".to_vec()),
);
});
{
let mut state = offchain_state.write();
state.expect_request(PendingRequest {
method: "POST".into(),
uri: "https://publicbitcoin.io".into(),
headers: vec![
("Accept".into(), "application/json".into()),
("Content-Type".into(), "application/json".into()),
],
body: b"{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"getblockhash\",\"params\":[960129]}".to_vec(),
response: Some(b"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0000000000000000000091c72779c83d76dfa8149e950f3a5287377a9708d5c1\"}".to_vec()),
sent: true,
..Default::default()
});
state.expect_request(PendingRequest {
method: "POST".into(),
uri: "http://localhost:8545".into(),
headers: vec![
("Accept".into(), "application/json".into()),
("Content-Type".into(), "application/json".into()),
("X-Custom-Provider".into(), "GhostWeaverNode".into()),
("Authorization".into(), "Bearer new-api-token-123".into()),
],
// Match the EXACT field order: data first, then to
body: b"{\"id\":2,\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"data\":\"0x00000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000001873a84\",\"to\":\"0x02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9\"},\"latest\"]}".to_vec(),
response: Some(b"{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":\"0xf72b9ef0b66c8cc3952ffd11dc0b85f3b02e6edfa9236da400138f2fcba4668d0000000000000000000000000000000000000000000000000000000000000046\"}".to_vec()),
sent: true,
..Default::default()
});
}
ext.execute_with(|| {
System::set_block_number(1u64);
GhostWeaver::offchain_worker(1u64);
System::set_block_number(2u64);
let state = pool_state.read();
assert_eq!(state.transactions.len(), 1);
let prev_weaving_state = WeavingStates::<TestRuntime>::get(utxo_network_id);
apply_and_clear_pool_transaction(&pool_state, 0);
let curr_weaving_state = WeavingStates::<TestRuntime>::get(utxo_network_id);
assert_eq!(prev_weaving_state.count, 0);
assert_eq!(prev_weaving_state.next_session, 0);
assert_eq!(prev_weaving_state.hash, H256::default());
assert_eq!(curr_weaving_state.count, 1);
assert_eq!(curr_weaving_state.next_session, crate::WeavingSession::MAX);
let bytes = hex::decode("0000000000000000000091c72779c83d76dfa8149e950f3a5287377a9708d5c1")
.expect("UTXO block hash should be valid");
let block_hash = H256::from_slice(&bytes);
assert_eq!(curr_weaving_state.hash, block_hash);
let prev_state = LoomStates::<TestRuntime>::get(&utxo_network_id, &utxo_session);
GhostWeaver::on_initialize(3u64);
let curr_state = LoomStates::<TestRuntime>::get(&utxo_network_id, &utxo_session);
assert_eq!(prev_state, H256::default());
assert_eq!(curr_state, block_hash);
});
ext.execute_with(|| {
System::set_block_number(2u64);
GhostWeaver::offchain_worker(2u64);
System::set_block_number(3u64);
let state = pool_state.read();
assert_eq!(state.transactions.len(), 2);
let prev_weaving_state = WeavingStates::<TestRuntime>::get(evm_network_id);
apply_and_clear_pool_transaction(&pool_state, 1);
let curr_weaving_state = WeavingStates::<TestRuntime>::get(evm_network_id);
assert_eq!(prev_weaving_state.count, 0);
assert_eq!(prev_weaving_state.next_session, 0);
assert_eq!(prev_weaving_state.hash, H256::default());
assert_eq!(curr_weaving_state.count, 1);
assert_eq!(curr_weaving_state.next_session, evm_session + 1);
let bytes = hex::decode("f72b9ef0b66c8cc3952ffd11dc0b85f3b02e6edfa9236da400138f2fcba4668d")
.expect("UTXO block hash should be valid");
let block_hash = H256::from_slice(&bytes);
assert_eq!(curr_weaving_state.hash, block_hash);
let prev_state = LoomStates::<TestRuntime>::get(&evm_network_id, &evm_session);
GhostWeaver::on_initialize(4u64);
let curr_state = LoomStates::<TestRuntime>::get(&evm_network_id, &evm_session);
assert_eq!(prev_state, H256::default());
assert_eq!(curr_state, block_hash);
});
}
#[test]
fn should_successfully_pull_evm_thread_and_mint_currency() {
let pair = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
let network_id = 1;
let network_data = NetworkDataBuilder::default()
.with_network_type(NetworkType::Evm)
.build();
let authorities = vec![pair.clone()];
let networks = vec![(network_id, network_data, Default::default())];
// Contract address: 0x9a52C28dA655b69d8ddc430d6ebD04630BA0e845
//
// three inserts
// 69 sfJ9hc4A7r578XM35nd48B3FWcd33XjpVnAbGC5tvtwthMWdU - 0xc477f0a42fdd861485bd854c7a5851614ea163bd295f7844e1c5493d662fdc4e
// 420 sfDxNicCEPts6CVttVMPCs2eoymDpQahu1GUVyGPWu6dhwADJ - 0x0ae3ba32f8555fe3031461e2cb6feeadb67029ef7490b982824fc90751a4cb3e
// 1337 sfH6zJ63fcxrFAZ1RmiHbxgy2t3sEufY26WdsBSS359AzJd4w - 0x962a2794f66c89e55d9b9692ce59f67345b99f6fddb51d90462c7eca8b5a7e1b
// https://sepolia.etherscan.io/address/0x9cffbdbdf29c67c5dbab1b5e8ae897aeafcaf70c#readContract
// getRoot: atBlock 11383380, globalIndexes from 0 to 2
let inserts = vec![
(
0,
hex::decode("c477f0a42fdd861485bd854c7a5851614ea163bd295f7844e1c5493d662fdc4e")
.expect("Bytes32 from EVM should be valid"),
69u64,
vec![
H256::from_slice(
&hex::decode(
"c7a710c9acbc193692f34ac1f1979013b04240a4c0d02b49b9178f17721350b1",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"9e0f9c5b8ffa146b74bb614482c0bc65a1ef961a8fa9a3a031a36c97f8c70733",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"890740a8eb06ce9be422cb8da5cdafc2b58c0a5e24036c578de2a433c828ff7d",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"3b8ec09e026fdc305365dfc94e189a81b38c7597b3d941c279f042e8206e0bd8",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"ecd50eee38e386bd62be9bedb990706951b65fe053bd9d8a521af753d139e2da",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"defff6d330bb5403f63b14f33b578274160de3a50df4efecf0e0db73bcdd3da5",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"617bdd11f7c0a11f49db22f629387a12da7596f9d1704d7465177c63d88ec7d7",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"292c23a9aa1d8bea7e2435e555a4a60e379a5a35f3f452bae60121073fb6eead",
)
.expect("Proof should be valid"),
),
],
vec![H256::from_slice(
&hex::decode("350a78f78c810898b7c60ddba671543bfbe683ffb0ac393df0e132f607b5f738")
.expect("Slot should be valid hex"),
)],
),
(
1,
hex::decode("0ae3ba32f8555fe3031461e2cb6feeadb67029ef7490b982824fc90751a4cb3e")
.expect("Bytes32 from EVM should be valid"),
420u64,
vec![
H256::from_slice(
&hex::decode(
"6dfb19f049f557abff273ce9efb9195924d41e5699b1b7755be2c515b25ef7b6",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"9e0f9c5b8ffa146b74bb614482c0bc65a1ef961a8fa9a3a031a36c97f8c70733",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"890740a8eb06ce9be422cb8da5cdafc2b58c0a5e24036c578de2a433c828ff7d",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"3b8ec09e026fdc305365dfc94e189a81b38c7597b3d941c279f042e8206e0bd8",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"ecd50eee38e386bd62be9bedb990706951b65fe053bd9d8a521af753d139e2da",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"defff6d330bb5403f63b14f33b578274160de3a50df4efecf0e0db73bcdd3da5",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"617bdd11f7c0a11f49db22f629387a12da7596f9d1704d7465177c63d88ec7d7",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"292c23a9aa1d8bea7e2435e555a4a60e379a5a35f3f452bae60121073fb6eead",
)
.expect("Proof should be valid"),
),
],
vec![H256::from_slice(
&hex::decode("2e474dabf492786b3505d7b7cf0bf07c9dbce92c91982fa7344c77e24fd4310d")
.expect("Slot should be valid hex"),
)],
),
(
2,
hex::decode("962a2794f66c89e55d9b9692ce59f67345b99f6fddb51d90462c7eca8b5a7e1b")
.expect("Bytes32 from EVM should be valid"),
1337u64,
vec![
H256::from_slice(
&hex::decode(
"290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"82ca531d09faff937c9b68f53d473c9f2c88bdbc8b1629af894479367881db5c",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"890740a8eb06ce9be422cb8da5cdafc2b58c0a5e24036c578de2a433c828ff7d",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"3b8ec09e026fdc305365dfc94e189a81b38c7597b3d941c279f042e8206e0bd8",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"ecd50eee38e386bd62be9bedb990706951b65fe053bd9d8a521af753d139e2da",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"defff6d330bb5403f63b14f33b578274160de3a50df4efecf0e0db73bcdd3da5",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"617bdd11f7c0a11f49db22f629387a12da7596f9d1704d7465177c63d88ec7d7",
)
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode(
"292c23a9aa1d8bea7e2435e555a4a60e379a5a35f3f452bae60121073fb6eead",
)
.expect("Proof should be valid"),
),
],
vec![H256::from_slice(
&hex::decode("bde870fce92d701a7a5d6f63003e1cd2cb61c3266fc1be256844b8ca61d9c9ab")
.expect("Slot should be valid hex"),
)],
),
];
new_test_ext(authorities, networks).execute_with(|| {
// https://sepolia.etherscan.io/address/0x9cffbdbdf29c67c5dbab1b5e8ae897aeafcaf70c#readContract
// getRoot: session 0, atBlock 11383380
let session = 0;
let root_string =
hex::decode("f72b9ef0b66c8cc3952ffd11dc0b85f3b02e6edfa9236da400138f2fcba4668d")
.expect("Root hex should be valid");
let expected_root = H256::from_slice(&root_string);
LoomStates::<TestRuntime>::insert(&network_id, &session, expected_root);
for insert in inserts.into_iter() {
let index_usize = insert.0 as crate::ThreadId;
let proof_index = index_usize % 256; // tree depth is fixed to 8
let slot_index = index_usize / 256; // slots depth is fixed to 8
let receiver_32: [u8; 32] = insert
.1
.try_into()
.expect("Hex string must be exactly 32 bytes long");
let receiver_account = sp_runtime::AccountId32::from(receiver_32);
let transfer_amount = insert.2;
let payload =
ThreadPayload::new(proof_index, transfer_amount, receiver_account.clone());
let proof = insert.3;
let slots = insert.4;
let evm_thread_proof = EvmThreadProof::new(slot_index, payload, proof, slots);
let thread_info = ThreadProof::EvmThreadProof(evm_thread_proof);
let pulled_thread_key = thread_info.get_unique_key(session);
assert_eq!(Balances::free_balance(&receiver_account), 0);
assert!(!PulledThreads::<TestRuntime>::contains_key(
&pulled_thread_key
));
assert!(GhostWeaver::pull_thread(
RuntimeOrigin::none(),
network_id,
session,
thread_info
)
.is_ok());
assert!(PulledThreads::<TestRuntime>::contains_key(
&pulled_thread_key
));
let expected_commission = 0u64;
let expected_pure_amount = transfer_amount - expected_commission;
assert_eq!(
Balances::free_balance(&receiver_account),
expected_pure_amount
);
System::assert_last_event(RuntimeEvent::GhostWeaver(Event::ThreadPulled {
pulled_thread_key,
network_id,
receiver: receiver_account,
amount: transfer_amount,
}));
}
});
}
#[test]
fn should_successfully_pull_utxo_thread_and_mint_currency() {
let pair = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
let network_id = 69;
let network_data = NetworkDataBuilder::default()
.with_network_type(NetworkType::Utxo)
.with_gatekeeper(
// Transaction: https://blockstream.info/tx/c41999f86269d5a22f39e18530b065854f447ef64a48be4013c4a9faa657f418
hex::decode("c6e4e0f822c31540b4b05903ce3c67b6cff8e6d4000000000000000000000000")
.expect("Bytes32 from UTXO should be valid"),
)
.build();
let authorities = vec![pair.clone()];
let networks = vec![(network_id, network_data, Default::default())];
// Real world tx at block #960127
// Block: https://blockstream.info/block/0000000000000000000091c72779c83d76dfa8149e950f3a5287377a9708d5c1
let inserts = vec![
(
// Transaction: https://blockstream.info/tx/c41999f86269d5a22f39e18530b065854f447ef64a48be4013c4a9faa657f418
// We are looking into second output and first witness
// NOTE: would be nice to use on of our txs
hex::decode("6969696969696969696969696969696969696969696969696969696969696969").expect("Bytes32 from UTXO should be valid"),
140_693u64,
1,
0,
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "gettxoutproof", "params": [["c41999f86269d5a22f39e18530b065854f447ef64a48be4013c4a9faa657f418"], "0000000000000000000091c72779c83d76dfa8149e950f3a5287377a9708d5c1"]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
hex::decode("008008201643a05d24fe1f015bf472fdedd8ef39dd8247568d3901000000000000000000569ac8f0aed0c5b632dd1dcc031f3d826a170e13cd8176430a2873a33d4694c2501f6a6ad43a02178dbac0d66b1600000e88be94dbcedc3f32bce789eb4512c68b39b91b54c4f2913e84baf7bae21bdc466d954b10876c1e8ad6591461027236c9a471883f96c028a7d1f08deb833f56fa18f457a6faa9c41340be484af67e444f8565b03085e1392fa2d56962f89919c4fcfff99fc6e92be067d01028fb54e8dc1e8425f0ef6bf1cc64220a2ba22163666eb01f99648e8c2af1bb1a0e1ba297f0244a09aa6a6ff87092312b02e8d75aef61e5c08f5b59184a45855e987c5ea114e5da61e55337bfae99f3a2e9601e72ae6036633dc97631609813a30d1185f98e7d6effc4a0330db0394da388c0058e9a730a62d8c679c35bf8d270ca8b95cec7d848729cdfd94a284f060f353b379bfc36310ac78306183a056a04156efd7e77661ce7ba50a4ef9f9e6b3681dfb39d8d0161ad0d2ec46834b5f270b67aa63a8a5b69287e8606b8a438785f8a5af66f84a2641438ba4a5fa22029227e9c2ddb6f23867b112c027d2fc2cf031a46c874701768b5c4983959b98d98736a2ed5ab0dd064ee24e49a3a7dac63e8b857ad4c8b5aa0cf22a7eac3180f3a54630b486de928ecf590e807d2e821818e2fe7e86610187d6673a6f95c639af80a7a7710b0088d44057ca61bdf5f4d5ba68d46b88e4304ffb70000").expect("Bytes32 from UTXO should be valid"),
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getrawtransaction", "params": ["c41999f86269d5a22f39e18530b065854f447ef64a48be4013c4a9faa657f418", true]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
hex::decode("020000000001029ee4235efd4a3cb183d4c311ce858eeee7748263a28afd3d861bac1cb4a8d368010000000001000000d7c5869bfffaa1877d0ecd37e46dc70c232a16f7955e635d57f926d1b3e640bf0100000000ffffffff02ff490b0000000000160014f635b17877709ba3c5acbe153f38a47a733f5b169525020000000000160014c6e4e0f822c31540b4b05903ce3c67b6cff8e6d40247304402200f2f3763fad59d38b4e101b9675450c0a54d45fe17e2b1babde0d0ce6051ea5b0220643e71d66752c0f1bd30dc0897305da77f223d14c781e86063ac181ad7fb90b6012102aad4cca3dab6427bc754a5a123de32cd037e7177fade9f72d83088b00066cffc0247304402200c8f1ff8230813e5e4ab8ec802c614bc8705c407c5958d64dd79cc8422d85a6202203ea832d581c404200b5974fd1a11abfe1ebac213842083cb02eba3f253c8c2aa012102d70df1d4d58701adab82b4816e0640c27403d1fab859ca9f47cb6eccca3dd96200000000").expect("Bytes32 from UTXO should be valid"),
vec![
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["0000000000000000000084bd8092a07bc89245c2bf05f2cf75851bcc5ef92f70", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
hex::decode("00a01520c1d508977a3787523a0f959e14a8df763dc87927c7910000000000000000000036303f2bcf94f752a1d4b0641d1adbc2d80aa89628feefd7f837a74792e26ab8bb276a6ad43a0217bece2e64").expect("Bytes32 from UTXO should be valid"),
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["000000000000000000016792d9c1460bfa4db07da3b724db76c3dd0640ca5754", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
hex::decode("0400e020702ff95ecc1b8575cff205bfc24592c87ba09280bd84000000000000000000008ca9c40d5ab762f160661635cade50fd14571dcf5fdf8c463b617caf9b07dc5a3b296a6ad43a02171340d661").expect("Bytes32 from UTXO should be valid"),
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["0000000000000000000078eb1af63fe8d57f93079121e19968a8563c05af370e", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
hex::decode("000000345457ca4006ddc376db24b7a37db04dfa0b46c1d99267010000000000000000009409d64eb06a6dd83ae2937f70d07222d75505a07f12e756b61a27189851e3bbfe2c6a6ad43a0217a06575b5").expect("Bytes32 from UTXO should be valid"),
],
)
];
new_test_ext(authorities, networks).execute_with(|| {
let session = 0;
let root_string =
hex::decode("0000000000000000000078eb1af63fe8d57f93079121e19968a8563c05af370e")
.expect("Bitcoin Root hex should be valid");
let expected_root = H256::from_slice(&root_string);
LoomStates::<TestRuntime>::insert(&network_id, &session, expected_root);
for insert in inserts.into_iter() {
let receiver_32: [u8; 32] = insert
.0
.try_into()
.expect("Receiver address must be exactly 32 bytes");
let receiver_account = sp_runtime::AccountId32::from(receiver_32);
let transfer_amount = insert.1;
let payload = ThreadPayload::new(12, transfer_amount, receiver_account.clone());
let utxo_thread_proof = UtxoThreadProof::new(
insert.2,
insert.3,
transfer_amount,
payload,
insert.4,
insert.5,
insert.6,
);
let thread_info = ThreadProof::UtxoThreadProof(utxo_thread_proof);
let pulled_thread_key = thread_info.get_unique_key(session);
assert_eq!(Balances::free_balance(&receiver_account), 0);
assert!(!PulledThreads::<TestRuntime>::contains_key(
&pulled_thread_key
));
assert!(GhostWeaver::pull_thread(
RuntimeOrigin::none(),
network_id,
session,
thread_info
)
.is_ok());
assert!(PulledThreads::<TestRuntime>::contains_key(
&pulled_thread_key
));
let expected_commission = 0u64;
let expected_pure_amount = transfer_amount - expected_commission;
assert_eq!(
Balances::free_balance(&receiver_account),
expected_pure_amount
);
System::assert_last_event(RuntimeEvent::GhostWeaver(Event::ThreadPulled {
pulled_thread_key,
network_id,
receiver: receiver_account,
amount: transfer_amount,
}));
}
});
}
#[test]
fn on_initialize_should_finalize_session_when_threshold_or_timeout_is_reached() {
let pair1 = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
let pair2 = AuthorityPair::from_string("//Bob", None).expect("Should be valid seed");
let pair3 = AuthorityPair::from_string("//Charlie", None).expect("Should be valid seed");
let network_id = 1;
let new_network_data = NetworkDataBuilder::default()
.with_network_type(NetworkType::Evm)
.build();
let authorities = vec![pair1.clone(), pair2.clone(), pair3.clone()];
let networks = vec![(network_id, new_network_data, 0u64)];
new_test_ext(authorities, networks).execute_with(|| {
let current_substrate_block = 10;
System::set_block_number(current_substrate_block + 1);
let tapestry_draft = TapestryDraftBuilder::default()
.with_current_block(current_substrate_block)
.with_block_delay(5u32)
.with_median_external(100)
.build();
TapestryDrafts::<TestRuntime>::insert(&network_id, tapestry_draft);
let target_hash = H256::repeat_byte(0xAA);
let next_session = 69;
let attestation = AttestationContext::default()
.with_current_block(current_substrate_block)
.with_network_id(network_id)
.with_weaving_session(next_session);
let hash_att1 = attestation
.with_authority_index(0)
.build_hash_attestation(target_hash, next_session);
let hash_att2 = attestation
.with_authority_index(1)
.build_hash_attestation(target_hash, next_session);
assert!(GhostWeaver::weave_weft(
RuntimeOrigin::none(),
hash_att1,
pair1.sign(&hash_att1.encode())
)
.is_ok());
assert!(GhostWeaver::weave_weft(
RuntimeOrigin::none(),
hash_att2,
pair2.sign(&hash_att2.encode())
)
.is_ok());
let state_before = WeavingStates::<TestRuntime>::get(&network_id);
assert_eq!(state_before.count, 2);
assert_eq!(state_before.hash, target_hash);
let current_session = CurrentWeavingSession::<TestRuntime>::get(&network_id);
GhostWeaver::on_initialize(current_substrate_block);
let saved_loot = LoomStates::<TestRuntime>::get(&network_id, current_session);
assert_eq!(saved_loot, target_hash);
let updated_session = CurrentWeavingSession::<TestRuntime>::get(&network_id);
assert_eq!(updated_session, current_session + 1);
System::set_block_number(15);
GhostWeaver::on_initialize(15);
});
}
#[test]
fn test_validate_block_numbers_consensus_comprehensive() {
// Test 1: Not enough blocks (below threshold)
{
let mut blocks = create_block_numbers(&[1, 2, 3]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 10u64, 5);
assert_eq!(result, None, "Should return None when below threshold");
}
// Test 2: Single block with threshold 1
{
let mut blocks = create_block_numbers(&[42]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 10u64, 1);
assert_eq!(result, Some(42u8.into()), "Should accept single block");
}
// Test 3: All blocks within deviation
{
let mut blocks = create_block_numbers(&[10, 11, 12, 13, 14]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 5u64, 3);
assert_eq!(
result,
Some(12u8.into()),
"Should find median when all within deviation"
);
}
// Test 4: Blocks with outliers
{
let mut blocks = create_block_numbers(&[1, 10, 11, 12, 13]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 3u64, 3);
assert_eq!(
result,
Some(11u8.into()),
"Should find consensus among 10,11,12,13"
);
}
// Test 5: Even number of blocks in consensus window
{
let mut blocks = create_block_numbers(&[1, 5, 6, 7, 8, 9, 10, 100]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 5u64, 4);
assert_eq!(
result,
Some(7u8.into()),
"Even window should average middle two"
);
}
// Test 6: Large deviations
{
let mut blocks = create_block_numbers(&[1, 2, 3, 100, 101, 102]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 2u64, 3);
assert_eq!(
result,
Some(101u8.into()),
"Should pick majority cluster 1,2,3"
);
}
// Test 7: Multiple clusters
{
let mut blocks = create_block_numbers(&[1, 2, 3, 20, 21, 22, 23]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 3u64, 4);
assert_eq!(
result,
Some(21u8.into()),
"Should pick larger cluster 20-23"
);
}
// Test 8: Max deviation = 0 (all must be equal)
{
let mut blocks = create_block_numbers(&[5, 5, 5, 6, 5]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 0u64, 3);
assert_eq!(result, Some(5u8.into()), "Should only accept exact matches");
}
// Test 9: Max deviation = 0 with no consensus
{
let mut blocks = create_block_numbers(&[1, 2, 3, 4, 5]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 0u64, 3);
assert_eq!(result, None, "Should reject when no exact matches");
}
// Test 10: Duplicate values
{
let mut blocks = create_block_numbers(&[1, 1, 1, 2, 2, 2, 2, 3]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 1u64, 4);
assert_eq!(
result,
Some(2u8.into()),
"Should handle duplicates correctly"
);
}
// Test 11: Unsorted input (should sort internally)
{
let mut blocks = create_block_numbers(&[5, 1, 4, 2, 3]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 10u64, 5);
assert_eq!(result, Some(3u8.into()), "Should sort and find median");
}
}
#[test]
fn test_validate_block_numbers_consensus_edge_cases() {
// Edge 1: Empty vector
{
let mut blocks: Vec<crate::ExternalBlockNumber> = vec![];
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 10u64, 1);
assert_eq!(result, None, "Empty vector should return None");
}
// Edge 2: Very large deviation
{
let mut blocks = create_block_numbers(&[1, 100, 200, 300, 400]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 1000u64, 3);
assert_eq!(
result,
Some(200u8.into()),
"Should find consensus with large deviation"
);
}
// Edge 3: Threshold equals total blocks
{
let mut blocks = create_block_numbers(&[1, 2, 3, 4, 5]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 10u64, 5);
assert_eq!(
result,
Some(3u8.into()),
"Should work when threshold equals total"
);
}
// Edge 4: Threshold = 1 (minimum)
{
let mut blocks = create_block_numbers(&[10, 20, 30, 40, 50]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 100u64, 1);
assert_eq!(
result,
Some(30u8.into()),
"Threshold 1 should return median of all"
);
}
// Edge 5: Max deviation = usize::MAX
{
let mut blocks = create_block_numbers(&[1, 1000, 2000]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, u64::MAX, 2);
assert_eq!(
result,
Some(1000u16.into()),
"Should handle maximum deviation"
);
}
// Edge 6: All values identical
{
let mut blocks = create_block_numbers(&[5, 5, 5, 5, 5]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 0u64, 3);
assert_eq!(
result,
Some(5u8.into()),
"Should return the identical value"
);
}
// Edge 7: Sequential numbers with tight deviation
{
let mut blocks = create_block_numbers(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 5u64, 5);
assert_eq!(
result,
Some(3u8.into()),
"Sequential numbers with small deviation"
);
}
}
#[test]
fn test_validate_block_numbers_consensus_boundary_conditions() {
// Boundary 1: Exactly at threshold
{
let mut blocks = create_block_numbers(&[10, 11, 12]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 2u64, 3);
assert_eq!(
result,
Some(11u8.into()),
"Exactly at threshold should work"
);
}
// Boundary 2: One less than threshold
{
let mut blocks = create_block_numbers(&[10, 11, 12]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 1u64, 3);
assert_eq!(result, None, "Below threshold should return None");
}
// Boundary 3: Exactly at max deviation
{
let mut blocks = create_block_numbers(&[10, 15, 20]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 10u64, 3);
assert_eq!(
result,
Some(15u8.into()),
"Deviation exactly at max should be accepted"
);
}
// Boundary 4: Just over max deviation
{
let mut blocks = create_block_numbers(&[10, 16, 20]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 15u64, 3);
assert_eq!(
result,
Some(16u8.into()),
"Deviation just over max for some pairs"
);
}
// Boundary 5: Two equal-sized clusters
{
let mut blocks = create_block_numbers(&[1, 2, 3, 100, 101, 102]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 2u64, 3);
assert_eq!(
result,
Some(101u8.into()),
"Should pick first cluster when equal"
);
}
}
#[test]
fn test_validate_block_numbers_consensus_complex_scenarios() {
// Scenario 1: Multi-modal distribution
{
let mut blocks = create_block_numbers(&[
1, 2, 3, // Cluster 1
10, 11, // Cluster 2
20, 21, 22, 23, // Cluster 3 (largest)
100, // Outlier
]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 3u64, 4);
assert_eq!(result, Some(21u8.into()), "Should pick largest cluster");
}
// Scenario 2: Even spread with no clear consensus
{
let mut blocks = create_block_numbers(&[1, 5, 10, 15, 20, 25, 30]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 21u64, 4);
assert_eq!(
result,
Some(10u8.into()),
"Should find best fit even with spread"
);
}
// Scenario 3: Clusters with different densities
{
let mut blocks = create_block_numbers(&[
1, 2, 2, 2, 3, // Dense cluster
10, 11, 12, // Sparse cluster
]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 3u64, 4);
assert_eq!(
result,
Some(2u8.into()),
"Dense cluster should be preferred"
);
}
// Scenario 4: Decreasing order input
{
let mut blocks = create_block_numbers(&[10, 9, 8, 7, 6, 5]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 10u64, 5);
assert_eq!(
result,
Some(7u8.into()),
"Should sort and handle decreasing order"
);
}
// Scenario 5: Values at type boundaries
{
let min_val: crate::ExternalBlockNumber = 0u64.into();
let max_val: crate::ExternalBlockNumber = u64::MAX.into();
let mut blocks = vec![
min_val,
(u64::MAX / 2).into(),
max_val,
(u64::MAX / 4).into(),
(3 * (u64::MAX / 4)).into(),
];
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, u64::MAX / 8, 3);
// Should work with large numbers
assert!(
result.is_some() || result.is_none(),
"Should handle boundary values"
);
}
}
#[test]
fn test_block_numbers_consensus_median_calculation() {
// Test median for odd number of elements
{
let mut blocks = create_block_numbers(&[1, 2, 3, 100, 101]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 200u64, 5);
assert_eq!(
result,
Some(3u8.into()),
"Should return median for odd count"
);
}
// Test median for even number of elements
{
let mut blocks = create_block_numbers(&[1, 2, 3, 4]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 0u64, 4);
// When no consensus, should return None for max_deviation=0
assert!(
result.is_none() || result.is_some(),
"Even median calculation"
);
}
// Test median with duplicate values
{
let mut blocks = create_block_numbers(&[1, 2, 2, 2, 2, 2, 3, 3, 4]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 1u64, 5);
assert_eq!(
result,
Some(2u8.into()),
"Should handle duplicates in median"
);
}
}
#[test]
fn test_validate_block_numbers_consensus_expansion_strategy() {
// Test left expansion preference
{
let mut blocks = create_block_numbers(&[1, 2, 3, 100, 101]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 2u64, 3);
assert_eq!(result, Some(2u8.into()), "Should expand left when closer");
}
// Test right expansion preference
{
let mut blocks = create_block_numbers(&[1, 2, 100, 101, 102]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 2u64, 3);
assert_eq!(
result,
Some(101u8.into()),
"Should expand right when closer"
);
}
// Test tie-breaking (expand right)
{
let mut blocks = create_block_numbers(&[1, 2, 13, 14, 15]);
let result = GhostWeaver::validate_block_numbers_consensus(&mut blocks, 3u64, 3);
assert_eq!(
result,
Some(14u8.into()),
"Should break ties by expanding right"
);
}
}