1380 lines
54 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|