iota_graphql_rpc/types/
validator.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::{BTreeMap, HashMap};
6
7use async_graphql::{
8    connection::{Connection, CursorType, Edge},
9    dataloader::Loader,
10    *,
11};
12use futures::TryFutureExt;
13use iota_indexer::apis::GovernanceReadApi;
14use iota_json_rpc::governance_api::median_apy_from_exchange_rates;
15use iota_types::{
16    base_types::IotaAddress as NativeIotaAddress,
17    committee::EpochId,
18    iota_system_state::{
19        PoolTokenExchangeRate,
20        iota_system_state_summary::IotaValidatorSummary as NativeIotaValidatorSummary,
21    },
22};
23
24use crate::{
25    consistency::ConsistentIndexCursor,
26    data::{DataLoader, Db},
27    error::Error,
28    types::{
29        address::Address,
30        base64::Base64,
31        big_int::BigInt,
32        cursor::{JsonCursor, Page},
33        iota_address::IotaAddress,
34        move_object::MoveObject,
35        object::Object,
36        owner::Owner,
37        system_state_summary::SystemStateSummaryView,
38        uint53::UInt53,
39        validator_credentials::ValidatorCredentials,
40    },
41};
42
43#[derive(Clone, Debug)]
44pub(crate) struct Validator {
45    pub validator_summary: NativeIotaValidatorSummary,
46    pub at_risk: Option<u64>,
47    pub report_records: Option<Vec<Address>>,
48    /// The checkpoint sequence number at which this was viewed at.
49    pub checkpoint_viewed_at: u64,
50    /// The epoch at which this validator's information was requested to be
51    /// viewed at.
52    pub requested_for_epoch: u64,
53}
54
55/// Loads the exchange rates from the cache and return a tuple (epoch stake
56/// subsidy started, and a BTreeMap holiding the exchange rates for each epoch
57/// for each validator.
58///
59/// It automatically filters the exchange rate table to only include data for
60/// the epochs that are less than or equal to the requested epoch.
61impl Loader<u64> for Db {
62    type Value = BTreeMap<NativeIotaAddress, Vec<(EpochId, PoolTokenExchangeRate)>>;
63
64    type Error = Error;
65
66    async fn load(
67        &self,
68        keys: &[u64],
69    ) -> Result<
70        HashMap<u64, BTreeMap<NativeIotaAddress, Vec<(EpochId, PoolTokenExchangeRate)>>>,
71        Error,
72    > {
73        let latest_iota_system_state = self
74            .inner
75            .spawn_blocking(move |this| this.get_latest_iota_system_state())
76            .await
77            .map_err(|_| Error::Internal("Failed to fetch latest IOTA system state".to_string()))?;
78        let governance_api = GovernanceReadApi::new(self.inner.clone());
79
80        let (candidate_rates, pending_rates) = tokio::try_join!(
81            governance_api
82                .candidate_validators_exchange_rate(&latest_iota_system_state)
83                .map_err(|e| {
84                    Error::Internal(format!(
85                        "Error fetching candidate validators exchange rates. {e}"
86                    ))
87                }),
88            governance_api
89                .pending_validators_exchange_rate()
90                .map_err(|e| {
91                    Error::Internal(format!(
92                        "Error fetching pending validators exchange rates. {e}"
93                    ))
94                })
95        )?;
96
97        let mut exchange_rates = governance_api
98            .exchange_rates(&latest_iota_system_state)
99            .await
100            .map_err(|e| Error::Internal(format!("Error fetching exchange rates. {e}")))?;
101
102        exchange_rates.extend(candidate_rates.into_iter());
103        exchange_rates.extend(pending_rates.into_iter());
104
105        let mut results = BTreeMap::new();
106
107        // The requested epoch is the epoch for which we want to compute the APY. For
108        // the current ongoing epoch we cannot compute an APY, so we compute it
109        // for epoch - 1. First need to check if that requested epoch is not the
110        // current running one. If it is, then subtract one as the APY cannot be
111        // computed for a running epoch. If no epoch is passed in the key, then
112        // we default to the latest epoch - 1 for the same reasons as above.
113        let epoch_to_filter_out = if let Some(epoch) = keys.first() {
114            if epoch == &latest_iota_system_state.epoch() {
115                *epoch - 1
116            } else {
117                *epoch
118            }
119        } else {
120            latest_iota_system_state.epoch() - 1
121        };
122
123        // filter the exchange rates to only include data for the epochs that are less
124        // than or equal to the requested epoch. This enables us to get
125        // historical exchange rates accurately and pass this to the APY
126        // calculation function TODO we might even filter here by the epoch at
127        // which the stake subsidy started to avoid passing that to the
128        // `calculate_apy` function and doing another filter there
129        for er in exchange_rates {
130            results.insert(
131                er.address,
132                er.rates
133                    .into_iter()
134                    .filter(|(epoch, _)| epoch <= &epoch_to_filter_out)
135                    .collect(),
136            );
137        }
138
139        let requested_epoch = match keys.first() {
140            Some(x) => *x,
141            None => latest_iota_system_state.epoch(),
142        };
143
144        let mut r = HashMap::new();
145        r.insert(requested_epoch, results);
146
147        Ok(r)
148    }
149}
150
151type CAddr = JsonCursor<ConsistentIndexCursor>;
152
153#[Object]
154impl Validator {
155    /// The validator's address.
156    async fn address(&self) -> Address {
157        Address {
158            address: IotaAddress::from(self.validator_summary.iota_address),
159            checkpoint_viewed_at: self.checkpoint_viewed_at,
160        }
161    }
162
163    /// Validator's set of credentials such as public keys, network addresses
164    /// and others.
165    async fn credentials(&self) -> Option<ValidatorCredentials> {
166        let v = &self.validator_summary;
167        let credentials = ValidatorCredentials {
168            authority_pub_key: Some(Base64::from(v.authority_pubkey_bytes.clone())),
169            network_pub_key: Some(Base64::from(v.network_pubkey_bytes.clone())),
170            protocol_pub_key: Some(Base64::from(v.protocol_pubkey_bytes.clone())),
171            proof_of_possession: Some(Base64::from(v.proof_of_possession_bytes.clone())),
172            net_address: Some(v.net_address.clone()),
173            p2p_address: Some(v.p2p_address.clone()),
174            primary_address: Some(v.primary_address.clone()),
175        };
176        Some(credentials)
177    }
178
179    /// Validator's set of credentials for the next epoch.
180    async fn next_epoch_credentials(&self) -> Option<ValidatorCredentials> {
181        let v = &self.validator_summary;
182        let credentials = ValidatorCredentials {
183            authority_pub_key: v
184                .next_epoch_authority_pubkey_bytes
185                .as_ref()
186                .map(Base64::from),
187            network_pub_key: v.next_epoch_network_pubkey_bytes.as_ref().map(Base64::from),
188            protocol_pub_key: v
189                .next_epoch_protocol_pubkey_bytes
190                .as_ref()
191                .map(Base64::from),
192            proof_of_possession: v.next_epoch_proof_of_possession.as_ref().map(Base64::from),
193            net_address: v.next_epoch_net_address.clone(),
194            p2p_address: v.next_epoch_p2p_address.clone(),
195            primary_address: v.next_epoch_primary_address.clone(),
196        };
197        Some(credentials)
198    }
199
200    /// Validator's name.
201    async fn name(&self) -> Option<String> {
202        Some(self.validator_summary.name.clone())
203    }
204
205    /// Validator's description.
206    async fn description(&self) -> Option<String> {
207        Some(self.validator_summary.description.clone())
208    }
209
210    /// Validator's url containing their custom image.
211    async fn image_url(&self) -> Option<String> {
212        Some(self.validator_summary.image_url.clone())
213    }
214
215    /// Validator's homepage URL.
216    async fn project_url(&self) -> Option<String> {
217        Some(self.validator_summary.project_url.clone())
218    }
219
220    /// The validator's current valid `Cap` object. Validators can delegate
221    /// the operation ability to another address. The address holding this `Cap`
222    /// object can then update the reference gas price and tallying rule on
223    /// behalf of the validator.
224    async fn operation_cap(&self, ctx: &Context<'_>) -> Result<Option<MoveObject>> {
225        MoveObject::query(
226            ctx,
227            self.operation_cap_id(),
228            Object::latest_at(self.checkpoint_viewed_at),
229        )
230        .await
231        .extend()
232    }
233
234    /// The validator's current staking pool object, used to track the amount of
235    /// stake and to compound staking rewards.
236    #[graphql(
237        deprecation = "The staking pool is a wrapped object. Access its fields directly on the \
238        `Validator` type."
239    )]
240    async fn staking_pool(&self) -> Result<Option<MoveObject>> {
241        Ok(None)
242    }
243
244    /// The ID of this validator's `0x3::staking_pool::StakingPoolV1`.
245    async fn staking_pool_id(&self) -> IotaAddress {
246        self.validator_summary.staking_pool_id.into()
247    }
248
249    /// The validator's current exchange object. The exchange rate is used to
250    /// determine the amount of IOTA tokens that each past IOTA staker can
251    /// withdraw in the future.
252    #[graphql(
253        deprecation = "The exchange object is a wrapped object. Access its dynamic fields through \
254        the `exchangeRatesTable` query."
255    )]
256    async fn exchange_rates(&self) -> Result<Option<MoveObject>> {
257        Ok(None)
258    }
259
260    /// A wrapped object containing the validator's exchange rates. This is a
261    /// table from epoch number to `PoolTokenExchangeRate` value. The
262    /// exchange rate is used to determine the amount of IOTA tokens that
263    /// each past IOTA staker can withdraw in the future.
264    async fn exchange_rates_table(&self) -> Result<Option<Owner>> {
265        Ok(Some(Owner {
266            address: self.validator_summary.exchange_rates_id.into(),
267            checkpoint_viewed_at: self.checkpoint_viewed_at,
268            root_version: None,
269        }))
270    }
271
272    /// Number of exchange rates in the table.
273    async fn exchange_rates_size(&self) -> Option<UInt53> {
274        Some(self.validator_summary.exchange_rates_size.into())
275    }
276
277    /// The epoch at which this pool became active.
278    async fn staking_pool_activation_epoch(&self) -> Option<UInt53> {
279        self.validator_summary
280            .staking_pool_activation_epoch
281            .map(UInt53::from)
282    }
283
284    /// The total number of IOTA tokens in this pool.
285    async fn staking_pool_iota_balance(&self) -> Option<BigInt> {
286        Some(BigInt::from(
287            self.validator_summary.staking_pool_iota_balance,
288        ))
289    }
290
291    /// The epoch stake rewards will be added here at the end of each epoch.
292    async fn rewards_pool(&self) -> Option<BigInt> {
293        Some(BigInt::from(self.validator_summary.rewards_pool))
294    }
295
296    /// Total number of pool tokens issued by the pool.
297    async fn pool_token_balance(&self) -> Option<BigInt> {
298        Some(BigInt::from(self.validator_summary.pool_token_balance))
299    }
300
301    /// Pending stake amount for this epoch.
302    async fn pending_stake(&self) -> Option<BigInt> {
303        Some(BigInt::from(self.validator_summary.pending_stake))
304    }
305
306    /// Pending stake withdrawn during the current epoch, emptied at epoch
307    /// boundaries.
308    async fn pending_total_iota_withdraw(&self) -> Option<BigInt> {
309        Some(BigInt::from(
310            self.validator_summary.pending_total_iota_withdraw,
311        ))
312    }
313
314    /// Pending pool token withdrawn during the current epoch, emptied at epoch
315    /// boundaries.
316    async fn pending_pool_token_withdraw(&self) -> Option<BigInt> {
317        Some(BigInt::from(
318            self.validator_summary.pending_pool_token_withdraw,
319        ))
320    }
321
322    /// The voting power of this validator in basis points (e.g., 100 = 1%
323    /// voting power).
324    async fn voting_power(&self) -> Option<u64> {
325        Some(self.validator_summary.voting_power)
326    }
327
328    // TODO async fn stake_units(&self) -> Option<u64>{}
329
330    /// The reference gas price for this epoch.
331    async fn gas_price(&self) -> Option<BigInt> {
332        Some(BigInt::from(self.validator_summary.gas_price))
333    }
334
335    /// The fee charged by the validator for staking services.
336    async fn commission_rate(&self) -> Option<u64> {
337        Some(self.validator_summary.commission_rate)
338    }
339
340    /// The total number of IOTA tokens in this pool plus
341    /// the pending stake amount for this epoch.
342    async fn next_epoch_stake(&self) -> Option<BigInt> {
343        Some(BigInt::from(self.validator_summary.next_epoch_stake))
344    }
345
346    /// The validator's gas price quote for the next epoch.
347    async fn next_epoch_gas_price(&self) -> Option<BigInt> {
348        Some(BigInt::from(self.validator_summary.next_epoch_gas_price))
349    }
350
351    /// The proposed next epoch fee for the validator's staking services.
352    async fn next_epoch_commission_rate(&self) -> Option<u64> {
353        Some(self.validator_summary.next_epoch_commission_rate)
354    }
355
356    /// The number of epochs for which this validator has been below the
357    /// low stake threshold.
358    async fn at_risk(&self) -> Option<UInt53> {
359        self.at_risk.map(UInt53::from)
360    }
361
362    /// The addresses of other validators this validator has reported.
363    async fn report_records(
364        &self,
365        ctx: &Context<'_>,
366        first: Option<u64>,
367        before: Option<CAddr>,
368        last: Option<u64>,
369        after: Option<CAddr>,
370    ) -> Result<Connection<String, Address>> {
371        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
372
373        let mut connection = Connection::new(false, false);
374        let Some(addresses) = &self.report_records else {
375            return Ok(connection);
376        };
377
378        let Some((prev, next, _, cs)) =
379            page.paginate_consistent_indices(addresses.len(), self.checkpoint_viewed_at)?
380        else {
381            return Ok(connection);
382        };
383
384        connection.has_previous_page = prev;
385        connection.has_next_page = next;
386
387        for c in cs {
388            connection.edges.push(Edge::new(
389                c.encode_cursor(),
390                Address {
391                    address: addresses[c.ix].address,
392                    checkpoint_viewed_at: c.c,
393                },
394            ));
395        }
396
397        Ok(connection)
398    }
399
400    /// The APY of this validator in basis points. To get the APY in
401    /// percentage, divide by 100.
402    async fn apy(&self, ctx: &Context<'_>) -> Result<Option<u64>, Error> {
403        let DataLoader(loader) = ctx.data_unchecked();
404        let exchange_rates = loader
405            .load_one(self.requested_for_epoch)
406            .await?
407            .ok_or_else(|| Error::Internal("DataLoading exchange rates failed".to_string()))?;
408        let rates = exchange_rates
409            .get(&self.validator_summary.iota_address)
410            .ok_or_else(|| {
411                Error::Internal(format!(
412                    "Failed to get the exchange rate for this validator address {} for requested epoch {}",
413                    self.validator_summary.iota_address, self.requested_for_epoch
414                ))
415            })?
416            .iter()
417            .map(|(_, exchange_rate)| exchange_rate);
418
419        let avg_apy = Some(median_apy_from_exchange_rates(rates));
420
421        Ok(avg_apy.map(|x| (x * 10000.0) as u64))
422    }
423}
424
425impl Validator {
426    pub fn operation_cap_id(&self) -> IotaAddress {
427        IotaAddress::from_array(**self.validator_summary.operation_cap_id)
428    }
429}