pallet_msa/
offchain_storage.rs

1use crate::{pallet::OffchainIndexEventCount, Config, Event, Pallet, PublicKeyToMsaId};
2pub use common_primitives::msa::MessageSourceId;
3/// Offchain Storage for MSA
4use common_primitives::offchain::{
5	self as offchain_common, get_msa_account_lock_name, get_msa_account_storage_key_name,
6	LockStatus, MSA_ACCOUNT_LOCK_TIMEOUT_EXPIRATION_MS,
7};
8use frame_support::{RuntimeDebugNoBound, Twox128};
9use frame_system::pallet_prelude::BlockNumberFor;
10use parity_scale_codec::{Decode, Encode};
11use sp_core::serde::{Deserialize, Serialize};
12extern crate alloc;
13use alloc::{collections::btree_map::BTreeMap, string::String, vec, vec::Vec};
14use core::fmt::Debug;
15use frame_support::{
16	pallet_prelude::{DecodeWithMemTracking, TypeInfo},
17	StorageHasher,
18};
19use sp_io::offchain_index;
20use sp_runtime::{
21	offchain::{
22		storage::StorageValueRef,
23		storage_lock::{BlockAndTime, StorageLock, Time},
24		Duration,
25	},
26	traits::One,
27	Saturating,
28};
29
30/// Block event storage prefix
31const BLOCK_EVENT_KEY: &[u8] = b"frequency::block_event::msa::";
32/// Block event storage prefix for fork-aware events
33const BLOCK_EVENT_FORK_AWARE_KEY: &[u8] = b"frequency::block_event_fork::msa::";
34/// number of buckets to map the events for fork-aware storage
35const MAX_FORK_AWARE_BUCKET: u32 = 1000;
36/// max number of events to check from storage
37const MAX_NUMBER_OF_STORAGE_CHECKS: u16 = 1000;
38/// Lock expiration timeout in in milli-seconds for initial data import msa pallet
39const MSA_INITIAL_LOCK_TIMEOUT_EXPIRATION_MS: u64 = 6000;
40
41/// Lock expiration block for initial data import msa pallet
42const MSA_INITIAL_LOCK_BLOCK_EXPIRATION_BLOCKS: u32 = 120;
43
44/// Lock name for initial data index for msa pallet
45const MSA_INITIAL_LOCK_NAME: &[u8; 28] = b"Msa::ofw::initial-index-lock";
46
47/// storage name for initial data import storage
48pub const MSA_INITIAL_INDEXED_STORAGE_NAME: &[u8; 25] = b"Msa::ofw::initial-indexed";
49
50/// Lock name for last processed block number events
51const LAST_PROCESSED_BLOCK_LOCK_NAME: &[u8; 35] = b"Msa::ofw::last-processed-block-lock";
52
53/// lst processed block storage name
54pub const LAST_PROCESSED_BLOCK_STORAGE_NAME: &[u8; 30] = b"Msa::ofw::last-processed-block";
55
56/// Lock expiration timeout in milliseconds for last processed block
57const LAST_PROCESSED_BLOCK_LOCK_TIMEOUT_EXPIRATION_MS: u64 = 5000;
58
59/// Lock expiration for last processed block
60const LAST_PROCESSED_BLOCK_LOCK_BLOCK_EXPIRATION_BLOCKS: u32 = 20;
61
62/// number of previous blocks to check to mitigate offchain worker skips processing any block
63const NUMBER_OF_PREVIOUS_BLOCKS_TO_CHECK: u32 = 5u32;
64
65/// number of blocks to explore when trying to find the block number from block hash
66const NUMBER_OF_BLOCKS_TO_EXPLORE: u32 = 1000;
67
68/// HTTP request deadline in milliseconds
69pub const HTTP_REQUEST_DEADLINE_MS: u64 = 2000;
70
71/// LOCAL RPC URL and port
72/// warning: this should be updated if rpc port is set to anything different from 9944
73pub const RPC_FINALIZED_BLOCK_REQUEST_URL: &str = "http://localhost:9944";
74/// request body for getting last finalized block from rpc
75pub const RPC_FINALIZED_BLOCK_REQUEST_BODY: &[u8; 78] =
76	b"{\"id\": 10, \"jsonrpc\": \"2.0\", \"method\": \"chain_getFinalizedHead\", \"params\": []}";
77
78/// The overarching Offchain replay type that can allow replay of different events across different pallets
79#[derive(
80	TypeInfo, RuntimeDebugNoBound, Clone, Decode, DecodeWithMemTracking, Encode, PartialEq, Eq,
81)]
82#[scale_info(skip_type_params(T))]
83pub enum OffchainReplayEvent<T: Config> {
84	/// Msa pallet related replay event
85	MsaPallet(MsaOffchainReplayEvent<T>),
86}
87/// The Offchain replay type for Msa Pallet that can allow replay of different events
88#[derive(
89	TypeInfo, RuntimeDebugNoBound, Clone, Decode, DecodeWithMemTracking, Encode, PartialEq, Eq,
90)]
91#[scale_info(skip_type_params(T))]
92pub enum MsaOffchainReplayEvent<T: Config> {
93	/// Key re-indexing event
94	KeyReIndex {
95		/// Message Source Id that we like to reindex
96		msa_id: MessageSourceId,
97		/// optional key to index
98		index_key: Option<T::AccountId>,
99	},
100}
101
102/// offchain worker main execution function
103#[cfg(not(feature = "no-custom-host-functions"))]
104pub fn do_offchain_worker<T: Config>(block_number: BlockNumberFor<T>) {
105	if let Some(finalized_block_number) = get_finalized_block_number::<T>(block_number) {
106		match offchain_index_initial_state::<T>(finalized_block_number) {
107			LockStatus::Locked => {
108				log::info!("initiating-index is still locked in {block_number:?}");
109			},
110			LockStatus::Released => {
111				apply_offchain_events::<T>(finalized_block_number);
112			},
113		}
114	};
115}
116/// stores the event into offchain DB using offchain indexing
117#[cfg(not(feature = "no-custom-host-functions"))]
118pub fn offchain_index_event<T: Config>(event: Option<&Event<T>>, msa_id: MessageSourceId) {
119	if let Some(event) = IndexedEvent::map(event, msa_id) {
120		let block_number: u32 =
121			<frame_system::Pallet<T>>::block_number().try_into().unwrap_or_default();
122		let current_event_count: u16 = <OffchainIndexEventCount<T>>::get().saturating_add(1);
123		<OffchainIndexEventCount<T>>::put(current_event_count);
124		let event_key = get_indexed_event_key(block_number, current_event_count);
125		// set the event in offchain storage
126		set_offchain_index(&event_key, event.clone());
127
128		// to ensure we can handle the issues due to forking and overriding stored events we double
129		// index an event, and we choose to use or discard it on offchain worker side
130		let fork_aware_key = get_fork_aware_event_key(block_number, get_bucket_number(&event));
131
132		set_offchain_index(&fork_aware_key, event);
133	}
134}
135
136/// Offchain indexes all existing data in chain state
137/// returns the LockStatus
138fn offchain_index_initial_state<T: Config>(block_number: BlockNumberFor<T>) -> LockStatus {
139	let mut lock = StorageLock::<BlockAndTime<Pallet<T>>>::with_block_and_time_deadline(
140		MSA_INITIAL_LOCK_NAME,
141		MSA_INITIAL_LOCK_BLOCK_EXPIRATION_BLOCKS,
142		Duration::from_millis(MSA_INITIAL_LOCK_TIMEOUT_EXPIRATION_MS),
143	);
144	if let Ok(mut guard) = lock.try_lock() {
145		let processed_storage = StorageValueRef::persistent(MSA_INITIAL_INDEXED_STORAGE_NAME);
146		let is_initial_indexed = processed_storage.get::<bool>().unwrap_or(None);
147
148		if !is_initial_indexed.unwrap_or_default() {
149			log::info!("Msa::ofw::initial-indexed is {is_initial_indexed:?}");
150
151			// setting last processed block so we can start indexing from that block after
152			// initial index is done
153			init_last_processed_block::<T>(block_number);
154
155			let mut counter = 0u64;
156			for (account_id, msa_id) in PublicKeyToMsaId::<T>::iter() {
157				process_offchain_events::<T>(
158					msa_id,
159					vec![IndexedEvent::IndexedPublicKeyAdded { key: account_id, msa_id }],
160				);
161
162				// extend the initial index lock
163				counter += 1;
164				if counter.is_multiple_of(1000) {
165					log::info!("Added {counter} more keys!");
166					if guard.extend_lock().is_err() {
167						log::warn!("lock is expired in block {block_number:?}");
168						return LockStatus::Released;
169					}
170				}
171			}
172
173			processed_storage.set(&true);
174			log::info!("Finished adding {counter} keys!");
175		}
176	} else {
177		return LockStatus::Locked;
178	};
179	LockStatus::Released
180}
181
182/// apply offchain event into offchain DB
183fn apply_offchain_events<T: Config>(block_number: BlockNumberFor<T>) {
184	let mut lock = StorageLock::<BlockAndTime<Pallet<T>>>::with_block_and_time_deadline(
185		LAST_PROCESSED_BLOCK_LOCK_NAME,
186		LAST_PROCESSED_BLOCK_LOCK_BLOCK_EXPIRATION_BLOCKS,
187		Duration::from_millis(LAST_PROCESSED_BLOCK_LOCK_TIMEOUT_EXPIRATION_MS),
188	);
189
190	if let Ok(mut guard) = lock.try_lock() {
191		log::info!("processing events in {block_number:?}");
192
193		let last_processed_block_storage =
194			StorageValueRef::persistent(LAST_PROCESSED_BLOCK_STORAGE_NAME);
195		let default_starting_block_number = block_number
196			.saturating_sub(BlockNumberFor::<T>::from(NUMBER_OF_PREVIOUS_BLOCKS_TO_CHECK));
197		let mut start_block_number = last_processed_block_storage
198			.get::<BlockNumberFor<T>>()
199			.unwrap_or(Some(default_starting_block_number))
200			.unwrap_or(default_starting_block_number);
201
202		// since this is the last processed block number we already processed it and starting from the next one
203		start_block_number += BlockNumberFor::<T>::one();
204		while start_block_number <= block_number {
205			if reverse_map_msa_keys::<T>(start_block_number) && guard.extend_lock().is_err() {
206				log::warn!("last processed block lock is expired in block {block_number:?}");
207				break;
208			}
209			last_processed_block_storage.set(&start_block_number);
210			start_block_number += BlockNumberFor::<T>::one();
211		}
212	} else {
213		log::info!("skip processing events on {block_number:?} due to existing lock!");
214	};
215}
216
217/// Set offchain index value, used to store MSA Events to be process by offchain worker
218fn set_offchain_index<V>(key: &[u8], value: V)
219where
220	V: Encode + Clone + Decode + Eq + Debug,
221{
222	offchain_index::set(key, value.encode().as_slice());
223}
224
225/// Get offchain index value, used to store MSA Events to be process by offchain worker
226fn get_offchain_index<V>(key: &[u8]) -> Option<V>
227where
228	V: Encode + Clone + Decode + Eq + Debug,
229{
230	let value = offchain_common::get_index_value::<V>(key);
231	value.unwrap_or_else(|e| {
232		log::error!("Error getting offchain index value: {e:?}");
233		None
234	})
235}
236
237/// Offchain indexed compatible Event type
238#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebugNoBound)]
239pub enum IndexedEvent<T: Config> {
240	/// A new Message Service Account was created with a new MessageSourceId
241	IndexedMsaCreated {
242		/// The MSA for the Event
243		msa_id: MessageSourceId,
244
245		/// The key added to the MSA
246		key: T::AccountId,
247	},
248	/// An AccountId has been associated with a MessageSourceId
249	IndexedPublicKeyAdded {
250		/// The MSA for the Event
251		msa_id: MessageSourceId,
252
253		/// The key added to the MSA
254		key: T::AccountId,
255	},
256	/// An AccountId had all permissions revoked from its MessageSourceId
257	IndexedPublicKeyDeleted {
258		/// The MSA for the Event
259		msa_id: MessageSourceId,
260		/// The key no longer approved for the associated MSA
261		key: T::AccountId,
262	},
263	/// The offchain MSA->PubKey index has been marked invalid for the indicated MessageSourceId.
264	MsaIndexInvalidated {
265		/// The MSA for the Event
266		msa_id: MessageSourceId,
267	},
268}
269
270impl<T: Config> IndexedEvent<T> {
271	/// maps a pallet event to indexed event type
272	pub fn map(event: Option<&Event<T>>, event_msa_id: MessageSourceId) -> Option<Self> {
273		match event {
274			Some(Event::MsaCreated { msa_id, key }) =>
275				Some(Self::IndexedMsaCreated { msa_id: *msa_id, key: key.clone() }),
276			Some(Event::PublicKeyAdded { msa_id, key }) =>
277				Some(Self::IndexedPublicKeyAdded { msa_id: *msa_id, key: key.clone() }),
278			Some(Event::PublicKeyDeleted { key }) =>
279				Some(Self::IndexedPublicKeyDeleted { msa_id: event_msa_id, key: key.clone() }),
280			None => Some(Self::MsaIndexInvalidated { msa_id: event_msa_id }),
281			_ => None,
282		}
283	}
284}
285
286/// Initializes the last_process_block value in offchain DB
287fn init_last_processed_block<T: Config>(current_block_number: BlockNumberFor<T>) {
288	let mut last_processed_block_lock = StorageLock::<'_, Time>::with_deadline(
289		LAST_PROCESSED_BLOCK_LOCK_NAME,
290		Duration::from_millis(LAST_PROCESSED_BLOCK_LOCK_TIMEOUT_EXPIRATION_MS),
291	);
292	let _ = last_processed_block_lock.lock();
293	let last_processed_block_storage =
294		StorageValueRef::persistent(LAST_PROCESSED_BLOCK_STORAGE_NAME);
295
296	// setting current_block-1 as the last processed so that we start indexing from current_block
297	let target_block: BlockNumberFor<T> =
298		current_block_number.saturating_sub(BlockNumberFor::<T>::one());
299	last_processed_block_storage.set(&target_block);
300}
301
302fn read_offchain_events<T: Config>(
303	block_number: BlockNumberFor<T>,
304) -> Vec<(IndexedEvent<T>, Vec<u8>)> {
305	let current_block: u32 = block_number.try_into().unwrap_or_default();
306	let mut events = vec![];
307
308	for i in 1..=MAX_NUMBER_OF_STORAGE_CHECKS {
309		let key = get_indexed_event_key(current_block, i);
310		match get_offchain_index::<IndexedEvent<T>>(&key) {
311			Some(decoded_event) => {
312				events.push((decoded_event, key));
313			},
314			None => {
315				// no more events for this block
316				break;
317			},
318		}
319	}
320
321	for i in 1u16..=MAX_FORK_AWARE_BUCKET.try_into().unwrap_or_default() {
322		let key = get_fork_aware_event_key(current_block, i);
323		if let Some(decoded_event) = get_offchain_index::<IndexedEvent<T>>(&key) {
324			events.push((decoded_event, key));
325		}
326	}
327	events
328}
329
330/// cleans the events from offchain storage
331fn clean_offchain_events(storage_keys: &Vec<Vec<u8>>) {
332	for key in storage_keys {
333		offchain_index::clear(key);
334	}
335}
336
337/// offchain worker callback for indexing msa keys
338/// return true if there are events and false if not
339fn reverse_map_msa_keys<T: Config>(block_number: BlockNumberFor<T>) -> bool {
340	// read the events indexed for the current block
341	let events_to_process: Vec<(IndexedEvent<T>, Vec<u8>)> = read_offchain_events(block_number);
342	let events_exists = !events_to_process.is_empty();
343	if events_exists {
344		log::info!(
345			"found {} double indexed events for block {:?}",
346			events_to_process.len(),
347			block_number
348		);
349	}
350
351	// collect a replay of all events by MSA id
352	let mut events_by_msa_id: BTreeMap<MessageSourceId, Vec<IndexedEvent<T>>> = BTreeMap::new();
353
354	// collect relevant events
355	for (event, _) in events_to_process.iter() {
356		match event {
357			IndexedEvent::IndexedPublicKeyAdded { msa_id, .. } |
358			IndexedEvent::IndexedMsaCreated { msa_id, .. } |
359			IndexedEvent::IndexedPublicKeyDeleted { msa_id, .. } |
360			IndexedEvent::MsaIndexInvalidated { msa_id } => {
361				let events = events_by_msa_id.entry(*msa_id).or_default();
362				events.push(event.clone());
363			},
364		}
365	}
366
367	// process and save to offchain db
368	for (msa_id, events) in events_by_msa_id {
369		if !events.is_empty() {
370			process_offchain_events(msa_id, events);
371		}
372	}
373
374	if events_exists {
375		let storage_keys = events_to_process.iter().map(|(_, key)| key.clone()).collect();
376		clean_offchain_events(&storage_keys);
377	}
378
379	events_exists
380}
381
382fn process_offchain_events<T: Config>(msa_id: MessageSourceId, events: Vec<IndexedEvent<T>>) {
383	// Lock will specifically prevent multiple offchain workers from
384	// processing the same msa events at the same time
385	let msa_lock_name = get_msa_account_lock_name(msa_id);
386	let mut msa_lock = StorageLock::<'_, Time>::with_deadline(
387		&msa_lock_name,
388		Duration::from_millis(MSA_ACCOUNT_LOCK_TIMEOUT_EXPIRATION_MS),
389	);
390	let _lock = msa_lock.lock();
391	let msa_storage_name = get_msa_account_storage_key_name(msa_id);
392	let mut msa_storage = StorageValueRef::persistent(&msa_storage_name);
393
394	let mut msa_keys = msa_storage.get::<Vec<T::AccountId>>().unwrap_or(None).unwrap_or_default();
395	let mut old_msa_keys = msa_keys.clone();
396	let mut changed = false;
397
398	for event in events {
399		match &event {
400			IndexedEvent::IndexedPublicKeyAdded { key, .. } |
401			IndexedEvent::IndexedMsaCreated { key, .. } => {
402				if let Some(on_chain_msa_id) = PublicKeyToMsaId::<T>::get(key) {
403					if on_chain_msa_id != msa_id {
404						log::warn!(
405							"{key:?} forked onchain-MsaId={on_chain_msa_id:?}, forked-MsaId=={msa_id:?}",
406						);
407					} else if !msa_keys.contains(key) {
408						msa_keys.push(key.clone());
409						changed = true;
410					}
411				}
412			},
413			IndexedEvent::IndexedPublicKeyDeleted { key, .. } => {
414				if PublicKeyToMsaId::<T>::get(key).is_none() && msa_keys.contains(key) {
415					msa_keys.retain(|k| k != key);
416					old_msa_keys.retain(|k| k != key);
417					changed = true;
418				}
419			},
420			IndexedEvent::MsaIndexInvalidated { .. } => {
421				// nothing to do since we take care of removing extra keys for all events anyway
422			},
423		}
424	}
425
426	// check old keys to ensure they are valid
427	for old_key in &old_msa_keys {
428		match PublicKeyToMsaId::<T>::get(old_key) {
429			Some(on_chain_msa_id) if on_chain_msa_id == msa_id => {
430				// everything is as expected. Do nothing
431			},
432			_ => {
433				msa_keys.retain(|k| k != old_key);
434				changed = true;
435			},
436		}
437	}
438
439	if changed {
440		if msa_keys.len() > 0 {
441			msa_storage.set(&msa_keys);
442		} else {
443			msa_storage.clear();
444		}
445	}
446}
447/// Response type of rpc to get finalized block
448#[derive(Serialize, Deserialize, Encode, Decode, Default, Debug)]
449pub struct FinalizedBlockResponse {
450	/// Hex encoded hash of last finalized block
451	pub result: String,
452}
453
454/// fetches finalized block hash from rpc
455#[cfg(not(feature = "no-custom-host-functions"))]
456fn fetch_finalized_block_hash<T: Config>() -> Result<T::Hash, sp_runtime::offchain::http::Error> {
457	// we are not able to use the custom extension in benchmarks due to feature conflict
458	// Build rpc_address bytes (Vec<u8>) either from benchmarks constant or via custom extension
459	let rpc_address_bytes: Vec<u8> = if cfg!(feature = "runtime-benchmarks") {
460		RPC_FINALIZED_BLOCK_REQUEST_URL.into()
461	} else {
462		// call the runtime-interface function that fills our fixed buffer
463		let mut buffer = vec![0u8; 256];
464		let len = common_primitives::offchain::custom::get_val_buffered(&mut buffer);
465		if len == 0 {
466			RPC_FINALIZED_BLOCK_REQUEST_URL.into()
467		} else {
468			match Vec::<u8>::decode(&mut &buffer[..len as usize]) {
469				Ok(v) if !v.is_empty() => v,
470				_ => RPC_FINALIZED_BLOCK_REQUEST_URL.into(),
471			}
472		}
473	};
474	let url = core::str::from_utf8(&rpc_address_bytes)
475		.map_err(|_| sp_runtime::offchain::http::Error::Unknown)?;
476	// We want to keep the offchain worker execution time reasonable, so we set a hard-coded
477	// deadline to 2s to complete the external call.
478	// You can also wait indefinitely for the response, however you may still get a timeout
479	// coming from the host machine.
480	let deadline =
481		sp_io::offchain::timestamp().add(Duration::from_millis(HTTP_REQUEST_DEADLINE_MS));
482	let body = vec![RPC_FINALIZED_BLOCK_REQUEST_BODY];
483	let request = sp_runtime::offchain::http::Request::post(url, body);
484	let pending = request
485		.add_header("Content-Type", "application/json")
486		.deadline(deadline)
487		.send()
488		.map_err(|_| sp_runtime::offchain::http::Error::IoError)?;
489
490	let response = pending
491		.try_wait(deadline)
492		.map_err(|_| sp_runtime::offchain::http::Error::DeadlineReached)??;
493	// Let's check the status code before we proceed to reading the response.
494	if response.code != 200 {
495		log::warn!("Unexpected status code: {}", response.code);
496		return Err(sp_runtime::offchain::http::Error::Unknown);
497	}
498
499	// Next we want to fully read the response body and collect it to a vector of bytes.
500	// Note that the return object allows you to read the body in chunks as well
501	// with a way to control the deadline.
502	let body = response.body().collect::<Vec<u8>>();
503
504	// Create a str slice from the body.
505	let body_str = core::str::from_utf8(&body).map_err(|_| {
506		log::warn!("No UTF8 body");
507		sp_runtime::offchain::http::Error::Unknown
508	})?;
509
510	log::debug!("{body_str}");
511	let finalized_block_response: FinalizedBlockResponse =
512		serde_json::from_str(body_str).map_err(|_| sp_runtime::offchain::http::Error::Unknown)?;
513
514	// skipping 0x on front
515	let decoded_from_hex = hex::decode(&finalized_block_response.result[2..])
516		.map_err(|_| sp_runtime::offchain::http::Error::Unknown)?;
517
518	let val = T::Hash::decode(&mut &decoded_from_hex[..])
519		.map_err(|_| sp_runtime::offchain::http::Error::Unknown)?;
520	Ok(val)
521}
522
523/// fetch finalized block hash and convert it to block number
524#[cfg(not(feature = "no-custom-host-functions"))]
525fn get_finalized_block_number<T: Config>(
526	current_block: BlockNumberFor<T>,
527) -> Option<BlockNumberFor<T>> {
528	let mut finalized_block_number = None;
529	let last_finalized_hash = match fetch_finalized_block_hash::<T>() {
530		Ok(hash) => hash,
531		Err(e) => {
532			log::error!("failure to get the finalized hash {e:?}");
533			return finalized_block_number;
534		},
535	};
536
537	// iterates on imported blocks to find the block_number from block_hash
538	let mut current_block_number = current_block;
539	let last_block_number =
540		current_block.saturating_sub(BlockNumberFor::<T>::from(NUMBER_OF_BLOCKS_TO_EXPLORE));
541	while current_block_number > last_block_number {
542		if last_finalized_hash == frame_system::Pallet::<T>::block_hash(current_block_number) {
543			finalized_block_number = Some(current_block_number);
544			break;
545		}
546		current_block_number.saturating_dec();
547	}
548
549	match finalized_block_number {
550		None => {
551			log::error!(
552				"Not able to find any imported block with {last_finalized_hash:?} hash and {current_block:?} block",
553			);
554		},
555		Some(inner) => {
556			log::info!("last finalized block number {inner:?} and hash {last_finalized_hash:?}",);
557		},
558	}
559	finalized_block_number
560}
561
562/// converts an event to a number between [1, `MAX_FORK_AWARE_BUCKET`]
563#[allow(clippy::precedence)]
564pub fn get_bucket_number<T: Config>(event: &IndexedEvent<T>) -> u16 {
565	let hashed = Twox128::hash(&event.encode());
566	// Directly combine the first 4 bytes into a u32 using shifts and bitwise OR
567	let num = (hashed[0] as u32) << 24 |
568		(hashed[1] as u32) << 16 |
569		(hashed[2] as u32) << 8 |
570		(hashed[3] as u32);
571
572	((num % MAX_FORK_AWARE_BUCKET) + 1u32) as u16
573}
574
575fn get_fork_aware_event_key(block_number: u32, event_index: u16) -> Vec<u8> {
576	[BLOCK_EVENT_FORK_AWARE_KEY, block_number.encode().as_slice(), event_index.encode().as_slice()]
577		.concat()
578}
579
580fn get_indexed_event_key(block_number: u32, event_index: u16) -> Vec<u8> {
581	[BLOCK_EVENT_KEY, block_number.encode().as_slice(), event_index.encode().as_slice()].concat()
582}