1368 lines
53 KiB
Rust
1368 lines
53 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::{
|
|
Perbill,
|
|
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()
|
|
.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(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(102);
|
|
let att3 = context.with_authority_index(2).build_block_attestation(104);
|
|
|
|
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/0x81acdf2da6b50d806c96aecd9a498a5359e373c0#readContract
|
|
// getRoot: atBlock 11660400, 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(
|
|
"675a2e7b05b8b3932cd0faf88f1bc29767b7462667f56d051b8d5325bda0ee73",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"da1fbdfefa1e9aedfedcfc48fb17cb0e731529f7a3695114e0a9caa1989dd420",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"1c792b14bf66f82af36f00f5fba7014fa0c1e2ff3c7c273bfe523c1acf67dc3f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"5fa080a686a5a0d05c3d4822fd54d632dc9cc04b1616046eba2ce499eb9af79f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"5eb949690a0404abf4cebafc7cfffa382191b7dd9e7df778581e6fb78efab35f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"d364c9d5dadad4569b6dd47f7feabafa3571f842434425548335ac6e690dd071",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"68d8bc5b77979c1a6702334f529f5783f79e942fd2cd03f6e55ac2cf496e849f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"de9c446fab46a8d27db1e3100f275a777d385b44e3cbc045cabac9da36cae040",
|
|
)
|
|
.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(
|
|
"c5354289f0397c916949373043eb1d5ea99447f67fe014324d96c58ef02a2d11",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"da1fbdfefa1e9aedfedcfc48fb17cb0e731529f7a3695114e0a9caa1989dd420",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"1c792b14bf66f82af36f00f5fba7014fa0c1e2ff3c7c273bfe523c1acf67dc3f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"5fa080a686a5a0d05c3d4822fd54d632dc9cc04b1616046eba2ce499eb9af79f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"5eb949690a0404abf4cebafc7cfffa382191b7dd9e7df778581e6fb78efab35f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"d364c9d5dadad4569b6dd47f7feabafa3571f842434425548335ac6e690dd071",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"68d8bc5b77979c1a6702334f529f5783f79e942fd2cd03f6e55ac2cf496e849f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"de9c446fab46a8d27db1e3100f275a777d385b44e3cbc045cabac9da36cae040",
|
|
)
|
|
.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(
|
|
"f39a869f62e75cf5f0bf914688a6b289caf2049435d8e68c5c5e6d05e44913f3",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"e0090094ce1d7af8a81ba78bef4b137c1ab32d20d5487224a9f8df95b516c1e4",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"1c792b14bf66f82af36f00f5fba7014fa0c1e2ff3c7c273bfe523c1acf67dc3f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"5fa080a686a5a0d05c3d4822fd54d632dc9cc04b1616046eba2ce499eb9af79f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"5eb949690a0404abf4cebafc7cfffa382191b7dd9e7df778581e6fb78efab35f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"d364c9d5dadad4569b6dd47f7feabafa3571f842434425548335ac6e690dd071",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"68d8bc5b77979c1a6702334f529f5783f79e942fd2cd03f6e55ac2cf496e849f",
|
|
)
|
|
.expect("Proof should be valid"),
|
|
),
|
|
H256::from_slice(
|
|
&hex::decode(
|
|
"de9c446fab46a8d27db1e3100f275a777d385b44e3cbc045cabac9da36cae040",
|
|
)
|
|
.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/0x81acdf2da6b50d806c96aecd9a498a5359e373c0#readContract
|
|
// getRoot: session 0, atBlock 11660400
|
|
let session = 0;
|
|
let root_string =
|
|
hex::decode("28d1e6b476b03a464f74d531d532572db29d6b57349e4f17f84e92843f6c4ee7")
|
|
.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 proof = insert.3;
|
|
let slots = insert.4;
|
|
|
|
let evm_thread_proof = EvmThreadProof::new(
|
|
slot_index,
|
|
proof_index,
|
|
receiver_account.clone(),
|
|
transfer_amount,
|
|
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 incoming_share = 500_000_000;
|
|
|
|
let network_id = 69;
|
|
let network_data = NetworkDataBuilder::default()
|
|
.with_network_type(NetworkType::Utxo)
|
|
.with_incoming_share(incoming_share)
|
|
.with_gatekeeper(
|
|
// Transaction: https://blockstream.info/tx/4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e
|
|
hex::decode("4635b1794a22bfbe6c5c5eba17b693f4aaf0e348")
|
|
.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 #481838
|
|
// Block https://blockstream.info/block/0000000000000000011bdccb39e19e942a60edffdccd9779653c5b46b337904a
|
|
// Transaction: https://blockstream.info/tx/4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e
|
|
|
|
let payload = (
|
|
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "gettxoutproof", "params": [["4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e"], "0000000000000000011bdccb39e19e942a60edffdccd9779653c5b46b337904a"]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
|
hex::decode("00000020375a14cfcf5730daa8ff8d7a5bd2cf59fba797e289c2e0000000000000000000529cd017146361b57e6b7353b16d8d38d32f92ee842039dd779d35bfbfa2f3e53f649e59e93c01187900ea9fb40600000cfbd7b718845d123b1ae1a7fd3bce9c97acba097dc912c4ac67a920395db07ba4eef685960869ba70e0ca38890565c97dc11f1d7ae538e7b509069f81e38eff1a2e4a6b66fc4233d7d0580af4d195c7f0b274849f19d405dfc6b930bb9edd3f4a4c1f1e9fa3a336c50e8ea16039e4db4bef7ff5946e3a019937bdf626c2336d0e3926a83a575218f84e6dc55ffb9d0997a41e4b4d5425a85f22160c9f52362a95021a573ff96bb429bd80a8ec9dcb899971553efe91b6a0fe57c4694c3e8da8dc7f1babd514f2746da1ed06f08664baed853c334e0a3917d0c859b19ee14f448f77e624822070db3b606d5092027ca9ecf7e0243e6c151081021ebb3ddcdd28c1b8f9fdcffebd1962590477675f3d29d52602a1e7e941f772a3bbff42cf2eb6e74a1124bf424f767e294ae6253a21602c7c6b0381743dd6dcd57b025983e0e34375b24f67ac8beb00713d2f70594bbf797d5d94c061cb9965e6458f2f017f983d7ce37361fa13fda372f595dc1f22895ab93339457c72c480fed1c4d1164a242303ff3600").expect("Bytes32 from UTXO should be valid"),
|
|
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getrawtransaction", "params": ["4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
|
hex::decode("010000000299a43fae581f80baf44e021ddf7b11995f184db282d6e48c22248b9fe79c43bb010000008b483045022100e9db01784dcd879c65d66127c2d52a4f9244321d15796893d6dccadaa1146e3d02203d9bb80f8df8b66692d3d9517f7cbb898d7cc40a30b175432322e1956c9b973a0141048097e4f8bb8ee104d5c2ece11d42f7e4719df8ddba459ecadecaba87d233c81a3c14830371392dd8ed030ef970450c0b7af48d9db4ba20c046896481602eb9dbffffffffb3fd05bc6000a5a0384e9a613d452e9307568df0a964907068e00403a0a0e09e020000006a47304402205bb1cbad4b29421648c1e22cac32a56aa06c933fe655559823fffb71ffebbc0b022021648d970560aa5b2031560b52a0fd06305995a58a457663324c18c1a9b0bd0b8121020d6e50b2660af27933c42bc1395fe93df90ffac5e2a989f6a134919fb8cf8075ffffffff030000000000000000296a2769642b5bb666ac1657090d02985f0cdb41436fe2d5a83a3acb1995d963a698e7e619e927bdcc5e7c150000000000001976a914c1c69d91a5719a23ba2094d89a4df3eadfa4aef788acefea2309000000001976a9144635b1794a22bfbe6c5c5eba17b693f4aaf0e34888ac00000000").expect("Bytes32 from UTXO should be valid"),
|
|
vec![
|
|
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["000000000000000000e7da64065cc23d5c5a5aa402f088aaaf37c5fe1c3b6b92", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
|
hex::decode("000000204a9037b3465b3c657997cddcffed602a949ee139cbdc1b010000000000000000669c3e2472384bc95993f154aada38d6b2113d8d32069e32e2724ba40915a1802a669e59e93c011801c8c735").expect("Bytes32 from UTXO should be valid"),
|
|
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["00000000000000000063da6d9362c0ac3b87877d1c5ad7afd5bd706571d5b393", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
|
hex::decode("02000020926b3b1cfec537afaa88f002a45a5a5c3dc25c0664dae7000000000000000000af0f270bf3f868d47089f395197809c360f1506636c960d96afed68bbd72d0b4496d9e59e93c01183ac94b67").expect("Bytes32 from UTXO should be valid"),
|
|
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["000000000000000000ec6063eb743366264368be7ff5021727682cb518963026", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
|
hex::decode("0000002093b3d5716570bdd5afd75a1c7d87873bacc062936dda630000000000000000009ab437e7c66b10a59b67127f290d24f381aba4e03d5e16c062a356eea4d608dea56d9e59e93c0118264f310e").expect("Bytes32 from UTXO should be valid"),
|
|
],
|
|
);
|
|
|
|
new_test_ext(authorities, networks).execute_with(|| {
|
|
let session = 0;
|
|
let block_hash =
|
|
hex::decode("000000000000000000ec6063eb743366264368be7ff5021727682cb518963026")
|
|
.expect("Bitcoin Root hex should be valid");
|
|
|
|
let expected_root = H256::from_slice(&block_hash);
|
|
LoomStates::<TestRuntime>::insert(&network_id, &session, expected_root);
|
|
|
|
let expected_receiver_bytes = hex::decode("1657090d02985f0cdb41436fe2d5a83a3acb1995d963a698e7e619e927bdcc5e").unwrap();
|
|
let mut receiver_raw = [0u8; 32];
|
|
receiver_raw.copy_from_slice(&expected_receiver_bytes);
|
|
|
|
let receiver_account = sp_runtime::AccountId32::from(receiver_raw);
|
|
let transfer_amount: u64 = 153348847;
|
|
|
|
let utxo_thread_proof = UtxoThreadProof::new(payload.0, payload.1, payload.2);
|
|
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 curve_share = Perbill::from_parts(incoming_share).mul_ceil(transfer_amount);
|
|
let expected_pure_amount = transfer_amount.saturating_sub(curve_share);
|
|
|
|
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_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_loom = LoomStates::<TestRuntime>::get(&network_id, current_session);
|
|
assert_ne!(saved_loom, target_hash);
|
|
assert_eq!(saved_loom, saved_loom);
|
|
|
|
let updated_session = CurrentWeavingSession::<TestRuntime>::get(&network_id);
|
|
assert_eq!(updated_session, current_session);
|
|
});
|
|
}
|
|
|
|
#[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"
|
|
);
|
|
}
|
|
}
|