#![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 { values.iter().map(|&v| v.into()).collect() } fn apply_and_clear_pool_transaction(pool_state: &Arc>, 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::::remove(network_id); System::set_block_number(12); GhostWeaver::on_initialize(12); assert!(TapestryDrafts::::get(network_id).is_none()); System::set_block_number(14); GhostWeaver::on_initialize(14); assert!(TapestryDrafts::::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 = (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![target_endpoint.clone()]; sp_io::offchain::local_storage_set( sp_core::offchain::StorageKind::PERSISTENT, &endpoint_key, &rpc_endpoints.encode(), ); let custom_headers: Vec<(Vec, Vec)> = 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::::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::::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::::get(&utxo_network_id).is_none()); GhostWeaver::on_initialize(3u64); assert!(TapestryDrafts::::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::::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::::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::::get(&evm_network_id).is_none()); GhostWeaver::on_initialize(4u64); assert!(TapestryDrafts::::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::::default(); let evm_tapestry_draft = tapestry_draft.with_median_external(25639556).build(); let utxo_tapestry_draft = tapestry_draft.with_median_external(960129).build(); CurrentWeavingSession::::insert(&evm_network_id, evm_session); CurrentWeavingSession::::insert(&utxo_network_id, utxo_session); TapestryDrafts::::insert(&evm_network_id, evm_tapestry_draft); TapestryDrafts::::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![target_endpoint.clone()]; sp_io::offchain::local_storage_set( sp_core::offchain::StorageKind::PERSISTENT, &endpoint_key, &rpc_endpoints.encode(), ); let custom_headers: Vec<(Vec, Vec)> = 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::::get(utxo_network_id); apply_and_clear_pool_transaction(&pool_state, 0); let curr_weaving_state = WeavingStates::::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::::get(&utxo_network_id, &utxo_session); GhostWeaver::on_initialize(3u64); let curr_state = LoomStates::::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::::get(evm_network_id); apply_and_clear_pool_transaction(&pool_state, 1); let curr_weaving_state = WeavingStates::::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::::get(&evm_network_id, &evm_session); GhostWeaver::on_initialize(4u64); let curr_state = LoomStates::::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::::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::::contains_key( &pulled_thread_key )); assert!(GhostWeaver::pull_thread( RuntimeOrigin::none(), network_id, session, thread_info ) .is_ok()); assert!(PulledThreads::::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::::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::::contains_key( &pulled_thread_key )); assert!(GhostWeaver::pull_thread( RuntimeOrigin::none(), network_id, session, thread_info ) .is_ok()); assert!(PulledThreads::::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::::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::::get(&network_id); assert_eq!(state_before.count, 2); assert_eq!(state_before.hash, target_hash); let current_session = CurrentWeavingSession::::get(&network_id); GhostWeaver::on_initialize(current_substrate_block); let saved_loot = LoomStates::::get(&network_id, current_session); assert_eq!(saved_loot, target_hash); let updated_session = CurrentWeavingSession::::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 = 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" ); } }