diff --git a/pallets/slow-clap/Cargo.toml b/pallets/slow-clap/Cargo.toml index c0bdefc..fc583a4 100644 --- a/pallets/slow-clap/Cargo.toml +++ b/pallets/slow-clap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-slow-clap" -version = "0.3.54" +version = "0.3.55" description = "Applause protocol for the EVM bridge" license.workspace = true authors.workspace = true diff --git a/pallets/slow-clap/src/benchmarking.rs b/pallets/slow-clap/src/benchmarking.rs index 432ce5f..960db1e 100644 --- a/pallets/slow-clap/src/benchmarking.rs +++ b/pallets/slow-clap/src/benchmarking.rs @@ -17,68 +17,21 @@ benchmarks! { let minimum_balance = <::Currency>::minimum_balance(); let receiver = create_account::(); let amount = minimum_balance + minimum_balance; + let network_id = NetworkIdOf::::default(); let session_index = T::ValidatorSet::session_index(); + let transaction_hash = H256::repeat_byte(1u8); + let args_hash = Pallet::::generate_unique_hash(&receiver, &amount, &network_id); let authorities = vec![T::AuthorityId::generate_pair(None)]; let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities.clone()) .map_err(|()| "more than the maximum number of keys provided")?; Authorities::::set(&session_index, bounded_authorities); + let authority_index = 0u32; - let clap = Clap { - session_index: 0, - authority_index: 0, - transaction_hash: H256::repeat_byte(1u8), - block_number: 69, - removed: false, - network_id, - receiver: receiver.clone(), - amount, - }; - - let authority_id = authorities - .get(0usize) - .expect("first authority should exist"); - let encoded_clap = clap.encode(); - let signature = authority_id.sign(&encoded_clap) - .ok_or("couldn't make signature")?; - - }: _(RawOrigin::None, clap, signature) - verify { - assert_eq!(<::Currency>::total_balance(&receiver), amount); - } - - self_applause { - let session_index = T::ValidatorSet::session_index(); - let next_session_index = session_index.saturating_add(1); - let authorities = vec![ - T::AuthorityId::generate_pair(None), - T::AuthorityId::generate_pair(None), - ]; - let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities.clone()) - .map_err(|()| "more than the maximum number of keys provided")?; - Authorities::::set(&session_index, bounded_authorities.clone()); - Authorities::::set(&next_session_index, bounded_authorities); - - let minimum_balance = <::Currency>::minimum_balance(); - let receiver = create_account::(); - let receiver_clone = receiver.clone(); - let amount = minimum_balance + minimum_balance; - let network_id = NetworkIdOf::::default(); - let transaction_hash = H256::repeat_byte(1u8); - - let unique_transaction_hash = >::generate_unique_hash( - &receiver, - &amount, - &network_id, - ); - let storage_key = (session_index, &transaction_hash, &unique_transaction_hash); - let next_storage_key = (next_session_index, &transaction_hash, &unique_transaction_hash); - - >::trigger_nullification_for_benchmark(); let clap = Clap { session_index, - authority_index: 0, + authority_index, transaction_hash, block_number: 69, removed: false, @@ -88,26 +41,54 @@ benchmarks! { }; let authority_id = authorities - .get(0usize) + .get(authority_index as usize) .expect("first authority should exist"); - let encoded_clap = clap.encode(); - let signature = authority_id.sign(&encoded_clap).unwrap(); - Pallet::::slow_clap(RawOrigin::None.into(), clap, signature)?; - Pallet::::trigger_nullification_for_benchmark(); + let signature = authority_id.sign(&clap.encode()) + .ok_or("couldn't make signature")?; - assert_eq!(<::Currency>::total_balance(&receiver), Default::default()); - assert_eq!(ApplausesForTransaction::::get(&storage_key), false); - - frame_system::Pallet::::on_initialize(1u32.into()); - - let mut fake_received_clap = - BoundedBTreeSet::::new(); - assert_eq!(fake_received_clap.try_insert(1).unwrap(), true); - pallet::ReceivedClaps::::insert(&next_storage_key, fake_received_clap); - }: _(RawOrigin::Signed(receiver_clone), network_id, session_index, transaction_hash, receiver_clone.clone(), amount) + }: _(RawOrigin::None, clap, signature) verify { + let clap_key = (session_index, transaction_hash, args_hash); + assert_eq!(ReceivedClaps::::get(&clap_key).get(&authority_index).is_some(), true); assert_eq!(<::Currency>::total_balance(&receiver), amount); - assert_eq!(ApplausesForTransaction::::get(&storage_key), true); + } + + commit_block { + let session_index = T::ValidatorSet::session_index(); + let network_id = NetworkIdOf::::default(); + + let authorities = vec![T::AuthorityId::generate_pair(None)]; + let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities.clone()) + .map_err(|()| "more than the maximum number of keys provided")?; + Authorities::::set(&session_index, bounded_authorities); + let authority_index = 0u32; + + let block_commitment = BlockCommitment { + session_index, + authority_index, + network_id, + commitment: CommitmentDetails { + last_registered_block: 69, + last_seen_block: 420, + last_updated: 1337, + } + }; + + let authority_id = authorities + .get(authority_index as usize) + .expect("first authority should exist"); + let signature = authority_id.sign(&block_commitment.encode()) + .ok_or("couldn't make signature")?; + + }: _(RawOrigin::None, block_commitment, signature) + verify { + let stored_commitment = BlockCommitments::::get(&network_id) + .get(&authority_index) + .cloned() + .unwrap_or_default(); + assert_eq!(stored_commitment.last_registered_block, 69); + assert_eq!(stored_commitment.last_seen_block, 420); + assert_eq!(stored_commitment.last_updated, 1337); } impl_benchmark_test_suite!( diff --git a/pallets/slow-clap/src/evm_types.rs b/pallets/slow-clap/src/evm_types.rs index 28a2104..8a04d0f 100644 --- a/pallets/slow-clap/src/evm_types.rs +++ b/pallets/slow-clap/src/evm_types.rs @@ -1,9 +1,14 @@ +use sp_runtime::SaturatedConversion; +use sp_staking::SessionIndex; + use crate::{ deserialisations::{ de_string_to_bytes, de_string_to_h256, de_string_to_u64, de_string_to_u64_pure, de_string_to_vec_of_bytes, }, - Decode, Deserialize, Encode, RuntimeDebug, Vec, H256, + AuthIndex, BalanceOf, BlockCommitment, BlockCommitments, Call, Clap, CommitmentDetails, Config, + Decode, Deserialize, Encode, NetworkIdOf, RuntimeAppPublic, RuntimeDebug, SubmitTransaction, + Vec, COMMITMENT_DELAY_MILLIS, H256, LOG_TARGET, }; const NUMBER_OF_TOPICS: usize = 3; @@ -40,6 +45,219 @@ pub struct Log { pub removed: bool, } +impl EvmResponseType { + fn prepare_block_commitment( + &self, + from_block: u64, + authority_index: AuthIndex, + session_index: SessionIndex, + network_id: NetworkIdOf, + ) -> BlockCommitment> { + let last_updated = sp_io::offchain::timestamp().unix_millis(); + let current_block = match self { + EvmResponseType::BlockNumber(block_number) => *block_number, + EvmResponseType::TransactionLogs(_) => Default::default(), + }; + + BlockCommitment { + session_index, + authority_index, + network_id, + commitment: CommitmentDetails { + last_registered_block: from_block, + last_seen_block: current_block, + last_updated, + }, + } + } + + fn prepare_clap( + &self, + authority_index: AuthIndex, + session_index: SessionIndex, + network_id: NetworkIdOf, + log: &Log, + ) -> Clap, BalanceOf> { + Clap { + authority_index, + session_index, + network_id, + removed: log.removed, + receiver: T::AccountId::decode(&mut &log.topics[1][0..32]) + .expect("32 bytes always construct an AccountId32"), + amount: u128::from_be_bytes( + log.topics[2][16..32] + .try_into() + .expect("amount is valid hex; qed"), + ) + .saturated_into::>(), + transaction_hash: log.transaction_hash.clone().expect("tx hash exists; qed"), + block_number: log.block_number.expect("block number exists; qed"), + } + } + + fn iter_claps_from_logs( + &self, + authority_index: AuthIndex, + session_index: SessionIndex, + network_id: NetworkIdOf, + ) -> Vec, BalanceOf>> { + match self { + EvmResponseType::TransactionLogs(evm_logs) => evm_logs + .iter() + .filter_map(move |log| { + log.is_sufficient().then(|| { + self.prepare_clap::(authority_index, session_index, network_id, log) + }) + }) + .collect(), + EvmResponseType::BlockNumber(_) => Vec::new(), + } + } + + fn sign_and_submit_claps( + &self, + authority_index: AuthIndex, + authority_key: T::AuthorityId, + session_index: SessionIndex, + network_id: NetworkIdOf, + ) { + let claps = self.iter_claps_from_logs::(authority_index, session_index, network_id); + let claps_len = claps.len(); + + log::info!( + target: LOG_TARGET, + "🧐 Found {:?} claps for network {:?}", + claps_len, + network_id, + ); + + for (clap_index, clap) in claps.iter().enumerate() { + let signature = match authority_key.sign(&clap.encode()) { + Some(signature) => signature, + None => { + log::info!( + target: LOG_TARGET, + "🧐 Clap #{} signing failed from authority #{:?} for network {:?}", + clap_index, + authority_index, + network_id, + ); + return; + } + }; + + let call = Call::slow_clap { + clap: clap.clone(), + signature, + }; + + if let Err(e) = + SubmitTransaction::>::submit_unsigned_transaction(call.into()) + { + log::info!( + target: LOG_TARGET, + "🧐 Failed to submit clap #{} from authority #{:?} for network {:?}: {:?}", + clap_index, + authority_index, + network_id, + e, + ); + } + } + } + + fn sign_and_submit_block_commitment( + &self, + from_block: u64, + authority_index: AuthIndex, + authority_key: T::AuthorityId, + session_index: SessionIndex, + network_id: NetworkIdOf, + ) { + let block_commitment = self.prepare_block_commitment::( + from_block, + authority_index, + session_index, + network_id, + ); + + let stored_last_updated = BlockCommitments::::get(&network_id) + .get(&authority_index) + .map(|details| details.last_updated) + .unwrap_or_default(); + + let current_last_updated = block_commitment + .commitment + .last_updated + .saturating_sub(COMMITMENT_DELAY_MILLIS); + + if current_last_updated < stored_last_updated { + return; + } + + log::info!( + target: LOG_TARGET, + "🧐 New block commitment from authority #{:?} for network {:?}", + authority_index, + network_id, + ); + + let signature = match authority_key.sign(&block_commitment.encode()) { + Some(signature) => signature, + None => { + log::info!( + target: LOG_TARGET, + "🧐 Block commitment signing failed from authority #{:?} for network {:?}", + authority_index, + network_id, + ); + return; + } + }; + + let call = Call::commit_block { + block_commitment, + signature, + }; + + if let Err(e) = SubmitTransaction::>::submit_unsigned_transaction(call.into()) { + log::info!( + target: LOG_TARGET, + "🧐 Failed to submit block commitment from authority #{:?} for network {:?}: {:?}", + authority_index, + network_id, + e, + ); + } + } + + pub fn sign_and_submit( + &self, + from_block: u64, + authority_index: AuthIndex, + authority_key: T::AuthorityId, + session_index: SessionIndex, + network_id: NetworkIdOf, + ) { + match self { + EvmResponseType::TransactionLogs(_) => self.sign_and_submit_claps::( + authority_index, + authority_key, + session_index, + network_id, + ), + EvmResponseType::BlockNumber(_) => self.sign_and_submit_block_commitment::( + from_block, + authority_index, + authority_key, + session_index, + network_id, + ), + } + } +} + impl Log { pub fn is_sufficient(&self) -> bool { self.transaction_hash.is_some() diff --git a/pallets/slow-clap/src/lib.rs b/pallets/slow-clap/src/lib.rs index 48b61b2..cfd33dc 100644 --- a/pallets/slow-clap/src/lib.rs +++ b/pallets/slow-clap/src/lib.rs @@ -28,7 +28,7 @@ use sp_runtime::{ HttpError, }, traits::{BlockNumberProvider, Convert, Saturating, TrailingZeroInput}, - Perbill, RuntimeAppPublic, RuntimeDebug, SaturatedConversion, + Perbill, RuntimeAppPublic, RuntimeDebug, }; use sp_staking::{ offence::{Kind, Offence, ReportOffence}, @@ -70,11 +70,43 @@ pub mod sr25519 { const LOG_TARGET: &str = "runtime::ghost-slow-clap"; const DB_PREFIX: &[u8] = b"slow_clap::"; +const MIN_LOCK_GUARD_PERIOD: u64 = 15_000; const FETCH_TIMEOUT_PERIOD: u64 = 3_000; const LOCK_BLOCK_EXPIRATION: u64 = 20; +const COMMITMENT_DELAY_MILLIS: u64 = 600_000; pub type AuthIndex = u32; +#[derive( + RuntimeDebug, + Default, + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, +)] +pub struct CommitmentDetails { + pub last_registered_block: u64, + pub last_seen_block: u64, + pub last_updated: u64, +} + +#[derive( + RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo, MaxEncodedLen, +)] +pub struct BlockCommitment { + pub session_index: SessionIndex, + pub authority_index: AuthIndex, + pub network_id: NetworkId, + pub commitment: CommitmentDetails, +} + #[derive( RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo, MaxEncodedLen, )] @@ -260,6 +292,10 @@ pub mod pallet { receiver: T::AccountId, received_amount: BalanceOf, }, + BlockCommited { + authority_id: AuthIndex, + network_id: NetworkIdOf, + }, } #[pallet::error] @@ -271,8 +307,20 @@ pub mod pallet { CouldNotAccumulateCommission, CouldNotAccumulateIncomingImbalance, CouldNotIncreaseGatekeeperAmount, + NonExistentAuthorityIndex, + TimeWentBackwards, } + #[pallet::storage] + #[pallet::getter(fn block_commitments)] + pub(super) type BlockCommitments = StorageMap< + _, + Twox64Concat, + NetworkIdOf, + BTreeMap, + ValueQuery, + >; + #[pallet::storage] #[pallet::getter(fn received_claps)] pub(super) type ReceivedClaps = StorageNMap< @@ -361,23 +409,17 @@ pub mod pallet { } #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::self_applause())] - pub fn self_applause( + #[pallet::weight((T::WeightInfo::commit_block(), DispatchClass::Normal, Pays::No))] + pub fn commit_block( origin: OriginFor, - network_id: NetworkIdOf, - prev_session_index: SessionIndex, - transaction_hash: H256, - receiver: T::AccountId, - amount: BalanceOf, + block_commitment: BlockCommitment>, + // since signature verification is done in `validate_unsigned` + // we can skip doing it here again. + _signature: ::Signature, ) -> DispatchResult { - let _ = ensure_signed(origin)?; - Self::applause_if_posible( - network_id, - prev_session_index, - transaction_hash, - receiver, - amount, - ) + ensure_none(origin)?; + Self::try_commit_block(&block_commitment)?; + Ok(()) } } @@ -398,39 +440,65 @@ pub mod pallet { #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; - fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { - if let Call::slow_clap { clap, signature } = call { - let (session_index, _) = Self::mended_session_index(&clap); - let authorities = Authorities::::get(&session_index); - let authority = match authorities.get(clap.authority_index as usize) { - Some(authority) => authority, - None => return InvalidTransaction::BadSigner.into(), - }; + match call { + Call::commit_block { + block_commitment, + signature, + } => { + let authorities = Authorities::::get(&block_commitment.session_index); + let authority = match authorities.get(block_commitment.authority_index as usize) + { + Some(authority) => authority, + None => return InvalidTransaction::BadSigner.into(), + }; - if ClapsInSession::::get(&session_index) - .get(&clap.authority_index) - .map(|info| info.disabled) - .unwrap_or_default() - { - return InvalidTransaction::BadSigner.into(); + let signature_valid = block_commitment.using_encoded(|encoded_commitment| { + authority.verify(&encoded_commitment, signature) + }); + + if !signature_valid { + return InvalidTransaction::BadProof.into(); + } + + ValidTransaction::with_tag_prefix("SlowClap") + .priority(T::UnsignedPriority::get()) + .and_provides(block_commitment.commitment.encode()) + .longevity(LOCK_BLOCK_EXPIRATION) + .propagate(true) + .build() } + Call::slow_clap { clap, signature } => { + let (session_index, _) = Self::mended_session_index(&clap); + let authorities = Authorities::::get(&session_index); + let authority = match authorities.get(clap.authority_index as usize) { + Some(authority) => authority, + None => return InvalidTransaction::BadSigner.into(), + }; - let signature_valid = - clap.using_encoded(|encoded_clap| authority.verify(&encoded_clap, signature)); + if ClapsInSession::::get(&session_index) + .get(&clap.authority_index) + .map(|info| info.disabled) + .unwrap_or_default() + { + return InvalidTransaction::BadSigner.into(); + } - if !signature_valid { - return InvalidTransaction::BadProof.into(); + let signature_valid = clap + .using_encoded(|encoded_clap| authority.verify(&encoded_clap, signature)); + + if !signature_valid { + return InvalidTransaction::BadProof.into(); + } + + ValidTransaction::with_tag_prefix("SlowClap") + .priority(T::UnsignedPriority::get()) + .and_provides(signature) + .longevity(LOCK_BLOCK_EXPIRATION) + .propagate(true) + .build() } - - ValidTransaction::with_tag_prefix("SlowClap") - .priority(T::UnsignedPriority::get()) - .and_provides(signature) - .longevity(LOCK_BLOCK_EXPIRATION) - .propagate(true) - .build() - } else { - InvalidTransaction::Call.into() + _ => InvalidTransaction::Call.into(), } } } @@ -618,79 +686,30 @@ impl Pallet { }) } - fn applause_if_posible( - network_id: NetworkIdOf, - prev_session_index: SessionIndex, - transaction_hash: H256, - receiver: T::AccountId, - amount: BalanceOf, - ) -> DispatchResult { - let curr_session_index = prev_session_index.saturating_add(1); - let clap_unique_hash = Self::generate_unique_hash(&receiver, &amount, &network_id); + fn try_commit_block(new_commitment: &BlockCommitment>) -> DispatchResult { + BlockCommitments::::try_mutate(&new_commitment.network_id, |current_commitments| { + let authority_index = new_commitment.authority_index; + let new_commitment_details = new_commitment.commitment; - let prev_authorities = Authorities::::get(&prev_session_index) - .into_iter() - .enumerate() - .map(|(i, auth)| (auth, i as AuthIndex)) - .collect::>(); - let curr_authorities = Authorities::::get(&curr_session_index); + let current_last_updated = current_commitments + .get(&authority_index) + .map(|details| details.last_updated) + .unwrap_or_default(); - let prev_received_claps_key = (prev_session_index, &transaction_hash, &clap_unique_hash); - let curr_received_claps_key = (curr_session_index, &transaction_hash, &clap_unique_hash); + ensure!( + new_commitment_details.last_updated > current_last_updated, + Error::::TimeWentBackwards + ); - let mut previous_claps = ClapsInSession::::get(&prev_session_index); - let mut total_received_claps = - ReceivedClaps::::get(&prev_received_claps_key).into_inner(); + current_commitments.insert(authority_index, new_commitment_details); - for (auth_index, info) in ClapsInSession::::get(&curr_session_index).iter() { - if !info.disabled { - continue; - } + Self::deposit_event(Event::::BlockCommited { + network_id: new_commitment.network_id, + authority_id: authority_index, + }); - if let Some(curr_authority) = curr_authorities.get(*auth_index as usize) { - if let Some(prev_position) = prev_authorities.get(&curr_authority) { - previous_claps - .entry(*prev_position as AuthIndex) - .and_modify(|individual| (*individual).disabled = true) - .or_insert(SessionAuthorityInfo { - claps: 0u32, - disabled: true, - }); - } - } - } - - for auth_index in ReceivedClaps::::get(&curr_received_claps_key).into_iter() { - if let Some(curr_authority) = curr_authorities.get(auth_index as usize) { - if let Some(prev_position) = prev_authorities.get(&curr_authority) { - let _ = total_received_claps.insert(*prev_position as AuthIndex); - } - } - } - - let disabled_authorities = previous_claps.values().filter(|info| info.disabled).count(); - - let active_authorities = prev_authorities.len().saturating_sub(disabled_authorities); - - let clap = Clap { - authority_index: Default::default(), - block_number: Default::default(), - removed: Default::default(), - session_index: Default::default(), - transaction_hash: Default::default(), - network_id, - receiver, - amount, - }; - - let enough_authorities = - Perbill::from_rational(total_received_claps.len() as u32, active_authorities as u32) - > Perbill::from_percent(T::ApplauseThreshold::get()); - - ensure!(enough_authorities, Error::::NotEnoughClaps); - Self::try_applause(&clap, &prev_received_claps_key)?; - - Ok(()) + Ok(()) + }) } fn start_slow_clapping(block_number: BlockNumberFor) -> OffchainResult { @@ -716,7 +735,7 @@ impl Pallet { let network_lock_key = Self::create_storage_key(b"network-lock-", &network_id_encoded); let block_until = - rt_offchain::Duration::from_millis(rate_limit_delay.max(FETCH_TIMEOUT_PERIOD)); + rt_offchain::Duration::from_millis(rate_limit_delay.max(MIN_LOCK_GUARD_PERIOD)); let mut network_lock = StorageLock::