1use 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
53const 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 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 .chain(candidate_validators_exchange_rate(&self.state)?)
238 .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 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 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 .chain(candidate_validators_exchange_rate(&self.state)?)
306 .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 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 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
510pub fn mean_apy_from_exchange_rates<'er>(
518 exchange_rates: impl DoubleEndedIterator<Item = &'er PoolTokenExchangeRate> + Clone,
519) -> f64 {
520 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 if apys.is_empty() || apys.iter().any(|&apy| apy < 0.0) {
535 return 0.0;
536 }
537 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
553const APY_DUST_THRESHOLD: f64 = 1e-9;
555
556fn 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 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(
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#[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
634fn 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 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 = 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
681fn 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
706fn 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
725fn 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 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
754fn 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 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
779fn 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
864fn backfill_rates(
868 mut rates: Vec<(EpochId, PoolTokenExchangeRate)>,
869) -> Vec<(EpochId, PoolTokenExchangeRate)> {
870 if rates.is_empty() {
871 return rates;
872 }
873 rates.sort_unstable_by_key(|(epoch_id, _)| *epoch_id);
875
876 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 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 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 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 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}