sui_rpc_api/grpc/v2/ledger_service/
get_epoch.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::ErrorReason;
5use crate::Result;
6use crate::RpcService;
7use prost_types::FieldMask;
8use sui_rpc::field::FieldMaskTree;
9use sui_rpc::field::FieldMaskUtil;
10use sui_rpc::merge::Merge;
11use sui_rpc::proto::google::rpc::bad_request::FieldViolation;
12use sui_rpc::proto::sui::rpc::v2::Epoch;
13use sui_rpc::proto::sui::rpc::v2::GetEpochRequest;
14use sui_rpc::proto::sui::rpc::v2::GetEpochResponse;
15use sui_rpc::proto::sui::rpc::v2::ProtocolConfig;
16use sui_rpc::proto::timestamp_ms_to_proto;
17use sui_sdk_types::EpochId;
18use sui_types::sui_system_state::SuiSystemStateTrait;
19
20pub const READ_MASK_DEFAULT: &str = crate::read_mask_defaults::EPOCH;
21
22#[tracing::instrument(skip(service))]
23pub fn get_epoch(service: &RpcService, request: GetEpochRequest) -> Result<GetEpochResponse> {
24    let read_mask = {
25        let read_mask = request
26            .read_mask
27            .unwrap_or_else(|| FieldMask::from_str(READ_MASK_DEFAULT));
28        read_mask.validate::<Epoch>().map_err(|path| {
29            FieldViolation::new("read_mask")
30                .with_description(format!("invalid read_mask path: {path}"))
31                .with_reason(ErrorReason::FieldInvalid)
32        })?;
33        FieldMaskTree::from(read_mask)
34    };
35
36    let mut message = Epoch::default();
37
38    let current_system_state = service.reader.get_system_state()?;
39    let current_epoch = current_system_state.epoch();
40
41    let epoch = request.epoch.unwrap_or(current_epoch);
42
43    if read_mask.contains(Epoch::EPOCH_FIELD.name) {
44        message.set_epoch(epoch);
45    }
46
47    // Fetch epoch info, if indexing is available.
48    let mut epoch_info = service
49        .reader
50        .inner()
51        .indexes()
52        .and_then(|indexes| indexes.get_epoch_info(epoch).ok().flatten());
53
54    let system_state = if epoch == current_epoch {
55        Some(current_system_state)
56    } else {
57        epoch_info
58            .as_mut()
59            .and_then(|info| info.system_state.take())
60    };
61
62    if let Some(system_state) = system_state {
63        if let Some(submask) = read_mask.subtree(Epoch::PROTOCOL_CONFIG_FIELD) {
64            let chain = service.reader.inner().get_chain_identifier()?.chain();
65            let config = get_protocol_config(system_state.protocol_version(), chain)?;
66
67            message.set_protocol_config(ProtocolConfig::merge_from(config, &submask));
68        }
69
70        if read_mask.contains(Epoch::START_FIELD) {
71            message.set_start(timestamp_ms_to_proto(
72                system_state.epoch_start_timestamp_ms(),
73            ));
74        }
75
76        if read_mask.contains(Epoch::REFERENCE_GAS_PRICE_FIELD) {
77            message.set_reference_gas_price(system_state.reference_gas_price());
78        }
79
80        if read_mask.contains(Epoch::SYSTEM_STATE_FIELD) {
81            message.system_state = Some(Box::new(system_state.into()));
82        }
83    }
84
85    if let Some(epoch_info) = epoch_info {
86        if read_mask.contains(Epoch::FIRST_CHECKPOINT_FIELD) {
87            message.first_checkpoint = epoch_info.start_checkpoint;
88        }
89
90        if read_mask.contains(Epoch::LAST_CHECKPOINT_FIELD) {
91            message.last_checkpoint = epoch_info.end_checkpoint;
92        }
93
94        if read_mask.contains(Epoch::START_FIELD) && message.start.is_none() {
95            message.start = epoch_info.start_timestamp_ms.map(timestamp_ms_to_proto);
96        }
97
98        if read_mask.contains(Epoch::END_FIELD) {
99            message.end = epoch_info.end_timestamp_ms.map(timestamp_ms_to_proto);
100        }
101
102        if read_mask.contains(Epoch::REFERENCE_GAS_PRICE_FIELD.name)
103            && message.reference_gas_price.is_none()
104        {
105            message.reference_gas_price = epoch_info.reference_gas_price;
106        }
107
108        if let Some(submask) = read_mask.subtree(Epoch::PROTOCOL_CONFIG_FIELD.name)
109            && message.protocol_config.is_none()
110        {
111            let chain = service.reader.inner().get_chain_identifier()?.chain();
112            let protocol_config = epoch_info
113                .protocol_version
114                .map(|version| get_protocol_config(version, chain))
115                .transpose()?;
116
117            message.protocol_config =
118                protocol_config.map(|config| ProtocolConfig::merge_from(config, &submask));
119        }
120    }
121
122    if read_mask.contains(Epoch::COMMITTEE_FIELD.name) {
123        message.committee = Some(
124            service
125                .reader
126                .get_committee(epoch)
127                .ok_or_else(|| CommitteeNotFoundError::new(epoch))?
128                .into(),
129        );
130    }
131
132    Ok(GetEpochResponse::new(message))
133}
134
135#[derive(Debug)]
136pub struct CommitteeNotFoundError {
137    epoch: EpochId,
138}
139
140impl CommitteeNotFoundError {
141    pub fn new(epoch: EpochId) -> Self {
142        Self { epoch }
143    }
144}
145
146impl std::fmt::Display for CommitteeNotFoundError {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        write!(f, "Committee for epoch {} not found", self.epoch)
149    }
150}
151
152impl std::error::Error for CommitteeNotFoundError {}
153
154impl From<CommitteeNotFoundError> for crate::RpcError {
155    fn from(value: CommitteeNotFoundError) -> Self {
156        Self::new(tonic::Code::NotFound, value.to_string())
157    }
158}
159
160#[derive(Debug)]
161struct ProtocolVersionNotFoundError {
162    version: u64,
163}
164
165impl ProtocolVersionNotFoundError {
166    pub fn new(version: u64) -> Self {
167        Self { version }
168    }
169}
170
171impl std::fmt::Display for ProtocolVersionNotFoundError {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        write!(f, "Protocol version {} not found", self.version)
174    }
175}
176
177impl std::error::Error for ProtocolVersionNotFoundError {}
178
179impl From<ProtocolVersionNotFoundError> for crate::RpcError {
180    fn from(value: ProtocolVersionNotFoundError) -> Self {
181        Self::new(tonic::Code::NotFound, value.to_string())
182    }
183}
184
185fn get_protocol_config(
186    version: u64,
187    chain: sui_protocol_config::Chain,
188) -> Result<ProtocolConfig, ProtocolVersionNotFoundError> {
189    let config =
190        sui_protocol_config::ProtocolConfig::get_for_version_if_supported(version.into(), chain)
191            .ok_or_else(|| ProtocolVersionNotFoundError::new(version))?;
192    Ok(protocol_config_to_proto(config))
193}
194
195pub fn protocol_config_to_proto(config: sui_protocol_config::ProtocolConfig) -> ProtocolConfig {
196    use prost_types::value::Kind;
197
198    let mut message = ProtocolConfig::default();
199    message.set_protocol_version(config.version.as_u64());
200
201    // Set deprecated feature flags to the exact feature_map the protocol config gives us
202    message.set_feature_flags(config.feature_map());
203
204    // Load configs (today this is just the `attributes`), rendered to a `Value`. Render emits
205    // explicit `NullValue`s for fields unset at this protocol version; filter them out so the
206    // public `configs` map only carries values that are actually configured.
207    let mut configs = config
208        .render::<prost_types::Value>(&mut mysten_common::rpc_format::Unmetered)
209        .expect("render to prost Value should succeed")
210        .into_iter()
211        // filter out NULLs
212        .filter(|(_, v)| !matches!(v.kind, None | Some(Kind::NullValue(_))))
213        .collect::<std::collections::BTreeMap<_, _>>();
214
215    // For backwards compatibility, render attributes to strings, complex types are json
216    // stringified
217    message.set_attributes(
218        configs
219            .iter()
220            .filter_map(|(k, v)| match &v.kind {
221                Some(Kind::NullValue(_)) => None,
222                Some(Kind::NumberValue(n)) => Some((k.to_owned(), n.to_string())),
223                Some(Kind::StringValue(s)) => Some((k.to_owned(), s.to_owned())),
224                Some(Kind::BoolValue(b)) => Some((k.to_owned(), b.to_string())),
225                Some(Kind::StructValue(s)) => Some((
226                    k.to_owned(),
227                    serde_json::to_string(&sui_rpc::_serde::StructSerializer(s)).unwrap(),
228                )),
229                Some(Kind::ListValue(list)) => Some((
230                    k.to_owned(),
231                    serde_json::to_string(&sui_rpc::_serde::ListValueSerializer(list)).unwrap(),
232                )),
233                None => None,
234            })
235            .collect(),
236    );
237
238    // Convert feature flags to a `Value` then merge with other attributes
239    for (k, v) in config
240        .feature_map()
241        .into_iter()
242        .map(|(key, value)| (key, prost_types::Value::from(value)))
243    {
244        let old = configs.insert(k, v);
245
246        debug_assert!(
247            old.is_none(),
248            "feature flags and attributes can't have keys which are the same"
249        );
250    }
251
252    // Set the joined set of attributes and feature flags
253    message.set_configs(configs);
254
255    message
256}