Avoid Resource Intensive Execution Inside Hooks
Severity: Medium
Description
Performing resource-intensive operations, such as iterating over large data sets, within runtime hooks like on_finalize
can significantly impact block execution time and lead to performance bottlenecks. Hooks are executed automatically for every block, and if they contain computationally heavy tasks, they can reduce transaction throughput and degrade network performance, especially as on-chain activity scales.
What should be avoided
Avoid conducting heavy computations or iterating through extensive data in hooks, as this adds unnecessary workload to every block. For example, consider the following inefficient approach in on_finalize
, which processes votes for each proposal that ends within a block:
#![allow(unused)] fn main() { impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { fn on_finalize(block_number: T::BlockNumber) { // Retrieve proposals that have ended at this block number let proposals = EndedProposals::<T>::get(block_number); for proposal_id in proposals { let mut ayes = 0; let mut nays = 0; // Retrieve all votes for the current proposal let votes = Votes::<T>::get(proposal_id); for vote in votes { match vote { VoteType::Aye => ayes = ayes.saturating_add(1), VoteType::Nay => nays = nays.saturating_add(1) } } // Process `ayes` and `nays` results as needed } } } }
In this example:
- Counting votes for each finalized proposal during
on_finalize
leads to high resource usage and may exceed block weight limits, especially as the number of proposals and votes grows.
Best practice
Optimize by performing the calculations within the extrinsics, maintaining incremental counters in storage, or enabling users to trigger the logic explicitly outside the hooks.
Option 1: Perform the execution within the extrinsic
Track necessary values as they are submitted, distributing the computation workload across each transaction.
#![allow(unused)] fn main() { #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::some_vote())] pub fn some_vote( origin: OriginFor<T>, vote: VoteType ) -> DispatchResult { // Verification logic ProposalVoteAmount::<T>::mutate(id, |item| -> Result<(), Error> { match vote { VoteType::Aye => *item.ayes = item.ayes.saturating_add(1), VoteType::Nay => *item.nays = item.nays.saturating_add(1), } })?; Ok(()) } fn on_finalize(block_number: T::BlockNumber) { // Retrieve proposals that have ended at this block number let proposals = EndedProposals::<T>::get(block_number); for proposal_id in proposals { let (ayes, nays) = ProposalVoteAmount::<T>::get(proposal_id); // Process `ayes` and `nays` results as needed } } }
Option 2: Allow users to trigger the logic explicitly
Allow users to close/finalize manually by explicitly calling an extrinsic when necessary, which avoids performing the work automatically in on_finalize
.
#![allow(unused)] fn main() { #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::close_proposal())] pub fn close_proposal( origin: OriginFor<T>, proposal_id: u32 ) -> DispatchResult { let proposal = Proposals::<T>::get(proposal_id); ensure!(proposal.end_block < current_block, Error::<T>::ProposalHasNotEnded); let mut ayes = 0; let mut nays = 0; let votes = Votes::<T>::get(proposal_id); for vote in votes.iter() { match vote { VoteType::Aye => ayes = ayes.saturating_add(1), VoteType::Nay => nays = nays.saturating_add(1), } } // Process `ayes` and `nays` results as needed Ok(()) } }
These approaches shift heavy computations away from automatic hooks and ensure more efficient block execution while maintaining network performance.