Skip to main content

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