#![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 { 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() .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::::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/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::::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::::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 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::::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::::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 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::::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_loom = LoomStates::::get(&network_id, current_session); assert_ne!(saved_loom, target_hash); assert_eq!(saved_loom, saved_loom); let updated_session = CurrentWeavingSession::::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 = 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" ); } }