From 3d5c167a123a899fd522812b4d22ac06be290f20 Mon Sep 17 00:00:00 2001 From: ancient_mage Date: Mon, 4 Nov 2024 13:22:40 +0100 Subject: [PATCH 1/4] NDEV-3112. Optimize getting of deactivated features --- evm_loader/lib/src/rpc/db_call_client.rs | 66 +++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/evm_loader/lib/src/rpc/db_call_client.rs b/evm_loader/lib/src/rpc/db_call_client.rs index 5664b09b3..4dc303f02 100644 --- a/evm_loader/lib/src/rpc/db_call_client.rs +++ b/evm_loader/lib/src/rpc/db_call_client.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use super::{e, Rpc, SliceConfig}; use crate::types::{TracerDb, TracerDbTrait}; use crate::NeonError; @@ -88,6 +90,68 @@ impl Rpc for CallDbClient { } async fn get_deactivated_solana_features(&self) -> ClientResult> { - Ok(vec![]) // TODO + use std::time::{Duration, Instant}; + use tokio::sync::Mutex; + + struct Cache { + // feature to slot when activated, if not then None + data: HashMap>, + timestamp: Instant, + } + + static CACHE: Mutex> = Mutex::const_new(None); + let mut cache = CACHE.lock().await; + + if let Some(cache) = cache.as_ref() { + if cache.timestamp.elapsed() < Duration::from_secs(24 * 60 * 60) { + let mut keys: Vec = cache.data.keys().copied().collect(); + + keys.retain(|pubkey| { + let value = cache.data.get(pubkey); + + if let Some(Some(slot)) = value { + if slot <= &self.slot { + return false; + } + } + + true + }); + + return Ok(keys); + } + } + + let feature_keys: Vec = solana_sdk::feature_set::FEATURE_NAMES + .keys() + .copied() + .collect(); + + let features = Rpc::get_multiple_accounts(self, &feature_keys).await?; + + let mut result = HashMap::>::new(); + for (pubkey, feature) in feature_keys.iter().zip(features) { + let slot = feature + .and_then(|a| solana_sdk::feature::from_account(&a)) + .and_then(|f| f.activated_at); + + result.insert(*pubkey, slot); + } + + cache.replace(Cache { + data: result.clone(), + timestamp: Instant::now(), + }); + drop(cache); + + Ok(result + .into_iter() + .filter_map(|(pubkey, slot)| { + if slot.is_none() { + return Some(pubkey); + } + None + }) + .collect()) } } From 6062b31a14319b9e37360fb5cfd4d1d88417846e Mon Sep 17 00:00:00 2001 From: Dzmitry Zdanovich Date: Tue, 10 Dec 2024 15:01:45 +0100 Subject: [PATCH 2/4] NDEV-3112. Fix incorrect logic --- evm_loader/lib/src/rpc/db_call_client.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/evm_loader/lib/src/rpc/db_call_client.rs b/evm_loader/lib/src/rpc/db_call_client.rs index 4dc303f02..6b25f9aac 100644 --- a/evm_loader/lib/src/rpc/db_call_client.rs +++ b/evm_loader/lib/src/rpc/db_call_client.rs @@ -127,7 +127,19 @@ impl Rpc for CallDbClient { .copied() .collect(); - let features = Rpc::get_multiple_accounts(self, &feature_keys).await?; + let tracer_db = self.tracer_db.clone(); + let slot = tracer_db + .get_latest_block() + .await + .map_err(|e| e!("get_latest_block error", e))?; + + let self_rpc: Self = Self { + tracer_db, + slot, + tx_index_in_block: None, + }; + + let features = Rpc::get_multiple_accounts(&self_rpc, &feature_keys).await?; let mut result = HashMap::>::new(); for (pubkey, feature) in feature_keys.iter().zip(features) { From e8149ccb03426001d6ddbcbcdc69fa076081e7f6 Mon Sep 17 00:00:00 2001 From: Dzmitry Zdanovich Date: Mon, 16 Dec 2024 13:27:26 +0100 Subject: [PATCH 3/4] NDEV-3112. Make unique cache for deactivated features --- evm_loader/Cargo.lock | 1 + evm_loader/lib/Cargo.toml | 1 + evm_loader/lib/src/rpc/db_call_client.rs | 79 +----------------- evm_loader/lib/src/rpc/validator_client.rs | 50 +---------- .../lib/src/types/deactivated_features.rs | 82 +++++++++++++++++++ evm_loader/lib/src/types/mod.rs | 4 +- 6 files changed, 90 insertions(+), 127 deletions(-) create mode 100644 evm_loader/lib/src/types/deactivated_features.rs diff --git a/evm_loader/Cargo.lock b/evm_loader/Cargo.lock index 83934a862..675b7bb16 100644 --- a/evm_loader/Cargo.lock +++ b/evm_loader/Cargo.lock @@ -3618,6 +3618,7 @@ dependencies = [ "lazy_static", "log", "neon-lib-interface", + "once_cell", "rand 0.8.5", "scroll 0.12.0", "serde", diff --git a/evm_loader/lib/Cargo.toml b/evm_loader/lib/Cargo.toml index 76a2afd41..8c339d684 100644 --- a/evm_loader/lib/Cargo.toml +++ b/evm_loader/lib/Cargo.toml @@ -53,6 +53,7 @@ lazy_static = "1.5.0" elsa = "1.10.0" arrayref = "0.3.8" futures = "0.3.30" +once_cell = "1.19.0" [dev-dependencies] hex-literal = "0.4.1" diff --git a/evm_loader/lib/src/rpc/db_call_client.rs b/evm_loader/lib/src/rpc/db_call_client.rs index 6b25f9aac..c0771f306 100644 --- a/evm_loader/lib/src/rpc/db_call_client.rs +++ b/evm_loader/lib/src/rpc/db_call_client.rs @@ -1,6 +1,5 @@ -use std::collections::HashMap; - use super::{e, Rpc, SliceConfig}; +use crate::types::deactivated_features::get_deactivated_features; use crate::types::{TracerDb, TracerDbTrait}; use crate::NeonError; use crate::NeonError::RocksDb; @@ -90,80 +89,6 @@ impl Rpc for CallDbClient { } async fn get_deactivated_solana_features(&self) -> ClientResult> { - use std::time::{Duration, Instant}; - use tokio::sync::Mutex; - - struct Cache { - // feature to slot when activated, if not then None - data: HashMap>, - timestamp: Instant, - } - - static CACHE: Mutex> = Mutex::const_new(None); - let mut cache = CACHE.lock().await; - - if let Some(cache) = cache.as_ref() { - if cache.timestamp.elapsed() < Duration::from_secs(24 * 60 * 60) { - let mut keys: Vec = cache.data.keys().copied().collect(); - - keys.retain(|pubkey| { - let value = cache.data.get(pubkey); - - if let Some(Some(slot)) = value { - if slot <= &self.slot { - return false; - } - } - - true - }); - - return Ok(keys); - } - } - - let feature_keys: Vec = solana_sdk::feature_set::FEATURE_NAMES - .keys() - .copied() - .collect(); - - let tracer_db = self.tracer_db.clone(); - let slot = tracer_db - .get_latest_block() - .await - .map_err(|e| e!("get_latest_block error", e))?; - - let self_rpc: Self = Self { - tracer_db, - slot, - tx_index_in_block: None, - }; - - let features = Rpc::get_multiple_accounts(&self_rpc, &feature_keys).await?; - - let mut result = HashMap::>::new(); - for (pubkey, feature) in feature_keys.iter().zip(features) { - let slot = feature - .and_then(|a| solana_sdk::feature::from_account(&a)) - .and_then(|f| f.activated_at); - - result.insert(*pubkey, slot); - } - - cache.replace(Cache { - data: result.clone(), - timestamp: Instant::now(), - }); - drop(cache); - - Ok(result - .into_iter() - .filter_map(|(pubkey, slot)| { - if slot.is_none() { - return Some(pubkey); - } - None - }) - .collect()) + get_deactivated_features(self, Some(self.slot)).await } } diff --git a/evm_loader/lib/src/rpc/validator_client.rs b/evm_loader/lib/src/rpc/validator_client.rs index e8a0585c9..232d26772 100644 --- a/evm_loader/lib/src/rpc/validator_client.rs +++ b/evm_loader/lib/src/rpc/validator_client.rs @@ -1,4 +1,4 @@ -use crate::{config::APIOptions, Config}; +use crate::{config::APIOptions, types::deactivated_features::get_deactivated_features, Config}; use super::{Rpc, SliceConfig}; use async_trait::async_trait; @@ -179,52 +179,6 @@ impl Rpc for CloneRpcClient { } async fn get_deactivated_solana_features(&self) -> ClientResult> { - use std::time::{Duration, Instant}; - use tokio::sync::Mutex; - - struct Cache { - data: Vec, - timestamp: Instant, - } - - static CACHE: Mutex> = Mutex::const_new(None); - let mut cache = CACHE.lock().await; - - if let Some(cache) = cache.as_ref() { - if cache.timestamp.elapsed() < Duration::from_secs(24 * 60 * 60) { - return Ok(cache.data.clone()); - } - } - - let feature_keys: Vec = solana_sdk::feature_set::FEATURE_NAMES - .keys() - .copied() - .collect(); - - let features = Rpc::get_multiple_accounts(self, &feature_keys).await?; - - let mut result = Vec::with_capacity(feature_keys.len()); - for (pubkey, feature) in feature_keys.iter().zip(features) { - let is_activated = feature - .and_then(|a| solana_sdk::feature::from_account(&a)) - .and_then(|f| f.activated_at) - .is_some(); - - if !is_activated { - result.push(*pubkey); - } - } - - for feature in &result { - debug!("Deactivated feature: {}", feature); - } - - cache.replace(Cache { - data: result.clone(), - timestamp: Instant::now(), - }); - drop(cache); - - Ok(result) + get_deactivated_features(self, None).await } } diff --git a/evm_loader/lib/src/types/deactivated_features.rs b/evm_loader/lib/src/types/deactivated_features.rs new file mode 100644 index 000000000..8544e9489 --- /dev/null +++ b/evm_loader/lib/src/types/deactivated_features.rs @@ -0,0 +1,82 @@ +// use crate::tracing::tracers::state_diff::Account; +use crate::rpc::Rpc; +use once_cell::sync::Lazy; +use solana_client::client_error::Result as ClientResult; +use solana_sdk::pubkey::Pubkey; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +static DEACTIVATED_FEATURES_UPDATE_SLOTS: u64 = 24 * 60 * 60; // approximately each 12h + +#[derive(Debug, Eq, PartialEq, Clone, Default)] +struct DeactivatedFeaturesCache { + // feature to slot when activated, if not then None + pub data: HashMap>, + pub last_slot: u64, +} + +impl DeactivatedFeaturesCache { + fn should_update(&self, slot: u64) -> bool { + self.last_slot >= DEACTIVATED_FEATURES_UPDATE_SLOTS + slot + } +} + +static DEACTIVATED_FEATURES: Lazy>> = + Lazy::new(|| Arc::new(Mutex::new(DeactivatedFeaturesCache::default()))); + +async fn get_multiple_features(rpc: &dyn Rpc) -> ClientResult>> { + let feature_keys: Vec = solana_sdk::feature_set::FEATURE_NAMES + .keys() + .copied() + .collect(); + + let features = Rpc::get_multiple_accounts(rpc, &feature_keys).await?; + + let mut result = HashMap::>::new(); + for (pubkey, feature) in feature_keys.iter().zip(features) { + let slot = feature + .and_then(|a| solana_sdk::feature::from_account(&a)) + .and_then(|f| f.activated_at); + + result.insert(*pubkey, slot); + } + + Ok(result) +} + +async fn deactivated_features_get_instance( + rpc: &dyn Rpc, +) -> ClientResult { + let mut cache = DEACTIVATED_FEATURES.lock().await; + + let rpc_slot = rpc.get_slot().await?; + if cache.should_update(rpc_slot) { + cache.last_slot = rpc_slot; + cache.data = get_multiple_features(rpc).await?; + } + + Ok((*cache).clone()) +} + +pub async fn get_deactivated_features( + rpc: &dyn Rpc, + slot: Option, +) -> ClientResult> { + let features = deactivated_features_get_instance(rpc).await?; + + Ok(features + .data + .into_iter() + .filter_map(|(pubkey, activated_at)| match (slot, activated_at) { + (_, None) => Some(pubkey), + (Some(slot), Some(activated_at)) => { + if activated_at > slot { + return Some(pubkey); + } + None + } + (None, Some(_)) => None, + }) + .collect()) +} diff --git a/evm_loader/lib/src/types/mod.rs b/evm_loader/lib/src/types/mod.rs index f4ce03697..9b2f4ac0b 100644 --- a/evm_loader/lib/src/types/mod.rs +++ b/evm_loader/lib/src/types/mod.rs @@ -1,6 +1,6 @@ -pub mod tracer_ch_common; - +pub mod deactivated_features; pub mod programs_cache; +pub mod tracer_ch_common; pub(crate) mod tracer_ch_db; pub mod tracer_rocks_db; From 092e98c09cc2f367db3f87b6af56550d1a9c8be6 Mon Sep 17 00:00:00 2001 From: Dzmitry Zdanovich Date: Fri, 20 Dec 2024 18:14:23 +0100 Subject: [PATCH 4/4] NDEV-3112. Fix bug, add test for deactivated_features --- evm_loader/api/src/main.rs | 3 + evm_loader/cli/src/main.rs | 33 ++--- .../lib/src/deactivated_features_tests.rs | 130 ++++++++++++++++++ evm_loader/lib/src/rpc/db_call_client.rs | 4 +- evm_loader/lib/src/rpc/validator_client.rs | 6 +- .../lib/src/types/deactivated_features.rs | 76 +++++++--- 6 files changed, 205 insertions(+), 47 deletions(-) create mode 100644 evm_loader/lib/src/deactivated_features_tests.rs diff --git a/evm_loader/api/src/main.rs b/evm_loader/api/src/main.rs index 6076d0301..679b3b9e7 100644 --- a/evm_loader/api/src/main.rs +++ b/evm_loader/api/src/main.rs @@ -14,6 +14,7 @@ pub use neon_lib::commands; pub use neon_lib::config; pub use neon_lib::errors; pub use neon_lib::types; +pub use neon_lib::types::deactivated_features::set_deactivated_features_rpc; use tracing_appender::non_blocking::NonBlockingBuilder; use actix_request_identifier::RequestIdentifier; @@ -58,6 +59,8 @@ async fn main() -> NeonApiResult<()> { let api_config = config::load_api_config_from_environment(); let state: NeonApiState = Data::new(State::new(api_config).await); + set_deactivated_features_rpc(state.rpc_client.clone()).await; + let listener_addr = options .value_of("host") .map(std::borrow::ToOwned::to_owned) diff --git a/evm_loader/cli/src/main.rs b/evm_loader/cli/src/main.rs index f6a26dddc..018e2c0b2 100644 --- a/evm_loader/cli/src/main.rs +++ b/evm_loader/cli/src/main.rs @@ -14,7 +14,7 @@ use neon_lib::{ get_storage_at, init_environment, trace, }, rpc::CloneRpcClient, - types::{BalanceAddress, EmulateRequest}, + types::{deactivated_features::set_deactivated_features_rpc, BalanceAddress, EmulateRequest}, Config, }; @@ -42,10 +42,15 @@ type NeonCliResult = Result; async fn run(options: &ArgMatches<'_>) -> NeonCliResult { let config = &config::create(options)?; + let rpc = build_rpc(options, config).await?; + + set_deactivated_features_rpc(RpcEnum::CloneRpcClient(CloneRpcClient::new_from_config( + config, + ))) + .await; + match options.subcommand() { ("emulate", Some(_)) => { - let rpc = build_rpc(options, config).await?; - let request = read_tx_from_stdin()?; emulate::execute( &rpc, @@ -58,16 +63,12 @@ async fn run(options: &ArgMatches<'_>) -> NeonCliResult { .map(|(result, _)| json!(result)) } ("trace", Some(_)) => { - let rpc = build_rpc(options, config).await?; - let request = read_tx_from_stdin()?; trace::trace_transaction(&rpc, &config.db_config, &config.evm_loader, request) .await .map(|trace| json!(trace)) } ("get-ether-account-data", Some(params)) => { - let rpc = build_rpc(options, config).await?; - let address = address_of(params, "ether").unwrap(); let chain_id = value_of(params, "chain_id").unwrap(); @@ -79,8 +80,6 @@ async fn run(options: &ArgMatches<'_>) -> NeonCliResult { .map(|result| json!(result)) } ("get-contract-account-data", Some(params)) => { - let rpc = build_rpc(options, config).await?; - let account = address_of(params, "address").unwrap(); let accounts = std::slice::from_ref(&account); @@ -89,8 +88,6 @@ async fn run(options: &ArgMatches<'_>) -> NeonCliResult { .map(|result| json!(result)) } ("get-holder-account-data", Some(params)) => { - let rpc = build_rpc(options, config).await?; - let account = pubkey_of(params, "account").unwrap(); get_holder::execute(&rpc, &config.evm_loader, account) @@ -98,8 +95,6 @@ async fn run(options: &ArgMatches<'_>) -> NeonCliResult { .map(|result| json!(result)) } ("neon-elf-params", Some(params)) => { - let rpc = build_rpc(options, config).await?; - let program_location = params.value_of("program_location"); get_neon_elf::execute(config, &rpc, program_location) .await @@ -135,8 +130,6 @@ async fn run(options: &ArgMatches<'_>) -> NeonCliResult { .map(|result| json!(result)) } ("get-storage-at", Some(params)) => { - let rpc = build_rpc(options, config).await?; - let contract_id = address_of(params, "contract_id").expect("contract_it parse error"); let index = u256_of(params, "index").expect("index parse error"); @@ -144,13 +137,9 @@ async fn run(options: &ArgMatches<'_>) -> NeonCliResult { .await .map(|hash| json!(hex::encode(hash.0))) } - ("config", Some(_)) => { - let rpc = build_rpc(options, config).await?; - - get_config::execute(&rpc, config.evm_loader) - .await - .map(|result| json!(result)) - } + ("config", Some(_)) => get_config::execute(&rpc, config.evm_loader) + .await + .map(|result| json!(result)), _ => unreachable!(), } } diff --git a/evm_loader/lib/src/deactivated_features_tests.rs b/evm_loader/lib/src/deactivated_features_tests.rs new file mode 100644 index 000000000..e290f03bf --- /dev/null +++ b/evm_loader/lib/src/deactivated_features_tests.rs @@ -0,0 +1,130 @@ +mod deactivated_features_tests { + use std::collections::HashMap; + + use async_trait::async_trait; + use solana_account_decoder::UiDataSliceConfig as SliceConfig; + use solana_client::client_error::Result as ClientResult; + use solana_sdk::{ + account::{Account, AccountSharedData}, + clock::{Slot, UnixTimestamp}, + feature::Feature, + feature_set, + pubkey::Pubkey, + }; + + use crate::{ + rpc::Rpc, + types::deactivated_features::{ + get_deactivated_features_at_slot, set_deactivated_features_rpc, + }, + }; + + #[derive(Clone)] + struct RpcMockFeatures { + pub features: HashMap>, + } + + impl Default for RpcMockFeatures { + fn default() -> Self { + let accounts: Vec<_> = feature_set::FEATURE_NAMES.keys().copied().collect(); + + let mut features = HashMap::>::new(); + + for (slot, account) in accounts.iter().enumerate() { + let slot = if slot == 0 { None } else { Some(slot as u64) }; + features.insert(*account, slot); + } + + Self { features } + } + } + + #[async_trait(?Send)] + impl Rpc for RpcMockFeatures { + async fn get_account_slice( + &self, + _key: &Pubkey, + _slice: Option, + ) -> ClientResult> { + todo!() + } + + async fn get_multiple_accounts( + &self, + pubkeys: &[Pubkey], + ) -> ClientResult>> { + let mut result: Vec> = vec![]; + + for pubkey in pubkeys.iter() { + let feature = self.features.get(pubkey); + + match feature { + Some(slot) => { + let mut data = + AccountSharedData::new(1_000_000_000, 100, &solana_sdk::feature::ID); + if solana_sdk::feature::to_account( + &Feature { + activated_at: *slot, + }, + &mut data, + ) + .is_some() + { + result.push(Some(data.into())); + } else { + result.push(None) + } + } + None => result.push(None), + } + } + + Ok(result) + } + + async fn get_block_time(&self, _slot: Slot) -> ClientResult { + todo!() + } + + async fn get_slot(&self) -> ClientResult { + todo!() + } + + async fn get_deactivated_solana_features(&self) -> ClientResult> { + todo!() + } + } + + async fn get_deactivated_features_and_wait(slot: Option) -> Vec { + get_deactivated_features_at_slot(slot) + .await + .expect("get deactivated features failed at slot unexpectedly") + } + + // by default? mock rpc contains vector of features: RpcMockFeatures::features + // features[0] is not activated + // features[1] is activated at slot 1 + // features[i] is activated at slot i + // let we have N features overall, then... + // only 1 feature will be deactivated on slot N (features[0]) + // only 2 features will be deactivated on slot N-1 (features[0], features[n-1] (last one) which should be activated at slot N) + // ... + // all features will be deactivated on slot 0 + #[tokio::test] + async fn test_deactivated_features_cache() { + let rpc = RpcMockFeatures::default(); + let features_cnt = rpc.features.len() as u64; + set_deactivated_features_rpc(rpc.clone()).await; + + for i in 0..features_cnt { + let features = get_deactivated_features_and_wait(Some(i)).await; + + assert_eq!(features_cnt - i, features.len() as u64); + } + { + // current slot + let features = get_deactivated_features_and_wait(None).await; + assert_eq!(features.len(), 1); + } + } +} diff --git a/evm_loader/lib/src/rpc/db_call_client.rs b/evm_loader/lib/src/rpc/db_call_client.rs index c0771f306..2b160e572 100644 --- a/evm_loader/lib/src/rpc/db_call_client.rs +++ b/evm_loader/lib/src/rpc/db_call_client.rs @@ -1,5 +1,5 @@ use super::{e, Rpc, SliceConfig}; -use crate::types::deactivated_features::get_deactivated_features; +use crate::types::deactivated_features::get_deactivated_features_at_slot; use crate::types::{TracerDb, TracerDbTrait}; use crate::NeonError; use crate::NeonError::RocksDb; @@ -89,6 +89,6 @@ impl Rpc for CallDbClient { } async fn get_deactivated_solana_features(&self) -> ClientResult> { - get_deactivated_features(self, Some(self.slot)).await + get_deactivated_features_at_slot(Some(self.slot)).await } } diff --git a/evm_loader/lib/src/rpc/validator_client.rs b/evm_loader/lib/src/rpc/validator_client.rs index 232d26772..850f4c150 100644 --- a/evm_loader/lib/src/rpc/validator_client.rs +++ b/evm_loader/lib/src/rpc/validator_client.rs @@ -1,4 +1,6 @@ -use crate::{config::APIOptions, types::deactivated_features::get_deactivated_features, Config}; +use crate::{ + config::APIOptions, types::deactivated_features::get_deactivated_features_at_slot, Config, +}; use super::{Rpc, SliceConfig}; use async_trait::async_trait; @@ -179,6 +181,6 @@ impl Rpc for CloneRpcClient { } async fn get_deactivated_solana_features(&self) -> ClientResult> { - get_deactivated_features(self, None).await + get_deactivated_features_at_slot(None).await } } diff --git a/evm_loader/lib/src/types/deactivated_features.rs b/evm_loader/lib/src/types/deactivated_features.rs index 8544e9489..dd26eaf8e 100644 --- a/evm_loader/lib/src/types/deactivated_features.rs +++ b/evm_loader/lib/src/types/deactivated_features.rs @@ -5,27 +5,60 @@ use solana_client::client_error::Result as ClientResult; use solana_sdk::pubkey::Pubkey; use std::collections::HashMap; use std::sync::Arc; +use std::time::{Duration, Instant}; use tokio::sync::Mutex; -static DEACTIVATED_FEATURES_UPDATE_SLOTS: u64 = 24 * 60 * 60; // approximately each 12h +static DEACTIVATED_FEATURES_PERIOD: Duration = Duration::from_secs(60 * 60); // 1 hour + +pub trait BoxRpc: Rpc + Sync + Send {} +impl BoxRpc for T {} -#[derive(Debug, Eq, PartialEq, Clone, Default)] struct DeactivatedFeaturesCache { // feature to slot when activated, if not then None pub data: HashMap>, - pub last_slot: u64, + pub last_update: Instant, + pub rpc: Option>, +} + +impl Default for DeactivatedFeaturesCache { + fn default() -> Self { + Self { + data: HashMap::new(), + last_update: Instant::now() + .checked_sub(DEACTIVATED_FEATURES_PERIOD) + .unwrap(), + rpc: None, + } + } } impl DeactivatedFeaturesCache { - fn should_update(&self, slot: u64) -> bool { - self.last_slot >= DEACTIVATED_FEATURES_UPDATE_SLOTS + slot + pub fn should_update(&mut self) -> bool { + if self.rpc.is_some() { + self.last_update + DEACTIVATED_FEATURES_PERIOD <= Instant::now() + } else { + false + } + } + + pub fn set_deactivated_features_rpc(&mut self, rpc: impl BoxRpc + 'static) { + self.rpc = Some(Box::new(rpc)); + } + + pub async fn update(&mut self) -> ClientResult<()> { + if let Some(ref rpc) = self.rpc { + self.last_update = Instant::now(); + self.data = get_multiple_features(rpc.as_ref()).await?; + } + + Ok(()) } } static DEACTIVATED_FEATURES: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(DeactivatedFeaturesCache::default()))); -async fn get_multiple_features(rpc: &dyn Rpc) -> ClientResult>> { +async fn get_multiple_features(rpc: &dyn BoxRpc) -> ClientResult>> { let feature_keys: Vec = solana_sdk::feature_set::FEATURE_NAMES .keys() .copied() @@ -45,28 +78,20 @@ async fn get_multiple_features(rpc: &dyn Rpc) -> ClientResult ClientResult { +async fn get_deactivated_features() -> ClientResult>> { let mut cache = DEACTIVATED_FEATURES.lock().await; - let rpc_slot = rpc.get_slot().await?; - if cache.should_update(rpc_slot) { - cache.last_slot = rpc_slot; - cache.data = get_multiple_features(rpc).await?; - } + if cache.should_update() { + cache.update().await?; + }; - Ok((*cache).clone()) + Ok(cache.data.clone()) } -pub async fn get_deactivated_features( - rpc: &dyn Rpc, - slot: Option, -) -> ClientResult> { - let features = deactivated_features_get_instance(rpc).await?; +pub async fn get_deactivated_features_at_slot(slot: Option) -> ClientResult> { + let features = get_deactivated_features().await?; Ok(features - .data .into_iter() .filter_map(|(pubkey, activated_at)| match (slot, activated_at) { (_, None) => Some(pubkey), @@ -80,3 +105,12 @@ pub async fn get_deactivated_features( }) .collect()) } + +pub async fn set_deactivated_features_rpc(rpc: impl BoxRpc + 'static) { + let mut cache = DEACTIVATED_FEATURES.lock().await; + cache.set_deactivated_features_rpc(rpc); +} + +#[cfg(test)] +#[path = "../deactivated_features_tests.rs"] +mod deactivated_features_tests;