diff --git a/pallets/weaver/Cargo.toml b/pallets/weaver/Cargo.toml index 03124ec..ba049dc 100644 --- a/pallets/weaver/Cargo.toml +++ b/pallets/weaver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-weaver" -version = "0.0.16" +version = "0.0.21" description = "Weaving a secure cryptographic tapestry across different external chains." license.workspace = true authors.workspace = true diff --git a/pallets/weaver/src/benchmarking.rs b/pallets/weaver/src/benchmarking.rs index 106d7dd..52e4e63 100644 --- a/pallets/weaver/src/benchmarking.rs +++ b/pallets/weaver/src/benchmarking.rs @@ -35,6 +35,9 @@ fn prepare_pallet( T::NetworkDataHandler::register(network_id, network_data) .expect("network should be valid for insertion"); + let current_block = T::BlockNumberProvider::current_block_number(); + Pallet::::on_initialize(current_block); + (network_id, authority, 0) } @@ -53,6 +56,7 @@ mod benchmarks { let block_attestation = AttestationContext::default() .with_network_id(network_id) .with_authority_index(authority_index) + .with_current_block(1337u16.into()) .build_block_attestation(420); let signature = authority @@ -101,6 +105,7 @@ mod benchmarks { .with_network_id(network_id) .with_weaving_session(weaving_session) .with_authority_index(authority_index) + .with_current_block(1337u16.into()) .build_hash_attestation(root_hash, next_session); let signature = authority @@ -320,9 +325,12 @@ mod benchmarks { let first_sha = sp_io::hashing::sha2_256(&last_header[..80]); let mut native_le = sp_io::hashing::sha2_256(&first_sha); native_le.reverse(); - let dynamic_trusted_root = H256::from(native_le); - LoomStates::::insert(&network_id, &session, dynamic_trusted_root); + let at_block = 420; + let dynamic_trusted_root = H256::from(native_le); + let loom_state = LoomState { at_block, root_hash: dynamic_trusted_root }; + + LoomStates::::insert(&network_id, &session, loom_state); let utxo_thread_proof = UtxoThreadProof::new( gettxproof_bytes, @@ -360,8 +368,11 @@ mod benchmarks { let root_string = hex::decode("07f4a5ba4750d9ddfc080429287cbcbde7464aa2f8ae13feaade0dcb45ca6129") .expect("EVM Root hex should be valid"); + + let at_block = 420; let expected_root = H256::from_slice(&root_string); - LoomStates::::insert(&network_id, &session, expected_root); + let loom_state = LoomState { at_block, root_hash: expected_root }; + LoomStates::::insert(&network_id, &session, loom_state); let receiver_32: [u8; 32] = hex::decode("0ae3ba32f8555fe3031461e2cb6feeadb67029ef7490b982824fc90751a4cb3e") diff --git a/pallets/weaver/src/lib.rs b/pallets/weaver/src/lib.rs index 766ec07..6e01a67 100644 --- a/pallets/weaver/src/lib.rs +++ b/pallets/weaver/src/lib.rs @@ -22,7 +22,7 @@ use sp_runtime::{ storage::StorageValueRef, storage_lock::{StorageLock, Time}, }, - traits::{BlockNumberProvider, UniqueSaturatedInto}, + traits::{BlockNumberProvider, Saturating, UniqueSaturatedInto}, RuntimeAppPublic, }; @@ -182,6 +182,15 @@ pub mod pallet { CouldNotRegisterIncoming, } + #[pallet::storage] + #[pallet::getter(fn attested_blocks)] + pub(super) type AttestedBlocks = StorageMap< + _, + Twox64Concat, AuthIndex, + BlockNumberFor, + ValueQuery, + >; + #[pallet::storage] #[pallet::getter(fn current_weaving_session)] pub(super) type CurrentWeavingSession = @@ -202,7 +211,7 @@ pub mod pallet { _, Twox64Concat, NetworkIdOf, Twox64Concat, WeavingSession, - H256, + LoomState, ValueQuery, >; @@ -288,7 +297,12 @@ pub mod pallet { ); return; } - LoomStates::::insert(&data.0, data.1, data.2); + let initial_loom_state = LoomState { + at_block: 0u16.into(), + root_hash: data.2, + }; + + LoomStates::::insert(&data.0, data.1, initial_loom_state); }); if !self.authorities.is_empty() { @@ -330,26 +344,20 @@ pub mod pallet { let external_block = block_attestation.external_block; - let current_authorities_count = Authorities::::decode_len(). - unwrap_or_default(); - NetworkAttestations::::try_mutate( &network_id, |attestations| -> DispatchResult { - if attestations.is_empty() { - attestations.bounded_resize(current_authorities_count, None); - } + let index = authority_index as usize; - ensure!( - (authority_index as usize) < attestations.len(), - Error::::TooManyAttestations, - ); - - attestations[authority_index as usize] = Some(external_block); + ensure!(index < attestations.len(), Error::::TooManyAttestations); + attestations[index] = Some(external_block); Ok(()) })?; + let current_block = T::BlockNumberProvider::current_block_number(); + AttestedBlocks::::insert(&authority_index, current_block); + Self::deposit_event(Event::::BlockAttestated { network_id, authority_index, @@ -406,6 +414,9 @@ pub mod pallet { } }); + let current_block = T::BlockNumberProvider::current_block_number(); + AttestedBlocks::::insert(&authority_index, current_block); + Self::deposit_event(Event::::HashAttestated { authority_index, next_session, @@ -438,7 +449,7 @@ pub mod pallet { let network_data = T::NetworkDataHandler::get(&network_id) .ok_or(Error::::NonExistentNetworkId)?; - let root_hash = LoomStates::::get(&network_id, &session); + let state = LoomStates::::get(&network_id, &session); let pulled_thread_key = thread_proof.get_unique_key(session, network_id); ensure!( @@ -447,7 +458,7 @@ pub mod pallet { ); let (receiver_account, amount) = thread_proof - .verify_thread_proof(root_hash, network_id, &network_data.gatekeeper) + .verify_thread_proof(state.root_hash, network_id, &network_data.gatekeeper) .ok_or(Error::::InvalidMerkleProof)?; let receiver_bytes: &[u8; 32] = receiver_account.as_ref(); @@ -504,11 +515,13 @@ pub mod pallet { let current_session = CurrentWeavingSession::::get(&network_id); let loom_state_changed = LoomStates::::mutate(&network_id, ¤t_session, |loom_state| { - if *loom_state != weaving_state.hash && threshold_reached { - *loom_state = weaving_state.hash; - return true; - } - false + if !threshold_reached { return false; } + + let hash_changed = loom_state.root_hash != weaving_state.hash; + loom_state.at_block = tapestry_draft.median; + if hash_changed { loom_state.root_hash = weaving_state.hash; } + + hash_changed }); if loom_state_changed && weaving_state.next_session != current_session { @@ -518,8 +531,9 @@ pub mod pallet { CurrentWeavingSession::::insert(&network_id, next_session); } - weight.saturating_accrue(T::DbWeight::get().writes(1)); + weight.saturating_accrue(T::DbWeight::get().writes(2)); TapestryDrafts::::remove(&network_id); + WeavingStates::::remove(&network_id); } } None => { @@ -532,9 +546,11 @@ pub mod pallet { weight.saturating_accrue(T::DbWeight::get().reads(1)); let attestation_len = NetworkAttestations::::decode_len(&network_id); - if attestation_len.unwrap_or(0) > len { - weight.saturating_accrue(T::DbWeight::get().writes(1)); - NetworkAttestations::::remove(&network_id); + if attestation_len.unwrap_or(0) != len { + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + NetworkAttestations::::mutate(&network_id, |attestations| { + attestations.bounded_resize(len, None); + }); return weight; } @@ -616,6 +632,21 @@ pub mod pallet { .propagate(true) .build() } + Call::pull_thread { + network_id, + session, + thread_proof, + } => { + let thread_context = + Self::validate_thread(network_id, session, thread_proof)?; + + ValidTransaction::with_tag_prefix("WeaverThread") + .priority(T::UnsignedPriority::get()) + .and_provides(thread_context.to_fixed_bytes()) + .longevity(LOCK_BLOCK_EXPIRATION) + .propagate(true) + .build() + } _ => InvalidTransaction::Call.into(), } } @@ -623,6 +654,31 @@ pub mod pallet { } impl Pallet { + fn validate_thread( + network_id: &NetworkIdOf, + session: &WeavingSession, + thread_proof: &ThreadProof>, + ) -> Result { + let network_data = T::NetworkDataHandler::get(&network_id) + .ok_or(InvalidTransaction::BadProof)?; + + let root_hash = LoomStates::::get(&network_id, &session).root_hash; + if root_hash == Default::default() { + return Err(InvalidTransaction::BadProof); + } + + thread_proof + .verify_thread_proof(root_hash, *network_id, &network_data.gatekeeper) + .ok_or(InvalidTransaction::BadProof)?; + + let pulled_thread_key = thread_proof.get_unique_key(*session, *network_id); + if PulledThreads::::contains_key(&pulled_thread_key) { + return Err(InvalidTransaction::Stale); + } + + Ok(pulled_thread_key) + } + fn validate_attestation_signature( attestation: &A, signature: &::Signature, @@ -632,16 +688,6 @@ impl Pallet { A: Encode, { let authority_index = attestation.authority_index(); - let attestation_block = attestation.block(); - let network_id = attestation.network_id(); - - if DisabledAuthorities::::get().contains(authority_index) { - return Err(InvalidTransaction::BadSigner); - } - - if T::BlockNumberProvider::current_block_number() <= attestation_block { - return Err(InvalidTransaction::BadProof); - } Authorities::::get() .get(authority_index as usize) @@ -653,6 +699,31 @@ impl Pallet { .ok_or(InvalidTransaction::BadSigner) })?; + let attestation_block = attestation.block(); + let current_block = T::BlockNumberProvider::current_block_number(); + let lock: BlockNumberFor = LOCK_BLOCK_EXPIRATION.unique_saturated_into(); + let block_expiration = attestation_block.saturating_add(lock); + + let block_expired = current_block >= block_expiration; + let block_attested = AttestedBlocks::::get(authority_index) + .ge(&attestation_block); + + if block_expired || block_attested { + return Err(InvalidTransaction::BadProof); + } + + let network_id = attestation.network_id(); + let current_attestation_len = NetworkAttestations::::decode_len(&network_id) + .unwrap_or(0); + + if authority_index as usize >= current_attestation_len { + return Err(InvalidTransaction::BadSigner.into()); + } + + if DisabledAuthorities::::get().contains(authority_index) { + return Err(InvalidTransaction::BadSigner); + } + if !T::NetworkDataHandler::contains_key(&network_id) { return Err(InvalidTransaction::BadProof); } diff --git a/pallets/weaver/src/tests.rs b/pallets/weaver/src/tests.rs index 4fc1700..412f507 100644 --- a/pallets/weaver/src/tests.rs +++ b/pallets/weaver/src/tests.rs @@ -76,6 +76,7 @@ fn should_successfully_compute_median_and_start_weaving_round() { let networks = vec![(network_id, network_data, Default::default())]; new_test_ext(authorities, networks).execute_with(|| { + GhostWeaver::on_initialize(1); System::set_block_number(2); let context = AttestationContext::default() @@ -120,6 +121,7 @@ fn should_apply_cooldown_delay_on_consensus_failure() { let networks = vec![(network_id, network_data, Default::default())]; new_test_ext(authorities, networks).execute_with(|| { + GhostWeaver::on_initialize(9); System::set_block_number(10); let context = AttestationContext::default() @@ -168,6 +170,7 @@ fn validate_unsigned_should_prevent_mempool_dos_via_tags() { let networks = vec![(network_id, network_data, Default::default())]; new_test_ext(authorities, networks).execute_with(|| { + GhostWeaver::on_initialize(1); System::set_block_number(2); let att = AttestationContext::default() @@ -240,6 +243,9 @@ fn offchain_worker_should_fetch_block_and_submit_unsigned_tx() { ext.register_extension(TransactionPoolExt::new(pool)); ext.execute_with(|| { + GhostWeaver::on_initialize(1); + GhostWeaver::on_initialize(2); + let network_id_encoded = evm_network_id.encode(); let mut endpoint_key = b"ghost-weaver::".to_vec(); @@ -313,7 +319,6 @@ fn offchain_worker_should_fetch_block_and_submit_unsigned_tx() { 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); @@ -341,7 +346,6 @@ fn offchain_worker_should_fetch_block_and_submit_unsigned_tx() { 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); @@ -362,6 +366,7 @@ fn offchain_worker_should_fetch_block_and_submit_unsigned_tx() { assert_eq!(curr_evm_block, 25639556); assert!(TapestryDrafts::::get(&evm_network_id).is_none()); + GhostWeaver::on_initialize(3u64); GhostWeaver::on_initialize(4u64); assert!(TapestryDrafts::::get(&evm_network_id).is_some()); }); @@ -372,6 +377,9 @@ fn offchain_worker_should_fetch_hash_and_submit_unsigned_tx() { let evm_network_id = 1u32; let utxo_network_id = 69u32; + let median_utxo_block = 960129; + let median_evm_block = 25639556; + let pair = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed"); let new_evm_network_data = NetworkDataBuilder::default() @@ -406,11 +414,12 @@ fn offchain_worker_should_fetch_hash_and_submit_unsigned_tx() { ext.register_extension(TransactionPoolExt::new(pool)); ext.execute_with(|| { + GhostWeaver::on_initialize(1); + GhostWeaver::on_initialize(2); + 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(); + let evm_tapestry_draft = tapestry_draft.with_median_external(median_evm_block).build(); + let utxo_tapestry_draft = tapestry_draft.with_median_external(median_utxo_block).build(); CurrentWeavingSession::::insert(&evm_network_id, evm_session); CurrentWeavingSession::::insert(&utxo_network_id, utxo_session); @@ -492,7 +501,6 @@ fn offchain_worker_should_fetch_hash_and_submit_unsigned_tx() { 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); @@ -518,14 +526,15 @@ fn offchain_worker_should_fetch_hash_and_submit_unsigned_tx() { 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); + assert_eq!(prev_state.root_hash, H256::default()); + assert_eq!(curr_state.root_hash, block_hash); + assert_eq!(prev_state.at_block, 0); + assert_eq!(curr_state.at_block, median_utxo_block); }); 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); @@ -542,17 +551,20 @@ fn offchain_worker_should_fetch_hash_and_submit_unsigned_tx() { assert_eq!(curr_weaving_state.next_session, evm_session + 1); let bytes = hex::decode("f72b9ef0b66c8cc3952ffd11dc0b85f3b02e6edfa9236da400138f2fcba4668d") - .expect("UTXO block hash should be valid"); + .expect("EVM 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(3u64); 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); + assert_eq!(prev_state.root_hash, H256::default()); + assert_eq!(curr_state.root_hash, block_hash); + assert_eq!(prev_state.at_block, 0); + assert_eq!(curr_state.at_block, median_evm_block); }); } @@ -635,14 +647,18 @@ fn should_successfully_pull_evm_thread_and_mint_currency() { ]; new_test_ext(authorities, networks).execute_with(|| { + GhostWeaver::on_initialize(1); + // https://sepolia.etherscan.io/address/0x7fec11e652f4d54e8cf11c4b52c360380332f2bd#readContract // getRoot: session 0, atBlock 11716130 let session = 0; let root_string = hex::decode("07f4a5ba4750d9ddfc080429287cbcbde7464aa2f8ae13feaade0dcb45ca6129") .expect("Root hex should be valid"); + let expected_root = H256::from_slice(&root_string); - LoomStates::::insert(&network_id, &session, expected_root); + let expected_loom_state = LoomState { at_block: 420, root_hash: expected_root }; + LoomStates::::insert(&network_id, &session, expected_loom_state); let mut total_issuance = 0; @@ -755,7 +771,8 @@ fn should_successfully_pull_utxo_thread_and_mint_currency() { .expect("Bitcoin Root hex should be valid"); let expected_root = H256::from_slice(&block_hash); - LoomStates::::insert(&network_id, &session, expected_root); + let expected_loom_state = LoomState { at_block: 420, root_hash: expected_root }; + LoomStates::::insert(&network_id, &session, expected_loom_state); let expected_receiver_bytes = hex::decode("1657090d02985f0cdb41436fe2d5a83a3acb1995d963a698e7e619e927bdcc5e").unwrap(); let mut receiver_raw = [0u8; 32]; @@ -853,7 +870,8 @@ fn should_successfully_pull_utxo_thread_and_reject_nominal_varint_encoding() { .expect("Bitcoin Root hex should be valid"); let expected_root = H256::from_slice(&block_hash); - LoomStates::::insert(&network_id, &session, expected_root); + let expected_loom_state = LoomState { at_block: 420, root_hash: expected_root }; + LoomStates::::insert(&network_id, &session, expected_loom_state); let expected_receiver_bytes = hex::decode("1657090d02985f0cdb41436fe2d5a83a3acb1995d963a698e7e619e927bdcc5e").unwrap(); let mut receiver_raw = [0u8; 32]; @@ -928,6 +946,7 @@ fn on_initialize_should_finalize_session_when_timeout_is_reached() { new_test_ext(authorities, networks).execute_with(|| { let current_substrate_block = 10; + GhostWeaver::on_initialize(current_substrate_block); System::set_block_number(current_substrate_block + 1); let tapestry_draft = TapestryDraftBuilder::default() @@ -975,8 +994,96 @@ fn on_initialize_should_finalize_session_when_timeout_is_reached() { 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); + assert_ne!(saved_loom.root_hash, target_hash); + assert_eq!(saved_loom.at_block, 0); + + let updated_session = CurrentWeavingSession::::get(&network_id); + assert_eq!(updated_session, current_session); + }); +} + +#[test] +fn on_initialize_should_update_at_block_when_threshold_reached_with_same_hash() { + 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; + let current_session = CurrentWeavingSession::::get(&network_id); + + let target_hash = H256::repeat_byte(0xAA); + LoomStates::::insert(&network_id, ¤t_session, crate::LoomState { + root_hash: target_hash, + at_block: 0, + }); + + System::set_block_number(current_substrate_block); + GhostWeaver::on_initialize(current_substrate_block); + + 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 attestation = AttestationContext::default() + .with_current_block(current_substrate_block) + .with_network_id(network_id) + .with_weaving_session(current_session); + + let hash_att1 = attestation + .with_authority_index(0) + .build_hash_attestation(target_hash, current_session); + + let hash_att2 = attestation + .with_authority_index(1) + .build_hash_attestation(target_hash, current_session); + + let hash_att3 = attestation + .with_authority_index(2) + .build_hash_attestation(target_hash, current_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()); + assert!(GhostWeaver::weave_weft( + RuntimeOrigin::none(), + hash_att3, + pair3.sign(&hash_att3.encode()) + ) + .is_ok()); + + let state_before = WeavingStates::::get(&network_id); + assert_eq!(state_before.count, 3); + assert_eq!(state_before.hash, target_hash); + + GhostWeaver::on_initialize(current_substrate_block); + + let saved_loom = LoomStates::::get(&network_id, current_session); + + assert_eq!(saved_loom.root_hash, target_hash); + assert_eq!(saved_loom.at_block, 100); let updated_session = CurrentWeavingSession::::get(&network_id); assert_eq!(updated_session, current_session); diff --git a/pallets/weaver/src/types.rs b/pallets/weaver/src/types.rs index f934c69..e4be6c6 100644 --- a/pallets/weaver/src/types.rs +++ b/pallets/weaver/src/types.rs @@ -14,7 +14,6 @@ use ghost_helpers::{ merkle_tree::verify_tree_proof, SubstrateBlake2Hasher, SubstrateKeccakHasher, UtxoSha2Hasher, }; -use ghost_traits::hashing::GhostHasher; use crate::{ExternalBlockNumber, ThreadId, WeavingSession}; @@ -53,6 +52,25 @@ where } } +#[derive( + Default, + RuntimeDebug, + Clone, + Copy, + Eq, + PartialEq, + Ord, + PartialOrd, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, +)] +pub struct LoomState { + pub at_block: ExternalBlockNumber, + pub root_hash: H256, +} + #[derive( Default, RuntimeDebug, @@ -350,8 +368,10 @@ where { if EVM_MERKLE_DEPTH as usize != self.proof.len() { return None; } + let preimage_hash = self.get_preimage_hash(network_id)?; + let proof_valid = verify_tree_proof::( - &self.get_preimage_slice(network_id).to_fixed_bytes(), + &preimage_hash.to_fixed_bytes(), self.proof.as_ref(), root_hash, self.proof_index, @@ -362,22 +382,17 @@ where Some((self.receiver.clone(), self.amount)) } - fn get_preimage_slice(&self, network_id: NetworkId) -> H256 + fn get_preimage_hash(&self, network_id: NetworkId) -> Option where NetworkId: Copy + Clone + Eq + PartialEq + UniqueSaturatedInto, { let slot_index_usize: usize = self.slot_index.unique_saturated_into(); - let mut slot_values: Vec = self.slots.to_vec(); - match slot_values.get_mut(slot_index_usize) { - Some(slot) => { - *slot = self.compute_arguments_hash(network_id); - cumulative_hash::(slot_values) - } - None => { - SubstrateKeccakHasher::empty() - } - } + self.slots.get(slot_index_usize) + .filter(|&&provided_hash| { + provided_hash == self.compute_arguments_hash(network_id) + }) + .map(|_| cumulative_hash::(self.slots.iter())) } pub fn get_unique_key(