1use crate::{pallet::OffchainIndexEventCount, Config, Event, Pallet, PublicKeyToMsaId};
2pub use common_primitives::msa::MessageSourceId;
3use 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
30const BLOCK_EVENT_KEY: &[u8] = b"frequency::block_event::msa::";
32const BLOCK_EVENT_FORK_AWARE_KEY: &[u8] = b"frequency::block_event_fork::msa::";
34const MAX_FORK_AWARE_BUCKET: u32 = 1000;
36const MAX_NUMBER_OF_STORAGE_CHECKS: u16 = 1000;
38const MSA_INITIAL_LOCK_TIMEOUT_EXPIRATION_MS: u64 = 6000;
40
41const MSA_INITIAL_LOCK_BLOCK_EXPIRATION_BLOCKS: u32 = 120;
43
44const MSA_INITIAL_LOCK_NAME: &[u8; 28] = b"Msa::ofw::initial-index-lock";
46
47pub const MSA_INITIAL_INDEXED_STORAGE_NAME: &[u8; 25] = b"Msa::ofw::initial-indexed";
49
50const LAST_PROCESSED_BLOCK_LOCK_NAME: &[u8; 35] = b"Msa::ofw::last-processed-block-lock";
52
53pub const LAST_PROCESSED_BLOCK_STORAGE_NAME: &[u8; 30] = b"Msa::ofw::last-processed-block";
55
56const LAST_PROCESSED_BLOCK_LOCK_TIMEOUT_EXPIRATION_MS: u64 = 5000;
58
59const LAST_PROCESSED_BLOCK_LOCK_BLOCK_EXPIRATION_BLOCKS: u32 = 20;
61
62const NUMBER_OF_PREVIOUS_BLOCKS_TO_CHECK: u32 = 5u32;
64
65const NUMBER_OF_BLOCKS_TO_EXPLORE: u32 = 1000;
67
68pub const HTTP_REQUEST_DEADLINE_MS: u64 = 2000;
70
71pub const RPC_FINALIZED_BLOCK_REQUEST_URL: &str = "http://localhost:9944";
74pub const RPC_FINALIZED_BLOCK_REQUEST_BODY: &[u8; 78] =
76 b"{\"id\": 10, \"jsonrpc\": \"2.0\", \"method\": \"chain_getFinalizedHead\", \"params\": []}";
77
78#[derive(
80 TypeInfo, RuntimeDebugNoBound, Clone, Decode, DecodeWithMemTracking, Encode, PartialEq, Eq,
81)]
82#[scale_info(skip_type_params(T))]
83pub enum OffchainReplayEvent<T: Config> {
84 MsaPallet(MsaOffchainReplayEvent<T>),
86}
87#[derive(
89 TypeInfo, RuntimeDebugNoBound, Clone, Decode, DecodeWithMemTracking, Encode, PartialEq, Eq,
90)]
91#[scale_info(skip_type_params(T))]
92pub enum MsaOffchainReplayEvent<T: Config> {
93 KeyReIndex {
95 msa_id: MessageSourceId,
97 index_key: Option<T::AccountId>,
99 },
100}
101
102#[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#[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_offchain_index(&event_key, event.clone());
127
128 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
136fn 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 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 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
182fn 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 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
217fn 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
225fn 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#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebugNoBound)]
239pub enum IndexedEvent<T: Config> {
240 IndexedMsaCreated {
242 msa_id: MessageSourceId,
244
245 key: T::AccountId,
247 },
248 IndexedPublicKeyAdded {
250 msa_id: MessageSourceId,
252
253 key: T::AccountId,
255 },
256 IndexedPublicKeyDeleted {
258 msa_id: MessageSourceId,
260 key: T::AccountId,
262 },
263 MsaIndexInvalidated {
265 msa_id: MessageSourceId,
267 },
268}
269
270impl<T: Config> IndexedEvent<T> {
271 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
286fn 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 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 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
330fn clean_offchain_events(storage_keys: &Vec<Vec<u8>>) {
332 for key in storage_keys {
333 offchain_index::clear(key);
334 }
335}
336
337fn reverse_map_msa_keys<T: Config>(block_number: BlockNumberFor<T>) -> bool {
340 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 let mut events_by_msa_id: BTreeMap<MessageSourceId, Vec<IndexedEvent<T>>> = BTreeMap::new();
353
354 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 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 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 },
423 }
424 }
425
426 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 },
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#[derive(Serialize, Deserialize, Encode, Decode, Default, Debug)]
449pub struct FinalizedBlockResponse {
450 pub result: String,
452}
453
454#[cfg(not(feature = "no-custom-host-functions"))]
456fn fetch_finalized_block_hash<T: Config>() -> Result<T::Hash, sp_runtime::offchain::http::Error> {
457 let rpc_address_bytes: Vec<u8> = if cfg!(feature = "runtime-benchmarks") {
460 RPC_FINALIZED_BLOCK_REQUEST_URL.into()
461 } else {
462 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 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 if response.code != 200 {
495 log::warn!("Unexpected status code: {}", response.code);
496 return Err(sp_runtime::offchain::http::Error::Unknown);
497 }
498
499 let body = response.body().collect::<Vec<u8>>();
503
504 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 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#[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 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#[allow(clippy::precedence)]
564pub fn get_bucket_number<T: Config>(event: &IndexedEvent<T>) -> u16 {
565 let hashed = Twox128::hash(&event.encode());
566 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}