diff --git a/crates/sui-indexer-alt-e2e-tests/tests/graphql/epochs/validator_set/report_records.move b/crates/sui-indexer-alt-e2e-tests/tests/graphql/epochs/validator_set/report_records.move new file mode 100644 index 0000000000000..115eadcb75e12 --- /dev/null +++ b/crates/sui-indexer-alt-e2e-tests/tests/graphql/epochs/validator_set/report_records.move @@ -0,0 +1,24 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//# init --protocol-version 70 --accounts A --simulator --num-custom-validator-accounts 2 + +//# run-graphql +{ + epoch(epochId: 0) { + epochId + validatorSet { + activeValidators { + nodes { + name + # todo DVX-1697 populate reportRecords + reportRecords { + nodes { + name + } + } + } + } + } + } +} diff --git a/crates/sui-indexer-alt-e2e-tests/tests/graphql/epochs/validator_set/report_records.snap b/crates/sui-indexer-alt-e2e-tests/tests/graphql/epochs/validator_set/report_records.snap new file mode 100644 index 0000000000000..a984e15a47de8 --- /dev/null +++ b/crates/sui-indexer-alt-e2e-tests/tests/graphql/epochs/validator_set/report_records.snap @@ -0,0 +1,35 @@ +--- +source: external-crates/move/crates/move-transactional-test-runner/src/framework.rs +--- +processed 2 tasks + +init: +A: object(0,0), validator_0: object(0,1), validator_1: object(0,2) + +task 1, lines 6-24: +//# run-graphql +Response: { + "data": { + "epoch": { + "epochId": 0, + "validatorSet": { + "activeValidators": { + "nodes": [ + { + "name": "validator-0", + "reportRecords": { + "nodes": [] + } + }, + { + "name": "validator-1", + "reportRecords": { + "nodes": [] + } + } + ] + } + } + } + } +} diff --git a/crates/sui-indexer-alt-graphql/schema.graphql b/crates/sui-indexer-alt-graphql/schema.graphql index ac8839e3c9ce3..af32a0fdc8a40 100644 --- a/crates/sui-indexer-alt-graphql/schema.graphql +++ b/crates/sui-indexer-alt-graphql/schema.graphql @@ -4128,6 +4128,10 @@ type Validator implements IAddressable { """ projectUrl: String """ + Other validators this validator has reported. + """ + reportRecords(first: Int, before: String, last: Int, after: String): ValidatorConnection + """ The epoch stake rewards will be added here at the end of each epoch. """ rewardsPool: BigInt diff --git a/crates/sui-indexer-alt-graphql/src/api/types/epoch.rs b/crates/sui-indexer-alt-graphql/src/api/types/epoch.rs index d8996a138b822..134217329f56c 100644 --- a/crates/sui-indexer-alt-graphql/src/api/types/epoch.rs +++ b/crates/sui-indexer-alt-graphql/src/api/types/epoch.rs @@ -236,17 +236,20 @@ impl Epoch { return Ok(None); }; - let validator_set_v1 = match system_state { - SuiSystemState::V1(inner) => inner.validators, - SuiSystemState::V2(inner) => inner.validators, + let (validator_set_v1, report_records) = match system_state { + SuiSystemState::V1(inner) => (inner.validators, inner.validator_report_records), + SuiSystemState::V2(inner) => (inner.validators, inner.validator_report_records), #[cfg(msim)] SuiSystemState::SimTestV1(_) | SuiSystemState::SimTestShallowV2(_) | SuiSystemState::SimTestDeepV2(_) => return Ok(None), }; - let validator_set = - ValidatorSet::from_validator_set_v1(self.scope.clone(), validator_set_v1); + let validator_set = ValidatorSet::from_validator_set_v1( + self.scope.clone(), + validator_set_v1, + report_records, + ); Ok(Some(validator_set)) } diff --git a/crates/sui-indexer-alt-graphql/src/api/types/validator.rs b/crates/sui-indexer-alt-graphql/src/api/types/validator.rs index 4b0923f04fb36..1bcec9462ee59 100644 --- a/crates/sui-indexer-alt-graphql/src/api/types/validator.rs +++ b/crates/sui-indexer-alt-graphql/src/api/types/validator.rs @@ -1,14 +1,16 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use std::sync::Arc; + use async_graphql::{connection::Connection, Context, Object, SimpleObject}; use sui_types::sui_system_state::sui_system_state_inner_v1::ValidatorV1; use crate::{ api::{ scalars::{ - base64::Base64, big_int::BigInt, sui_address::SuiAddress, type_filter::TypeInput, - uint53::UInt53, + base64::Base64, big_int::BigInt, cursor::JsonCursor, sui_address::SuiAddress, + type_filter::TypeInput, uint53::UInt53, }, types::{ address::Address, @@ -17,17 +19,17 @@ use crate::{ move_object::MoveObject, object::{CLive, Error}, object_filter::{ObjectFilter, ObjectFilterValidator as OFValidator}, + validator_set::ValidatorSetContents, }, }, error::{upcast, RpcError}, - scope::Scope, + pagination::{Page, PaginationConfig}, }; #[derive(Clone, Debug)] pub(crate) struct Validator { - super_: Address, - native: ValidatorV1, - at_risk: u64, + pub(crate) contents: Arc, + pub(crate) idx: usize, } /// The credentials related fields associated with a validator. @@ -43,11 +45,13 @@ pub(crate) struct ValidatorCredentials { pub worker_address: Option, } +type CAddr = JsonCursor; + #[Object] impl Validator { /// The validator's address. pub(crate) async fn address(&self, ctx: &Context<'_>) -> Result { - self.super_.address(ctx).await + self.super_().address(ctx).await } /// Fetch the total balance for coins with marker type `coinType` (e.g. `0x2::sui::SUI`), owned by this address. @@ -58,7 +62,7 @@ impl Validator { ctx: &Context<'_>, coin_type: TypeInput, ) -> Result, RpcError> { - self.super_.balance(ctx, coin_type).await + self.super_().balance(ctx, coin_type).await } /// Total balance across coins owned by this address, grouped by coin type. @@ -70,7 +74,9 @@ impl Validator { last: Option, before: Option, ) -> Result>, RpcError> { - self.super_.balances(ctx, first, after, last, before).await + self.super_() + .balances(ctx, first, after, last, before) + .await } /// The domain explicitly configured as the default SuiNS name for this address. @@ -78,7 +84,7 @@ impl Validator { &self, ctx: &Context<'_>, ) -> Result, RpcError> { - self.super_.default_suins_name(ctx).await + self.super_().default_suins_name(ctx).await } /// Fetch the total balances keyed by coin types (e.g. `0x2::sui::SUI`) owned by this address. @@ -90,7 +96,7 @@ impl Validator { ctx: &Context<'_>, keys: Vec, ) -> Result>, RpcError> { - self.super_.multi_get_balances(ctx, keys).await + self.super_().multi_get_balances(ctx, keys).await } /// Objects owned by this object, optionally filtered by type. @@ -103,14 +109,14 @@ impl Validator { before: Option, #[graphql(validator(custom = "OFValidator::allows_empty()"))] filter: Option, ) -> Result>, RpcError> { - self.super_ + self.super_() .objects(ctx, first, after, last, before, filter) .await } /// Validator's set of credentials such as public keys, network addresses and others. async fn credentials(&self) -> Option { - let v = &self.native.metadata; + let v = &self.validator().metadata; let credentials = ValidatorCredentials { protocol_pub_key: Some(Base64::from(v.protocol_pubkey_bytes.clone())), network_pub_key: Some(Base64::from(v.network_pubkey_bytes.clone())), @@ -126,7 +132,7 @@ impl Validator { /// Validator's set of credentials for the next epoch. async fn next_epoch_credentials(&self) -> Option { - let v = &self.native.metadata; + let v = &self.validator().metadata; let credentials = ValidatorCredentials { protocol_pub_key: v .next_epoch_protocol_pubkey_bytes @@ -145,22 +151,22 @@ impl Validator { /// Validator's name. async fn name(&self) -> Option { - Some(self.native.metadata.name.clone()) + Some(self.validator().metadata.name.clone()) } /// Validator's description. async fn description(&self) -> Option { - Some(self.native.metadata.description.clone()) + Some(self.validator().metadata.description.clone()) } /// Validator's url containing their custom image. async fn image_url(&self) -> Option { - Some(self.native.metadata.image_url.clone()) + Some(self.validator().metadata.image_url.clone()) } /// Validator's homepage URL. async fn project_url(&self) -> Option { - Some(self.native.metadata.project_url.clone()) + Some(self.validator().metadata.project_url.clone()) } /// The validator's current valid `Cap` object. Validators can delegate the operation ability to another address. @@ -170,8 +176,8 @@ impl Validator { ctx: &Context<'_>, ) -> Result, RpcError> { let address = Address::with_address( - self.super_.scope.clone(), - self.native.operation_cap_id.bytes.into(), + self.contents.scope.clone(), + self.validator().operation_cap_id.bytes.into(), ); let Some(object) = address.as_object(ctx).await? else { return Ok(None); @@ -181,144 +187,151 @@ impl Validator { /// The ID of this validator's `0x3::staking_pool::StakingPool`. async fn staking_pool_id(&self) -> SuiAddress { - self.native.staking_pool.id.into() + self.validator().staking_pool.id.into() } /// A wrapped object containing the validator's exchange rates. This is a table from epoch number to `PoolTokenExchangeRate` value. /// The exchange rate is used to determine the amount of SUI tokens that each past SUI staker can withdraw in the future. async fn exchange_rates_table(&self) -> Option
{ let address = Address::with_address( - self.super_.scope.clone(), - self.native.staking_pool.exchange_rates.id.into(), + self.contents.scope.clone(), + self.validator().staking_pool.exchange_rates.id.into(), ); Some(address) } /// Number of exchange rates in the table. async fn exchange_rates_size(&self) -> Option { - Some(self.native.staking_pool.exchange_rates.size.into()) + Some(self.validator().staking_pool.exchange_rates.size.into()) } /// The epoch at which this pool became active. async fn staking_pool_activation_epoch(&self) -> Option { - self.native.staking_pool.activation_epoch.map(UInt53::from) + self.validator() + .staking_pool + .activation_epoch + .map(UInt53::from) } /// The total number of SUI tokens in this pool. async fn staking_pool_sui_balance(&self) -> Option { - Some(BigInt::from(self.native.staking_pool.sui_balance)) + Some(BigInt::from(self.validator().staking_pool.sui_balance)) } /// The epoch stake rewards will be added here at the end of each epoch. async fn rewards_pool(&self) -> Option { - Some(BigInt::from(self.native.staking_pool.rewards_pool.value())) + Some(BigInt::from( + self.validator().staking_pool.rewards_pool.value(), + )) } /// Total number of pool tokens issued by the pool. async fn pool_token_balance(&self) -> Option { - Some(BigInt::from(self.native.staking_pool.pool_token_balance)) + Some(BigInt::from( + self.validator().staking_pool.pool_token_balance, + )) } /// Pending stake amount for this epoch. async fn pending_stake(&self) -> Option { - Some(BigInt::from(self.native.staking_pool.pending_stake)) + Some(BigInt::from(self.validator().staking_pool.pending_stake)) } /// Pending stake withdrawn during the current epoch, emptied at epoch boundaries. async fn pending_total_sui_withdraw(&self) -> Option { Some(BigInt::from( - self.native.staking_pool.pending_total_sui_withdraw, + self.validator().staking_pool.pending_total_sui_withdraw, )) } /// Pending pool token withdrawn during the current epoch, emptied at epoch boundaries. async fn pending_pool_token_withdraw(&self) -> Option { Some(BigInt::from( - self.native.staking_pool.pending_pool_token_withdraw, + self.validator().staking_pool.pending_pool_token_withdraw, )) } /// The voting power of this validator in basis points (e.g., 100 = 1% voting power). async fn voting_power(&self) -> Option { - Some(self.native.voting_power) + Some(self.validator().voting_power) } /// The reference gas price for this epoch. async fn gas_price(&self) -> Option { - Some(BigInt::from(self.native.gas_price)) + Some(BigInt::from(self.validator().gas_price)) } /// The fee charged by the validator for staking services. async fn commission_rate(&self) -> Option { - Some(self.native.commission_rate) + Some(self.validator().commission_rate) } /// The total number of SUI tokens in this pool plus the pending stake amount for this epoch. async fn next_epoch_stake(&self) -> Option { - Some(BigInt::from(self.native.next_epoch_stake)) + Some(BigInt::from(self.validator().next_epoch_stake)) } /// The validator's gas price quote for the next epoch. async fn next_epoch_gas_price(&self) -> Option { - Some(BigInt::from(self.native.next_epoch_gas_price)) + Some(BigInt::from(self.validator().next_epoch_gas_price)) } /// The proposed next epoch fee for the validator's staking services. async fn next_epoch_commission_rate(&self) -> Option { - Some(self.native.next_epoch_commission_rate) + Some(self.validator().next_epoch_commission_rate) } /// The number of epochs for which this validator has been below the low stake threshold. async fn at_risk(&self) -> Option { - Some(self.at_risk.into()) - } - - // todo (ewall) - // /// The addresses of other validators this validator has reported. - // async fn report_records( - // &self, - // ctx: &Context<'_>, - // first: Option, - // before: Option, - // last: Option, - // after: Option, - // ) -> async_graphql::Result> { - // let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?; - // - // let mut connection = Connection::new(false, false); - // let Some(addresses) = &self.report_records else { - // return Ok(connection); - // }; - // - // let Some((prev, next, _, cs)) = - // page.paginate_consistent_indices(addresses.len(), self.checkpoint_viewed_at)? - // else { - // return Ok(connection); - // }; - // - // connection.has_previous_page = prev; - // connection.has_next_page = next; - // - // for c in cs { - // connection.edges.push(Edge::new( - // c.encode_cursor(), - // Address { - // address: addresses[c.ix].address, - // checkpoint_viewed_at: c.c, - // }, - // )); - // } - // - // Ok(connection) - // } + let at_risk = self + .contents + .native + .at_risk_validators + .get(&self.validator().metadata.sui_address) + .map_or(0, |at_risk| *at_risk); + Some(at_risk.into()) + } + + /// Other validators this validator has reported. + async fn report_records( + &self, + ctx: &Context<'_>, + first: Option, + before: Option, + last: Option, + after: Option, + ) -> Result>, RpcError> { + let Some(report_records) = self + .contents + .report_records + .get(&self.validator().metadata.sui_address) + else { + return Ok(Some(Connection::new(false, false))); + }; + + let pagination: &PaginationConfig = ctx.data()?; + let limits = pagination.limits("Validator", "reportRecords"); + let page = Page::from_params(limits, first, after, last, before)?; + page.paginate_indices(report_records.len(), |i| { + let idx = report_records[i]; + Ok(Validator { + contents: Arc::clone(&self.contents), + idx, + }) + }) + .map(Some) + } } impl Validator { - pub(crate) fn from_validator_v1(scope: Scope, native: ValidatorV1, at_risk: u64) -> Self { - Self { - super_: Address::with_address(scope, native.metadata.sui_address), - native, - at_risk, - } + fn super_(&self) -> Address { + Address::with_address( + self.contents.scope.clone(), + self.validator().metadata.sui_address, + ) + } + + fn validator(&self) -> &ValidatorV1 { + &self.contents.native.active_validators[self.idx] } } diff --git a/crates/sui-indexer-alt-graphql/src/api/types/validator_set.rs b/crates/sui-indexer-alt-graphql/src/api/types/validator_set.rs index cdf2a149aeef7..f411e893671b5 100644 --- a/crates/sui-indexer-alt-graphql/src/api/types/validator_set.rs +++ b/crates/sui-indexer-alt-graphql/src/api/types/validator_set.rs @@ -1,8 +1,14 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use async_graphql::{connection::Connection, Context, Object}; -use sui_types::sui_system_state::sui_system_state_inner_v1::ValidatorSetV1; +use std::{collections::BTreeMap, sync::Arc}; + +use async_graphql::{connection::Connection, indexmap::IndexMap, Context, Object}; +use sui_types::{ + base_types::SuiAddress as NativeSuiAddress, + collection_types::{Entry, VecMap, VecSet}, + sui_system_state::sui_system_state_inner_v1::ValidatorSetV1, +}; use crate::{ api::{ @@ -19,8 +25,14 @@ pub(crate) type CValidator = JsonCursor; /// Representation of `0x3::validator_set::ValidatorSet`. #[derive(Clone, Debug)] pub(crate) struct ValidatorSet { - scope: Scope, - native: ValidatorSetV1, + pub(crate) contents: Arc, +} + +#[derive(Clone, Debug)] +pub(crate) struct ValidatorSetContents { + pub(crate) scope: Scope, + pub(crate) native: ValidatorSetV1, + pub(crate) report_records: IndexMap>, } /// Representation of `0x3::validator_set::ValidatorSet`. @@ -39,79 +51,107 @@ impl ValidatorSet { let limits = pagination.limits("ValidatorSet", "activeValidators"); let page = Page::from_params(limits, first, after, last, before)?; - page.paginate_indices(self.native.active_validators.len(), |i| { - let validator = &self.native.active_validators[i]; - - let at_risk = self - .native - .at_risk_validators - .get(&validator.metadata.sui_address) - .map_or(0, |at_risk| *at_risk); - - Ok(Validator::from_validator_v1( - self.scope.clone(), - validator.clone(), - at_risk, - )) + page.paginate_indices(self.contents.native.active_validators.len(), |idx| { + Ok(Validator { + contents: Arc::clone(&self.contents), + idx, + }) }) .map(Some) } /// Object ID of the `Table` storing the inactive staking pools. async fn inactive_pools_id(&self) -> Option { - Some(self.native.inactive_validators.id.into()) + Some(self.contents.native.inactive_validators.id.into()) } /// Size of the inactive pools `Table`. async fn inactive_pools_size(&self) -> Option { - Some(self.native.inactive_validators.size) + Some(self.contents.native.inactive_validators.size) } // TODO: instead of returning the id and size of the table, potentially return the table itself, paginated. /// Object ID of the wrapped object `TableVec` storing the pending active validators. async fn pending_active_validators_id(&self) -> Option { - Some(self.native.pending_active_validators.contents.id.into()) + Some( + self.contents + .native + .pending_active_validators + .contents + .id + .into(), + ) } /// Size of the pending active validators table. async fn pending_active_validators_size(&self) -> Option { - Some(self.native.pending_active_validators.contents.size) + Some(self.contents.native.pending_active_validators.contents.size) } /// Validators that are pending removal from the active validator set, expressed as indices in to `activeValidators`. async fn pending_removals(&self) -> Option> { - Some(self.native.pending_removals.clone()) + Some(self.contents.native.pending_removals.clone()) } /// Object ID of the `Table` storing the mapping from staking pool ids to the addresses of the corresponding validators. /// This is needed because a validator's address can potentially change but the object ID of its pool will not. async fn staking_pool_mappings_id(&self) -> Option { - Some(self.native.staking_pool_mappings.id.into()) + Some(self.contents.native.staking_pool_mappings.id.into()) } /// Size of the stake pool mappings `Table`. async fn staking_pool_mappings_size(&self) -> Option { - Some(self.native.staking_pool_mappings.size) + Some(self.contents.native.staking_pool_mappings.size) } /// Total amount of stake for all active validators at the beginning of the epoch. async fn total_stake(&self) -> Option { - Some(self.native.total_stake.into()) + Some(self.contents.native.total_stake.into()) } /// Object ID of the `Table` storing the validator candidates. async fn validator_candidates_id(&self) -> Option { - Some(self.native.validator_candidates.id.into()) + Some(self.contents.native.validator_candidates.id.into()) } /// Size of the validator candidates `Table`. async fn validator_candidates_size(&self) -> Option { - Some(self.native.validator_candidates.size) + Some(self.contents.native.validator_candidates.size) } } impl ValidatorSet { - pub(crate) fn from_validator_set_v1(scope: Scope, native: ValidatorSetV1) -> Self { - Self { scope, native } + pub(crate) fn from_validator_set_v1( + scope: Scope, + native: ValidatorSetV1, + report_records: VecMap>, + ) -> Self { + let address_to_index: BTreeMap<_, _> = native + .active_validators + .iter() + .enumerate() + .map(|(i, v)| (v.metadata.sui_address, i)) + .collect(); + let report_records = report_records + .contents + .into_iter() + .map(|Entry { key, value }| { + ( + key, + value + .contents + .into_iter() + .map(|v| address_to_index[&v]) + .collect(), + ) + }) + .collect(); + Self { + contents: Arc::new(ValidatorSetContents { + scope, + native, + report_records, + }), + } } } diff --git a/crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__schema.graphql.snap b/crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__schema.graphql.snap index 77ffbde3fae8e..1969498951c06 100644 --- a/crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__schema.graphql.snap +++ b/crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__schema.graphql.snap @@ -4132,6 +4132,10 @@ type Validator implements IAddressable { """ projectUrl: String """ + Other validators this validator has reported. + """ + reportRecords(first: Int, before: String, last: Int, after: String): ValidatorConnection + """ The epoch stake rewards will be added here at the end of each epoch. """ rewardsPool: BigInt diff --git a/crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__staging.graphql.snap b/crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__staging.graphql.snap index 77ffbde3fae8e..1969498951c06 100644 --- a/crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__staging.graphql.snap +++ b/crates/sui-indexer-alt-graphql/src/snapshots/sui_indexer_alt_graphql__tests__staging.graphql.snap @@ -4132,6 +4132,10 @@ type Validator implements IAddressable { """ projectUrl: String """ + Other validators this validator has reported. + """ + reportRecords(first: Int, before: String, last: Int, after: String): ValidatorConnection + """ The epoch stake rewards will be added here at the end of each epoch. """ rewardsPool: BigInt diff --git a/crates/sui-indexer-alt-graphql/staging.graphql b/crates/sui-indexer-alt-graphql/staging.graphql index ac8839e3c9ce3..af32a0fdc8a40 100644 --- a/crates/sui-indexer-alt-graphql/staging.graphql +++ b/crates/sui-indexer-alt-graphql/staging.graphql @@ -4128,6 +4128,10 @@ type Validator implements IAddressable { """ projectUrl: String """ + Other validators this validator has reported. + """ + reportRecords(first: Int, before: String, last: Int, after: String): ValidatorConnection + """ The epoch stake rewards will be added here at the end of each epoch. """ rewardsPool: BigInt