use std::collections::BTreeMap; use std::future::Future; use std::{sync::Arc, time::Duration}; use backon::{BackoffBuilder, ExponentialBuilder}; use mpc_primitives::hash::{LauncherDockerComposeHash, NodeImageHash}; use near_account_id::AccountId; use near_mpc_contract_interface::types::{ChainEntry, ForeignChain, NodeId}; use tokio::sync::watch; use crate::indexer::IndexerState; const ALLOWED_HASHES_REFRESH_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1); const MIN_BACKOFF_DURATION: Duration = Duration::from_secs(1); const MAX_BACKOFF_DURATION: Duration = Duration::from_secs(60); const TEE_ACCOUNTS_REFRESH_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1); const FOREIGN_CHAIN_PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(300); async fn monitor_allowed_hashes( sender: watch::Sender, indexer_state: Arc, get_mpc_allowed_hashes: &Fetcher, ) where T: PartialEq, Fetcher: Fn(AccountId) -> FetcherResponseFuture + Send + Sync, FetcherResponseFuture: Future> + Send, { let fetch_allowed_hashes = { let indexer_state = indexer_state.clone(); async move || { let mut backoff = ExponentialBuilder::default() .with_min_delay(MIN_BACKOFF_DURATION) .with_max_delay(MAX_BACKOFF_DURATION) .without_max_times() .with_jitter() .build(); loop { match get_mpc_allowed_hashes(indexer_state.mpc_contract_id.clone()).await { Ok((_block_height, allowed_hashes)) => { break allowed_hashes; } Err(e) => { let error_msg = format!("{:?}", e); if error_msg.contains( "wasm execution failed with error: MethodResolveError(MethodNotFound)", ) { tracing::info!(target: "mpc", "method not found in contract: {error_msg}"); } else { tracing::error!(target: "mpc", "error reading tee state from chain: {error_msg}"); } let backoff_duration = backoff.next().unwrap_or(MAX_BACKOFF_DURATION); tokio::time::sleep(backoff_duration).await; continue; } }; } } }; tracing::debug!(target: "indexer", "awaiting full sync to read mpc contract state"); indexer_state.client.wait_for_full_sync().await; loop { tokio::time::sleep(ALLOWED_HASHES_REFRESH_INTERVAL).await; let allowed_hashes = fetch_allowed_hashes().await; sender.send_if_modified(|previous_allowed_hashes| { if *previous_allowed_hashes != allowed_hashes { *previous_allowed_hashes = allowed_hashes; true } else { false } }); } } /// This future waits for the indexer to fully sync, and returns /// a [`watch::Receiver`] that will be continuously updated with the latest /// allowed [`AllowedDockerImageHash`]es when a change is detected /// on the MPC smart contract. pub async fn monitor_allowed_docker_images( sender: watch::Sender>, indexer_state: Arc, ) { let view_client = indexer_state.view_client.clone(); let fetcher = { |id| view_client.get_mpc_allowed_image_hashes(id) }; monitor_allowed_hashes(sender, indexer_state, &fetcher).await } /// This future waits for the indexer to fully sync, and returns /// a [`watch::Receiver`] that will be continuously updated with the /// allowed [`LauncherDockerComposeHash`]es when a change is detected /// on the MPC smart contract. pub async fn monitor_allowed_launcher_compose_hashes( sender: watch::Sender>, indexer_state: Arc, ) { let view_client = indexer_state.view_client.clone(); let fetcher = { |id| view_client.get_mpc_allowed_launcher_compose_hashes(id) }; monitor_allowed_hashes(sender, indexer_state, &fetcher).await } /// Fetches TEE accounts from the contract with retry logic. async fn fetch_tee_accounts_with_retry(indexer_state: &IndexerState) -> Vec { let mut backoff = ExponentialBuilder::default() .with_min_delay(MIN_BACKOFF_DURATION) .with_max_delay(MAX_BACKOFF_DURATION) .without_max_times() .with_jitter() .build(); loop { match indexer_state .view_client .get_mpc_tee_accounts(indexer_state.mpc_contract_id.clone()) .await { Ok((_block_height, tee_accounts)) => return tee_accounts, Err(e) => { tracing::error!(target: "mpc", "error reading TEE accounts from chain: {:?}", e); let backoff_duration = backoff.next().unwrap_or(MAX_BACKOFF_DURATION); tokio::time::sleep(backoff_duration).await; } } } } /// Monitor TEE accounts stored in the contract and update the watch channel when changes are detected. pub async fn monitor_tee_accounts( sender: watch::Sender>, indexer_state: Arc, ) { indexer_state.client.wait_for_full_sync().await; loop { let tee_accounts = fetch_tee_accounts_with_retry(&indexer_state).await; sender.send_if_modified(|previous_tee_accounts| { if *previous_tee_accounts != tee_accounts { *previous_tee_accounts = tee_accounts; true } else { false } }); tokio::time::sleep(TEE_ACCOUNTS_REFRESH_INTERVAL).await; } } /// Fetches the allowed foreign-chain providers whitelist from the contract with retry logic. async fn fetch_allowed_foreign_chain_providers_with_retry( indexer_state: &IndexerState, ) -> BTreeMap { let mut backoff = ExponentialBuilder::default() .with_min_delay(MIN_BACKOFF_DURATION) .with_max_delay(MAX_BACKOFF_DURATION) .without_max_times() .with_jitter() .build(); loop { match indexer_state .view_client .get_allowed_foreign_chain_providers(indexer_state.mpc_contract_id.clone()) .await { Ok(whitelist) => return whitelist, Err(e) => { tracing::error!(target: "mpc", "error reading allowed_foreign_chain_providers from chain: {:?}", e); let backoff_duration = backoff.next().unwrap_or(MAX_BACKOFF_DURATION); tokio::time::sleep(backoff_duration).await; } } } } /// Monitor the allowed foreign-chain providers whitelist stored in the contract and update the /// watch channel when changes are detected. Consumed by /// `crate::foreign_chain_whitelist_verifier::run`. pub async fn monitor_allowed_foreign_chain_providers( sender: watch::Sender>, indexer_state: Arc, ) { indexer_state.client.wait_for_full_sync().await; loop { let whitelist = fetch_allowed_foreign_chain_providers_with_retry(&indexer_state).await; sender.send_if_modified(|previous| { if *previous != whitelist { *previous = whitelist; true } else { false } }); tokio::time::sleep(FOREIGN_CHAIN_PROVIDERS_REFRESH_INTERVAL).await; } }