Skip to main content

iota_json_rpc/
governance_api.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{cmp::max, collections::BTreeMap, fmt::Debug, sync::Arc};
6
7use async_trait::async_trait;
8use cached::{SizedCache, proc_macro::cached};
9use iota_core::authority::AuthorityState;
10use iota_json_rpc_api::{
11    GovernanceReadApiOpenRpc, GovernanceReadApiServer, JsonRpcMetrics, error_object_from_rpc,
12};
13use iota_json_rpc_types::{
14    DelegatedStake, DelegatedTimelockedStake, IotaCommittee,
15    IotaSystemStateSummary as IotaSystemStateSummarySchema,
16    IotaSystemStateSummaryV1 as IotaSystemStateSummaryV1Schema, Stake, StakeStatus,
17    TimelockedStake, ValidatorApy, ValidatorApys,
18};
19use iota_metrics::{spawn_monitored_blocking, spawn_monitored_task};
20use iota_open_rpc::Module;
21use iota_sdk_move_types::iota_framework::object::ID;
22use iota_sdk_types::{Address, ObjectId};
23use iota_types::{
24    MoveTypeTagTrait,
25    committee::EpochId,
26    dynamic_field::{DynamicFieldInfo, get_dynamic_field_from_store},
27    error::{IotaError, UserInputError},
28    governance::StakedIota,
29    iota_serde::BigInt,
30    iota_system_state::{
31        IotaSystemState, IotaSystemStateTrait, PoolTokenExchangeRate, get_validator_from_table,
32        iota_system_state_summary::{IotaSystemStateSummaryV1, IotaSystemStateSummaryV2},
33    },
34    object::{Object, ObjectRead},
35    timelock::timelocked_staked_iota::TimelockedStakedIota,
36};
37use itertools::Itertools;
38use jsonrpsee::{RpcModule, core::RpcResult};
39use serde::{Serialize, de::DeserializeOwned};
40use statrs::statistics::{Data, Median};
41use tokio::sync::{OwnedSemaphorePermit, Semaphore};
42use tracing::{info, instrument};
43
44use crate::{
45    IotaRpcModule, ObjectProvider,
46    authority_state::StateRead,
47    error::{Error, IotaRpcInputError, RpcInterimResult},
48    logger::FutureWithTracing as _,
49};
50
51type ValidatorTable = (Address, ObjectId, ObjectId, u64, bool);
52
53/// Maximum number of stake reads that run at once. Each one reads every object
54/// the address owns on a thread of the shared blocking pool.
55const MAX_CONCURRENT_STAKE_READS: usize = 8;
56
57#[derive(Clone)]
58pub struct GovernanceReadApi {
59    state: Arc<dyn StateRead>,
60    pub metrics: Arc<JsonRpcMetrics>,
61    stake_read_permits: Arc<Semaphore>,
62}
63
64impl GovernanceReadApi {
65    pub fn new(state: Arc<AuthorityState>, metrics: Arc<JsonRpcMetrics>) -> Self {
66        Self {
67            state,
68            metrics,
69            stake_read_permits: Arc::new(Semaphore::new(MAX_CONCURRENT_STAKE_READS)),
70        }
71    }
72
73    /// Waits until fewer than [`MAX_CONCURRENT_STAKE_READS`] stake reads run.
74    async fn acquire_stake_read_permit(&self) -> Result<OwnedSemaphorePermit, Error> {
75        self.stake_read_permits
76            .clone()
77            .acquire_owned()
78            .await
79            .map_err(|e| Error::Internal(e.into()))
80    }
81
82    async fn get_staked_iota(&self, owner: Address) -> Result<Vec<StakedIota>, Error> {
83        let state = self.state.clone();
84        let permit = self.acquire_stake_read_permit().await?;
85        let result = spawn_monitored_blocking!(move || {
86            let _permit = permit;
87            state.get_staked_iota(owner)
88        })
89        .await??;
90
91        self.metrics
92            .get_stake_iota_result_size
93            .observe(result.len() as f64);
94        self.metrics
95            .get_stake_iota_result_size_total
96            .inc_by(result.len() as u64);
97        Ok(result)
98    }
99
100    async fn get_timelocked_staked_iota(
101        &self,
102        owner: Address,
103    ) -> Result<Vec<TimelockedStakedIota>, Error> {
104        let state = self.state.clone();
105        let permit = self.acquire_stake_read_permit().await?;
106        let result = spawn_monitored_blocking!(move || {
107            let _permit = permit;
108            state.get_timelocked_staked_iota(owner)
109        })
110        .await??;
111
112        self.metrics
113            .get_stake_iota_result_size
114            .observe(result.len() as f64);
115        self.metrics
116            .get_stake_iota_result_size_total
117            .inc_by(result.len() as u64);
118        Ok(result)
119    }
120
121    async fn get_stakes_by_ids(
122        &self,
123        staked_iota_ids: Vec<ObjectId>,
124    ) -> Result<Vec<DelegatedStake>, Error> {
125        let state = self.state.clone();
126        let stakes_read = spawn_monitored_task!(async move {
127            staked_iota_ids
128                .iter()
129                .map(|id| state.get_object_read(id))
130                .collect::<Result<Vec<_>, _>>()
131        })
132        .await??;
133
134        if stakes_read.is_empty() {
135            return Ok(vec![]);
136        }
137
138        let stakes: Vec<(StakedIota, bool)> = self
139            .stakes_with_status(stakes_read.into_iter())
140            .await?
141            .into_iter()
142            .map(|(o, b)| StakedIota::try_from(&o).map(|stake| (stake, b)))
143            .collect::<Result<_, _>>()?;
144
145        self.get_delegated_stakes(stakes).await
146    }
147
148    async fn get_stakes(&self, owner: Address) -> Result<Vec<DelegatedStake>, Error> {
149        let timer = self.metrics.get_stake_iota_latency.start_timer();
150        let stakes = self.get_staked_iota(owner).await?;
151        if stakes.is_empty() {
152            return Ok(vec![]);
153        }
154        drop(timer);
155
156        let _timer = self.metrics.get_delegated_iota_latency.start_timer();
157
158        let self_clone = self.clone();
159        spawn_monitored_task!(
160            self_clone.get_delegated_stakes(stakes.into_iter().map(|s| (s, true)).collect())
161        )
162        .await?
163    }
164
165    async fn get_timelocked_stakes_by_ids(
166        &self,
167        timelocked_staked_iota_ids: Vec<ObjectId>,
168    ) -> Result<Vec<DelegatedTimelockedStake>, Error> {
169        let state = self.state.clone();
170        let stakes_read = spawn_monitored_task!(async move {
171            timelocked_staked_iota_ids
172                .iter()
173                .map(|id| state.get_object_read(id))
174                .collect::<Result<Vec<_>, _>>()
175        })
176        .await??;
177
178        if stakes_read.is_empty() {
179            return Ok(vec![]);
180        }
181
182        let stakes: Vec<(TimelockedStakedIota, bool)> = self
183            .stakes_with_status(stakes_read.into_iter())
184            .await?
185            .into_iter()
186            .map(|(o, b)| TimelockedStakedIota::try_from(&o).map(|stake| (stake, b)))
187            .collect::<Result<_, _>>()?;
188
189        self.get_delegated_timelocked_stakes(stakes).await
190    }
191
192    async fn get_timelocked_stakes(
193        &self,
194        owner: Address,
195    ) -> Result<Vec<DelegatedTimelockedStake>, Error> {
196        let timer = self.metrics.get_stake_iota_latency.start_timer();
197        let stakes = self.get_timelocked_staked_iota(owner).await?;
198        if stakes.is_empty() {
199            return Ok(vec![]);
200        }
201        drop(timer);
202
203        let _timer = self.metrics.get_delegated_iota_latency.start_timer();
204
205        let self_clone = self.clone();
206        spawn_monitored_task!(
207            self_clone
208                .get_delegated_timelocked_stakes(stakes.into_iter().map(|s| (s, true)).collect())
209        )
210        .await?
211    }
212
213    async fn get_delegated_stakes(
214        &self,
215        stakes: Vec<(StakedIota, bool)>,
216    ) -> Result<Vec<DelegatedStake>, Error> {
217        let pools = stakes.into_iter().fold(
218            BTreeMap::<_, Vec<_>>::new(),
219            |mut pools, (stake, exists)| {
220                pools
221                    .entry(stake.pool_id())
222                    .or_default()
223                    .push((stake, exists));
224                pools
225            },
226        );
227
228        let system_state = self.get_system_state()?;
229        let system_state_summary = IotaSystemStateSummaryV2::try_from(
230            system_state.clone().into_iota_system_state_summary(),
231        )?;
232
233        let rates = exchange_rates(&self.state, system_state_summary.epoch)
234            .await?
235            .into_iter()
236            // Try to find for any candidate validator exchange rate
237            .chain(candidate_validators_exchange_rate(&self.state)?)
238            // Try to find for any pending validator exchange rate
239            .chain(pending_validators_exchange_rate(&self.state)?)
240            .map(|rates| (rates.pool_id, rates))
241            .collect::<BTreeMap<_, _>>();
242
243        let mut delegated_stakes = vec![];
244        for (pool_id, stakes) in pools {
245            // Rate table and rate can be null when the pool is not active
246            let rate_table = rates.get(&pool_id).ok_or_else(|| {
247                IotaRpcInputError::GenericNotFound(format!(
248                    "Cannot find rates for staking pool {pool_id}"
249                ))
250            })?;
251            let current_rate = rate_table.rates.first().map(|(_, rate)| rate);
252
253            let mut delegations = vec![];
254            for (stake, exists) in stakes {
255                let status = stake_status(
256                    system_state_summary.epoch,
257                    stake.activation_epoch(),
258                    stake.principal(),
259                    exists,
260                    current_rate,
261                    rate_table,
262                );
263                delegations.push(Stake {
264                    staked_iota_id: stake.id(),
265                    // TODO: this might change when we implement warm up period.
266                    stake_request_epoch: stake.activation_epoch() - 1,
267                    stake_active_epoch: stake.activation_epoch(),
268                    principal: stake.principal(),
269                    status,
270                })
271            }
272            delegated_stakes.push(DelegatedStake {
273                validator_address: rate_table.address,
274                staking_pool: pool_id,
275                stakes: delegations,
276            })
277        }
278        Ok(delegated_stakes)
279    }
280
281    async fn get_delegated_timelocked_stakes(
282        &self,
283        stakes: Vec<(TimelockedStakedIota, bool)>,
284    ) -> Result<Vec<DelegatedTimelockedStake>, Error> {
285        let pools = stakes.into_iter().fold(
286            BTreeMap::<_, Vec<_>>::new(),
287            |mut pools, (stake, exists)| {
288                pools
289                    .entry(stake.pool_id())
290                    .or_default()
291                    .push((stake, exists));
292                pools
293            },
294        );
295
296        let system_state = self.get_system_state()?;
297        let system_state_summary = IotaSystemStateSummaryV2::try_from(
298            system_state.clone().into_iota_system_state_summary(),
299        )?;
300
301        let rates = exchange_rates(&self.state, system_state_summary.epoch)
302            .await?
303            .into_iter()
304            // Try to find for any candidate validator exchange rate
305            .chain(candidate_validators_exchange_rate(&self.state)?)
306            // Try to find for any pending validator exchange rate
307            .chain(pending_validators_exchange_rate(&self.state)?)
308            .map(|rates| (rates.pool_id, rates))
309            .collect::<BTreeMap<_, _>>();
310
311        let mut delegated_stakes = vec![];
312        for (pool_id, stakes) in pools {
313            // Rate table and rate can be null when the pool is not active
314            let rate_table = rates.get(&pool_id).ok_or_else(|| {
315                IotaRpcInputError::GenericNotFound(format!(
316                    "Cannot find rates for staking pool {pool_id}"
317                ))
318            })?;
319            let current_rate = rate_table.rates.first().map(|(_, rate)| rate);
320
321            let mut delegations = vec![];
322            for (stake, exists) in stakes {
323                let status = stake_status(
324                    system_state_summary.epoch,
325                    stake.activation_epoch(),
326                    stake.principal(),
327                    exists,
328                    current_rate,
329                    rate_table,
330                );
331                delegations.push(TimelockedStake {
332                    timelocked_staked_iota_id: stake.id(),
333                    // TODO: this might change when we implement warm up period.
334                    stake_request_epoch: stake.activation_epoch() - 1,
335                    stake_active_epoch: stake.activation_epoch(),
336                    principal: stake.principal(),
337                    status,
338                    expiration_timestamp_ms: stake.expiration_timestamp_ms(),
339                    label: stake.label().clone(),
340                })
341            }
342            delegated_stakes.push(DelegatedTimelockedStake {
343                validator_address: rate_table.address,
344                staking_pool: pool_id,
345                stakes: delegations,
346            })
347        }
348        Ok(delegated_stakes)
349    }
350
351    async fn stakes_with_status(
352        &self,
353        iter: impl Iterator<Item = ObjectRead>,
354    ) -> Result<Vec<(Object, bool)>, Error> {
355        let mut stakes = vec![];
356
357        for stake in iter {
358            match stake {
359                ObjectRead::Exists(_, o, _) => stakes.push((o, true)),
360                ObjectRead::Deleted(object_ref) => {
361                    let Some(o) = self
362                        .state
363                        .find_object_lt_or_eq_version(
364                            &object_ref.object_id,
365                            &object_ref.version.previous().unwrap(),
366                        )
367                        .await?
368                    else {
369                        Err(IotaRpcInputError::UserInput(
370                            UserInputError::ObjectNotFound {
371                                object_id: object_ref.object_id,
372                                version: None,
373                            },
374                        ))?
375                    };
376                    stakes.push((o, false));
377                }
378                ObjectRead::NotExists(id) => Err(IotaRpcInputError::UserInput(
379                    UserInputError::ObjectNotFound {
380                        object_id: id,
381                        version: None,
382                    },
383                ))?,
384            }
385        }
386
387        Ok(stakes)
388    }
389
390    fn get_system_state(&self) -> Result<IotaSystemState, Error> {
391        Ok(self.state.get_system_state()?)
392    }
393}
394
395#[async_trait]
396impl GovernanceReadApiServer for GovernanceReadApi {
397    #[instrument(skip(self, staked_iota_ids), fields(staked_iota_ids = staked_iota_ids.iter().map(|id| id.to_string()).collect::<Vec<String>>().join(", ")))]
398    async fn get_stakes_by_ids(
399        &self,
400        staked_iota_ids: Vec<ObjectId>,
401    ) -> RpcResult<Vec<DelegatedStake>> {
402        self.get_stakes_by_ids(staked_iota_ids).trace().await
403    }
404
405    #[instrument(skip(self, owner), fields(owner = %owner))]
406    async fn get_stakes(&self, owner: Address) -> RpcResult<Vec<DelegatedStake>> {
407        self.get_stakes(owner).trace().await
408    }
409
410    #[instrument(skip(self, timelocked_staked_iota_ids), fields(timelocked_staked_iota_ids = timelocked_staked_iota_ids.iter().map(|id| id.to_string()).collect::<Vec<String>>().join(", ")))]
411    async fn get_timelocked_stakes_by_ids(
412        &self,
413        timelocked_staked_iota_ids: Vec<ObjectId>,
414    ) -> RpcResult<Vec<DelegatedTimelockedStake>> {
415        self.get_timelocked_stakes_by_ids(timelocked_staked_iota_ids)
416            .trace()
417            .await
418    }
419
420    #[instrument(skip(self, owner), fields(owner = %owner))]
421    async fn get_timelocked_stakes(
422        &self,
423        owner: Address,
424    ) -> RpcResult<Vec<DelegatedTimelockedStake>> {
425        self.get_timelocked_stakes(owner).trace().await
426    }
427
428    #[instrument(skip(self))]
429    async fn get_committee_info(&self, epoch: Option<BigInt<u64>>) -> RpcResult<IotaCommittee> {
430        async move {
431            self.state
432                .get_or_latest_committee(epoch)
433                .map(|committee| committee.into())
434                .map_err(Error::from)
435        }
436        .trace()
437        .await
438    }
439
440    #[instrument(skip(self))]
441    async fn get_latest_iota_system_state_v2(&self) -> RpcResult<IotaSystemStateSummarySchema> {
442        async move {
443            Ok(self
444                .state
445                .get_system_state()?
446                .into_iota_system_state_summary()
447                .try_into()?)
448        }
449        .trace()
450        .await
451    }
452
453    #[instrument(skip(self))]
454    async fn get_latest_iota_system_state(&self) -> RpcResult<IotaSystemStateSummaryV1Schema> {
455        async move {
456            Ok(IotaSystemStateSummaryV1::try_from(
457                self.state
458                    .get_system_state()?
459                    .into_iota_system_state_summary(),
460            )?
461            .into())
462        }
463        .trace()
464        .await
465    }
466
467    #[instrument(skip(self))]
468    async fn get_reference_gas_price(&self) -> RpcResult<BigInt<u64>> {
469        async move {
470            let epoch_store = self.state.load_epoch_store_one_call_per_task();
471            Ok(epoch_store.reference_gas_price().into())
472        }
473        .trace()
474        .await
475    }
476
477    #[instrument(skip(self))]
478    async fn get_validators_apy(&self) -> RpcResult<ValidatorApys> {
479        info!("get_validator_apy");
480        let system_state_summary = self.get_latest_iota_system_state().await?;
481
482        let exchange_rate_table = exchange_rates(&self.state, system_state_summary.epoch)
483            .await
484            .map_err(|e| error_object_from_rpc(e.into()))?;
485
486        let apys = calculate_apys(exchange_rate_table);
487
488        Ok(ValidatorApys {
489            apys,
490            epoch: system_state_summary.epoch,
491        })
492    }
493}
494
495pub fn calculate_apys(exchange_rate_table: Vec<ValidatorExchangeRates>) -> Vec<ValidatorApy> {
496    let mut apys = vec![];
497
498    for rates in exchange_rate_table.into_iter().filter(|r| r.active) {
499        let exchange_rates = rates.rates.iter().map(|(_, rate)| rate);
500
501        let mean_apy = mean_apy_from_exchange_rates(exchange_rates);
502        apys.push(ValidatorApy {
503            address: rates.address,
504            apy: mean_apy,
505        });
506    }
507    apys
508}
509
510/// Calculate the APY using a 7-epoch moving average.
511///
512/// Returns the Mean by default, but falls back to the Median if outliers are
513/// detected. Outliers are defined as any APY > `MAX_VALID_APY` (100%) or if the
514/// trailing 8th epoch exchange rate is missing. This fallback protects against
515/// skewed results caused by large staking events or the spikes seen after
516/// missing exchange rates.
517pub fn mean_apy_from_exchange_rates<'er>(
518    exchange_rates: impl DoubleEndedIterator<Item = &'er PoolTokenExchangeRate> + Clone,
519) -> f64 {
520    // We set this value after observing the APY of validators in mainnet.
521    const MAX_VALID_APY: f64 = 1.00;
522    const SAMPLES: usize = 7;
523
524    let rates = exchange_rates.clone().dropping(1);
525    let rates_next = exchange_rates.dropping_back(1);
526
527    let mut apys = rates
528        .zip(rates_next)
529        .take(SAMPLES + 1)
530        .map(|(er, er_next)| calculate_apy(er, er_next))
531        .collect::<Vec<_>>();
532
533    // Return 0.0 if there is no data OR if any APY is negative
534    if apys.is_empty() || apys.iter().any(|&apy| apy < 0.0) {
535        return 0.0;
536    }
537    // If any single epoch has outliers (that is APY > MAX_VALID_APY or exchange
538    // rate for epoch e-8 is missing), we switch to Median. Otherwise, we use
539    // the standard Mean.
540    let has_outlier = apys.get(SAMPLES).is_some_and(|&apy| apy <= 0.0)
541        || apys.iter().any(|&apy| apy > MAX_VALID_APY);
542
543    apys.truncate(SAMPLES);
544
545    if has_outlier {
546        Data::new(apys).median()
547    } else {
548        let sum: f64 = apys.iter().sum();
549        sum / SAMPLES as f64
550    }
551}
552
553/// APY magnitudes below this threshold are treated as exactly zero.
554const APY_DUST_THRESHOLD: f64 = 1e-9;
555
556/// Calculate the APY from the exchange rate of two consecutive epochs
557/// (`er` is the older epoch, `er_next` the newer one).
558///
559/// The formula used is `APY_e = (er.rate - er_next.rate) / er_next.rate * 365`.
560fn calculate_apy(er: &PoolTokenExchangeRate, er_next: &PoolTokenExchangeRate) -> f64 {
561    let apy = ((er.rate() - er_next.rate()) / er_next.rate()) * 365.0;
562    if apy.abs() < APY_DUST_THRESHOLD {
563        0.0
564    } else {
565        apy
566    }
567}
568
569fn stake_status(
570    epoch: u64,
571    activation_epoch: u64,
572    principal: u64,
573    exists: bool,
574    current_rate: Option<&PoolTokenExchangeRate>,
575    rate_table: &ValidatorExchangeRates,
576) -> StakeStatus {
577    if !exists {
578        StakeStatus::Unstaked
579    } else if epoch >= activation_epoch {
580        // TODO: use dev_inspect to call a move function to get the estimated reward
581        let estimated_reward = if let Some(current_rate) = current_rate {
582            let stake_rate = rate_table
583                .rates
584                .iter()
585                .find_map(|(epoch, rate)| (*epoch == activation_epoch).then(|| rate.clone()))
586                .unwrap_or_default();
587            let estimated_reward =
588                ((stake_rate.rate() / current_rate.rate()) - 1.0) * principal as f64;
589            max(0, estimated_reward.round() as u64)
590        } else {
591            0
592        };
593        StakeStatus::Active { estimated_reward }
594    } else {
595        StakeStatus::Pending
596    }
597}
598
599/// Cached exchange rates for validators for the given epoch, the cache size is
600/// 1, it will be cleared when the epoch changes. rates are in descending order
601/// by epoch.
602#[cached(
603    type = "SizedCache<EpochId, Vec<ValidatorExchangeRates>>",
604    create = "{ SizedCache::with_size(1) }",
605    convert = "{ _current_epoch }",
606    result = true
607)]
608async fn exchange_rates(
609    state: &Arc<dyn StateRead>,
610    _current_epoch: EpochId,
611) -> RpcInterimResult<Vec<ValidatorExchangeRates>> {
612    Ok(active_validators_exchange_rates(state)?
613        .into_iter()
614        .chain(inactive_validators_exchange_rates(state)?.into_iter())
615        .collect())
616}
617
618// `cached` keeps results by the input key -- `current_epoch`.
619// `exchange_rates` is not a pure function, has effects via `state`
620// which `cached` is not aware of.
621// In normal node operation this does not create issues.
622// In tests that run several different networks the latter calls
623// will get incorrect/outdated cached results.
624// This function allows to clear `cached` cache for `exchange_rates`.
625#[cfg(msim)]
626pub async fn clear_exchange_rates_cache_for_testing() {
627    use cached::Cached;
628    if let Some(mutex) = ::cached::once_cell::sync::Lazy::get(&EXCHANGE_RATES) {
629        let mut cache = mutex.lock().await;
630        cache.cache_clear();
631    }
632}
633
634/// Get validator exchange rates
635fn validator_exchange_rates(
636    state: &Arc<dyn StateRead>,
637    tables: Vec<ValidatorTable>,
638) -> RpcInterimResult<Vec<ValidatorExchangeRates>> {
639    if tables.is_empty() {
640        return Ok(vec![]);
641    };
642
643    let mut exchange_rates = vec![];
644    // Get exchange rates for each validator
645    for (address, pool_id, exchange_rates_id, exchange_rates_size, active) in tables {
646        let mut rates = state
647            .get_dynamic_fields(exchange_rates_id, None, exchange_rates_size as usize)?
648            .into_iter()
649            .map(|(_object_id, df)| {
650                let epoch: EpochId = bcs::from_bytes(&df.bcs_name).map_err(|e| {
651                    IotaError::ObjectDeserialization {
652                        error: e.to_string(),
653                    }
654                })?;
655
656                let exchange_rate: PoolTokenExchangeRate = get_dynamic_field_from_store(
657                    &state.get_object_store().as_ref(),
658                    exchange_rates_id,
659                    &epoch,
660                )?;
661
662                Ok::<_, IotaError>((epoch, exchange_rate))
663            })
664            .collect::<Result<Vec<_>, _>>()?;
665
666        // Rates for some epochs might be missing due to safe mode, we need to backfill
667        // them.
668        rates = backfill_rates(rates);
669
670        exchange_rates.push(ValidatorExchangeRates {
671            address,
672            pool_id,
673            active,
674            rates,
675        });
676    }
677
678    Ok(exchange_rates)
679}
680
681/// Check for validators in the `Active` state and get its exchange rate
682fn active_validators_exchange_rates(
683    state: &Arc<dyn StateRead>,
684) -> RpcInterimResult<Vec<ValidatorExchangeRates>> {
685    let system_state_summary = IotaSystemStateSummaryV2::try_from(
686        state.get_system_state()?.into_iota_system_state_summary(),
687    )?;
688
689    let tables = system_state_summary
690        .active_validators
691        .into_iter()
692        .map(|validator| {
693            (
694                validator.iota_address,
695                validator.staking_pool_id,
696                validator.exchange_rates_id,
697                validator.exchange_rates_size,
698                true,
699            )
700        })
701        .collect();
702
703    validator_exchange_rates(state, tables)
704}
705
706/// Check for validators in the `Inactive` state and get its exchange rate
707fn inactive_validators_exchange_rates(
708    state: &Arc<dyn StateRead>,
709) -> RpcInterimResult<Vec<ValidatorExchangeRates>> {
710    let system_state_summary = IotaSystemStateSummaryV2::try_from(
711        state.get_system_state()?.into_iota_system_state_summary(),
712    )?;
713
714    let tables = validator_summary_from_system_state(
715        state,
716        system_state_summary.inactive_pools_id,
717        system_state_summary.inactive_pools_size,
718        |df| bcs::from_bytes::<ID>(&df.bcs_name).map_err(Into::into),
719        Some(system_state_summary.protocol_version),
720    )?;
721
722    validator_exchange_rates(state, tables)
723}
724
725/// Check for validators in the `Pending` state and get its exchange rate. For
726/// these validators, their exchange rates should not be cached as their state
727/// can occur during an epoch or across multiple ones. In contrast, exchange
728/// rates for `Active` and `Inactive` validators can be cached, as their state
729/// changes only at epoch change.
730fn pending_validators_exchange_rate(
731    state: &Arc<dyn StateRead>,
732) -> RpcInterimResult<Vec<ValidatorExchangeRates>> {
733    let system_state = state.get_system_state()?;
734    let object_store = state.get_object_store();
735
736    // Try to find for any pending active validator
737    let tables = system_state
738        .get_pending_active_validators(object_store)?
739        .into_iter()
740        .map(|pending_active_validator| {
741            (
742                pending_active_validator.iota_address,
743                pending_active_validator.staking_pool_id,
744                pending_active_validator.exchange_rates_id,
745                pending_active_validator.exchange_rates_size,
746                false,
747            )
748        })
749        .collect::<Vec<ValidatorTable>>();
750
751    validator_exchange_rates(state, tables)
752}
753
754/// Check for validators in the `Candidate` state and get its exchange rate. For
755/// these validators, their exchange rates should not be cached as their state
756/// can occur during an epoch or across multiple ones. In contrast, exchange
757/// rates for `Active` and `Inactive` validators can be cached, as their state
758/// changes only at epoch change.
759fn candidate_validators_exchange_rate(
760    state: &Arc<dyn StateRead>,
761) -> RpcInterimResult<Vec<ValidatorExchangeRates>> {
762    let system_state_summary = IotaSystemStateSummaryV2::try_from(
763        state.get_system_state()?.into_iota_system_state_summary(),
764    )?;
765
766    // From validator_candidates_id table get validator info using as key its
767    // Address
768    let tables = validator_summary_from_system_state(
769        state,
770        system_state_summary.validator_candidates_id,
771        system_state_summary.validator_candidates_size,
772        |df| bcs::from_bytes::<Address>(&df.bcs_name).map_err(Into::into),
773        Some(system_state_summary.protocol_version),
774    )?;
775
776    validator_exchange_rates(state, tables)
777}
778
779/// Fetches validator status information from `StateRead`.
780///
781/// This makes sense for validators not included in `IotaSystemStateSummary`.
782/// `IotaSystemStateSummary` only contains information about `Active`
783/// validators. To retrieve information about `Inactive`, `Candidate`, and
784/// `Pending` validators, we need to access dynamic fields within specific
785/// Move tables.
786///
787/// To retrieve validator status information, this function utilizes the
788/// corresponding `table_id` (an `ObjectId` value) and a `limit` to specify the
789/// number of records to fetch. Both the `table_id` and `limit` can be obtained
790/// from `IotaSystemStateSummary` in the caller. Additionally, keys are
791/// extracted from the table `DynamicFieldInfo` values according to the `key`
792/// closure. This helps in identifying the specific validator within the table.
793///
794/// # Example
795///
796/// ```text
797/// // Get inactive validators
798/// let system_state_summary = state.get_system_state()?.into_iota_system_state_summary();
799/// let _ = validator_summary_from_system_state(
800///     state,
801///     // ID of the object that maps from a staking pool ID to the inactive validator that has that pool as its staking pool.
802///     system_state_summary.inactive_pools_id,
803///     // Number of inactive staking pools.
804///     system_state_summary.inactive_pools_size,
805///     // Extract the `ID` of the `Inactive` validator from the `DynamicFieldInfo` in the `system_state_summary.inactive_pools_id` table
806///     |df| bcs::from_bytes::<ID>(&df.bcs_name).map_err(Into::into),
807/// ).unwrap();
808/// ```
809///
810/// # Example
811///
812/// ```text
813/// // Get candidate validators
814/// let system_state_summary = state.get_system_state()?.into_iota_system_state_summary();
815/// let _ = validator_summary_from_system_state(
816///     state,
817///     // ID of the object that stores preactive validators, mapping their addresses to their Validator structs
818///     system_state_summary.validator_candidates_id,
819///     // Number of preactive validators
820///     system_state_summary.validator_candidates_size,
821///     // Extract the `Address` of the `Candidate` validator from the `DynamicFieldInfo` in the `system_state_summary.validator_candidates_id` table
822///     |df| bcs::from_bytes::<Address>(&df.bcs_name).map_err(Into::into),
823/// ).unwrap();
824/// ```
825fn validator_summary_from_system_state<K, F>(
826    state: &Arc<dyn StateRead>,
827    table_id: ObjectId,
828    limit: u64,
829    key: F,
830    protocol_version: Option<u64>,
831) -> RpcInterimResult<Vec<ValidatorTable>>
832where
833    F: Fn(DynamicFieldInfo) -> RpcInterimResult<K>,
834    K: MoveTypeTagTrait + Serialize + DeserializeOwned + Debug,
835{
836    let object_store = state.get_object_store();
837
838    state
839        .get_dynamic_fields(table_id, None, limit as usize)?
840        .into_iter()
841        .map(|(_object_id, df)| {
842            let validator_summary =
843                get_validator_from_table(object_store, table_id, &key(df)?, protocol_version)?;
844
845            Ok((
846                validator_summary.iota_address,
847                validator_summary.staking_pool_id,
848                validator_summary.exchange_rates_id,
849                validator_summary.exchange_rates_size,
850                false,
851            ))
852        })
853        .collect()
854}
855
856#[derive(Clone, Debug)]
857pub struct ValidatorExchangeRates {
858    pub address: Address,
859    pub pool_id: ObjectId,
860    pub active: bool,
861    pub rates: Vec<(EpochId, PoolTokenExchangeRate)>,
862}
863
864/// Backfill missing rates for some epochs due to safe mode. If a rate is
865/// missing for epoch e, we will use the rate for epoch e-1 to fill it. Rates
866/// returned are in descending order by epoch.
867fn backfill_rates(
868    mut rates: Vec<(EpochId, PoolTokenExchangeRate)>,
869) -> Vec<(EpochId, PoolTokenExchangeRate)> {
870    if rates.is_empty() {
871        return rates;
872    }
873    // ensure epochs are processed in increasing order
874    rates.sort_unstable_by_key(|(epoch_id, _)| *epoch_id);
875
876    // Check if there are any gaps in the epochs
877    let (min_epoch, _) = rates.first().expect("rates should not be empty");
878    let (max_epoch, _) = rates.last().expect("rates should not be empty");
879    let expected_len = (max_epoch - min_epoch + 1) as usize;
880    let current_len = rates.len();
881
882    // Only perform backfilling if there are gaps
883    if current_len == expected_len {
884        rates.reverse();
885        return rates;
886    }
887
888    let mut filled_rates: Vec<(EpochId, PoolTokenExchangeRate)> = Vec::with_capacity(expected_len);
889    let mut missing_rates = Vec::with_capacity(expected_len - current_len);
890    for (epoch_id, rate) in rates {
891        // fill gaps between the last processed epoch and the current one
892        if let Some((prev_epoch_id, prev_rate)) = filled_rates.last() {
893            for missing_epoch_id in prev_epoch_id + 1..epoch_id {
894                missing_rates.push((missing_epoch_id, prev_rate.clone()));
895            }
896        };
897
898        // append any missing_rates before adding the current epoch.
899        // if empty, nothing gets appended.
900        // if not empty, it will be empty afterwards because it was moved into
901        // filled_rates
902        filled_rates.append(&mut missing_rates);
903        filled_rates.push((epoch_id, rate));
904    }
905    filled_rates.reverse();
906    filled_rates
907}
908
909impl IotaRpcModule for GovernanceReadApi {
910    fn rpc(self) -> RpcModule<Self> {
911        self.into_rpc()
912    }
913
914    fn rpc_doc_module() -> Module {
915        GovernanceReadApiOpenRpc::module_doc()
916    }
917}
918
919#[cfg(test)]
920mod tests {
921    use iota_types::iota_system_state::PoolTokenExchangeRate;
922
923    use super::*;
924
925    #[test]
926    fn calculate_apys_with_outliers() {
927        let file =
928            std::fs::File::open("src/unit_tests/data/validator_exchange_rate/rates-test.json")
929                .unwrap();
930        let rates: BTreeMap<String, Vec<(u64, PoolTokenExchangeRate)>> =
931            serde_json::from_reader(file).unwrap();
932
933        let mut address_map = BTreeMap::new();
934
935        let exchange_rates = rates
936            .into_iter()
937            .map(|(validator, rates_vec)| {
938                let address = Address::random();
939                address_map.insert(address, validator);
940                ValidatorExchangeRates {
941                    address,
942                    pool_id: ObjectId::random(),
943                    active: true,
944                    rates: backfill_rates(rates_vec),
945                }
946            })
947            .collect();
948
949        let apys = calculate_apys(exchange_rates);
950
951        for apy in &apys {
952            println!("{}: {}", address_map[&apy.address], apy.apy);
953            assert!(apy.apy < 0.15)
954        }
955    }
956
957    #[test]
958    fn calculate_apys_without_outliers() {
959        let file =
960            std::fs::File::open("src/unit_tests/data/validator_exchange_rate/rates-feb26.json")
961                .unwrap();
962        let rates: BTreeMap<String, Vec<(u64, PoolTokenExchangeRate)>> =
963            serde_json::from_reader(file).unwrap();
964
965        let mut address_map = BTreeMap::new();
966
967        let exchange_rates = rates
968            .into_iter()
969            .map(|(validator, rates_vec)| {
970                let address = Address::random();
971                address_map.insert(address, validator);
972                ValidatorExchangeRates {
973                    address,
974                    pool_id: ObjectId::random(),
975                    active: true,
976                    rates: backfill_rates(rates_vec),
977                }
978            })
979            .collect();
980
981        let apys = calculate_apys(exchange_rates);
982
983        for apy in &apys {
984            println!("{}: {}", address_map[&apy.address], apy.apy);
985            assert!(apy.apy < 0.15)
986        }
987    }
988
989    #[test]
990    fn calculate_apy_is_not_negative_for_zero_reward_epoch() {
991        // Real mainnet exchange rates for two validators transitioning from
992        // epoch 381 to 382, an epoch in which they earned no rewards. The rate is
993        // therefore unchanged up to integer-truncation dust, so `calculate_apy`
994        // must report an effectively-zero APY (within
995        // `[0, APY_DUST_THRESHOLD)`).
996        let cases = [
997            (
998                (48_913_429_030_426_080u64, 43_331_127_650_932_384u64),
999                (48_641_042_011_532_656u64, 43_089_827_114_043_304u64),
1000            ),
1001            (
1002                (33_370_417_056_337_732u64, 29_578_114_234_284_444u64),
1003                (33_370_374_157_145_896u64, 29_578_076_210_270_704u64),
1004            ),
1005        ];
1006
1007        for ((i_old, t_old), (i_new, t_new)) in cases {
1008            let er = PoolTokenExchangeRate::new_for_testing(i_old, t_old);
1009            let er_next = PoolTokenExchangeRate::new_for_testing(i_new, t_new);
1010            let apy = calculate_apy(&er, &er_next);
1011            assert!(
1012                (0.0..APY_DUST_THRESHOLD).contains(&apy),
1013                "expected an effectively-zero, non-negative APY, got {apy}"
1014            );
1015        }
1016    }
1017
1018    #[test]
1019    fn test_backfill_rates_empty() {
1020        let rates = vec![];
1021        assert_eq!(backfill_rates(rates), vec![]);
1022    }
1023
1024    #[test]
1025    fn test_backfill_rates_no_gaps() {
1026        let rate1 = PoolTokenExchangeRate::new_for_testing(100, 100);
1027        let rate2 = PoolTokenExchangeRate::new_for_testing(200, 220);
1028        let rate3 = PoolTokenExchangeRate::new_for_testing(300, 330);
1029        let rates = vec![(2, rate2.clone()), (3, rate3.clone()), (1, rate1.clone())];
1030
1031        let expected: Vec<(u64, PoolTokenExchangeRate)> = vec![(3, rate3), (2, rate2), (1, rate1)];
1032        assert_eq!(backfill_rates(rates), expected);
1033    }
1034
1035    #[test]
1036    fn test_backfill_single_rate() {
1037        let rate1 = PoolTokenExchangeRate::new_for_testing(100, 100);
1038        let rates = vec![(1, rate1.clone())];
1039        let expected = vec![(1, rate1)];
1040        assert_eq!(backfill_rates(rates), expected);
1041    }
1042
1043    #[test]
1044    fn test_backfill_rates_with_gaps() {
1045        let rate1 = PoolTokenExchangeRate::new_for_testing(100, 100);
1046        let rate3 = PoolTokenExchangeRate::new_for_testing(300, 330);
1047        let rate5 = PoolTokenExchangeRate::new_for_testing(500, 550);
1048        let rates = vec![(3, rate3.clone()), (1, rate1.clone()), (5, rate5.clone())];
1049
1050        let expected = vec![
1051            (5, rate5),
1052            (4, rate3.clone()),
1053            (3, rate3),
1054            (2, rate1.clone()),
1055            (1, rate1),
1056        ];
1057        assert_eq!(backfill_rates(rates), expected);
1058    }
1059
1060    #[test]
1061    fn test_backfill_rates_missing_middle_epoch() {
1062        let rate1 = PoolTokenExchangeRate::new_for_testing(100, 100);
1063        let rate3 = PoolTokenExchangeRate::new_for_testing(300, 330);
1064        let rates = vec![(1, rate1.clone()), (3, rate3.clone())];
1065        let expected = vec![(3, rate3), (2, rate1.clone()), (1, rate1)];
1066        assert_eq!(backfill_rates(rates), expected);
1067    }
1068
1069    #[test]
1070    fn test_backfill_rates_missing_middle_epochs() {
1071        let rate1 = PoolTokenExchangeRate::new_for_testing(100, 100);
1072        let rate4 = PoolTokenExchangeRate::new_for_testing(400, 440);
1073        let rates = vec![(1, rate1.clone()), (4, rate4.clone())];
1074        let expected = vec![
1075            (4, rate4),
1076            (3, rate1.clone()),
1077            (2, rate1.clone()),
1078            (1, rate1),
1079        ];
1080        assert_eq!(backfill_rates(rates), expected);
1081    }
1082
1083    #[test]
1084    fn test_backfill_rates_unordered_input() {
1085        let rate1 = PoolTokenExchangeRate::new_for_testing(100, 100);
1086        let rate3 = PoolTokenExchangeRate::new_for_testing(300, 330);
1087        let rate4 = PoolTokenExchangeRate::new_for_testing(400, 440);
1088        let rates = vec![(3, rate3.clone()), (1, rate1.clone()), (4, rate4.clone())];
1089        let expected = vec![(4, rate4), (3, rate3), (2, rate1.clone()), (1, rate1)];
1090        assert_eq!(backfill_rates(rates), expected);
1091    }
1092}