mithril_ticker/
ticker_service.rs

1//! ## Ticker Service
2//!
3//! This service read time information from the chain and helps create beacons
4//! for every message types.
5
6use anyhow::{anyhow, Context};
7use async_trait::async_trait;
8use std::sync::Arc;
9use thiserror::Error;
10
11use mithril_cardano_node_chain::chain_observer::ChainObserver;
12use mithril_cardano_node_internal_database::ImmutableFileObserver;
13use mithril_common::entities::{Epoch, TimePoint};
14use mithril_common::StdResult;
15
16/// ## TickerService
17///
18/// This service is responsible for giving the right time information to other
19/// services. It reads data either from the Chain or the filesystem to create
20/// beacons for each message type.
21#[async_trait]
22pub trait TickerService
23where
24    Self: Sync + Send,
25{
26    /// Get the current [Epoch] of the cardano node.
27    async fn get_current_epoch(&self) -> StdResult<Epoch> {
28        self.get_current_time_point()
29            .await
30            .map(|time_point| time_point.epoch)
31    }
32
33    /// Get the current [TimePoint] of the cardano node.
34    async fn get_current_time_point(&self) -> StdResult<TimePoint>;
35}
36
37/// [TickerService] related errors.
38#[derive(Error, Debug)]
39pub enum TickerServiceError {
40    /// Raised when reading the current epoch succeeded but yielded no result.
41    #[error("No epoch yielded by the chain observer, is your cardano node ready?")]
42    NoEpoch,
43
44    /// Raised when reading the current chain point succeeded but yielded no result.
45    #[error("No chain point yielded by the chain observer, is your cardano node ready?")]
46    NoChainPoint,
47}
48
49/// A [TickerService] using a [ChainObserver] and a [ImmutableFileObserver].
50pub struct MithrilTickerService {
51    chain_observer: Arc<dyn ChainObserver>,
52    immutable_observer: Arc<dyn ImmutableFileObserver>,
53}
54
55impl MithrilTickerService {
56    /// [MithrilTickerService] factory.
57    pub fn new(
58        chain_observer: Arc<dyn ChainObserver>,
59        immutable_observer: Arc<dyn ImmutableFileObserver>,
60    ) -> Self {
61        Self {
62            chain_observer,
63            immutable_observer,
64        }
65    }
66}
67
68#[async_trait]
69impl TickerService for MithrilTickerService {
70    async fn get_current_time_point(&self) -> StdResult<TimePoint> {
71        let epoch = self
72            .chain_observer
73            .get_current_epoch()
74            .await
75            .map_err(|e| anyhow!(e))
76            .with_context(|| "TimePoint Provider can not get current epoch")?
77            .ok_or(TickerServiceError::NoEpoch)?;
78
79        let immutable_file_number = self
80            .immutable_observer
81            .get_last_immutable_number()
82            .await
83            .with_context(|| {
84                format!(
85                    "TimePoint Provider can not get last immutable file number for epoch: '{epoch}'"
86                )
87            })?;
88
89        let chain_point = self
90            .chain_observer
91            .get_current_chain_point()
92            .await
93            .map_err(|e| anyhow!(e))
94            .with_context(|| "TimePoint Provider can not get current chain point")?
95            .ok_or(TickerServiceError::NoChainPoint)?;
96
97        Ok(TimePoint {
98            epoch,
99            immutable_file_number,
100            chain_point,
101        })
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use anyhow::anyhow;
108
109    use mithril_cardano_node_chain::chain_observer::{ChainObserver, ChainObserverError};
110    use mithril_cardano_node_chain::entities::{ChainAddress, TxDatum};
111    use mithril_cardano_node_internal_database::test::double::DumbImmutableFileObserver;
112    use mithril_common::entities::{BlockNumber, ChainPoint, Epoch, SlotNumber, StakeDistribution};
113
114    use super::*;
115
116    struct DumbChainObserver {}
117
118    #[async_trait]
119    impl ChainObserver for DumbChainObserver {
120        async fn get_current_datums(
121            &self,
122            _address: &ChainAddress,
123        ) -> Result<Vec<TxDatum>, ChainObserverError> {
124            Ok(Vec::new())
125        }
126
127        async fn get_current_era(&self) -> Result<Option<String>, ChainObserverError> {
128            Ok(Some(String::new()))
129        }
130
131        async fn get_current_epoch(&self) -> Result<Option<Epoch>, ChainObserverError> {
132            Ok(Some(Epoch(42)))
133        }
134
135        async fn get_current_chain_point(&self) -> Result<Option<ChainPoint>, ChainObserverError> {
136            Ok(Some(ChainPoint {
137                slot_number: SlotNumber(800),
138                block_number: BlockNumber(51),
139                block_hash: "1b69b3202fbe500".to_string(),
140            }))
141        }
142
143        async fn get_current_stake_distribution(
144            &self,
145        ) -> Result<Option<StakeDistribution>, ChainObserverError> {
146            Err(ChainObserverError::General(anyhow!(
147                "this should not be called in the TimePointProvider"
148            )))
149        }
150    }
151
152    #[tokio::test]
153    async fn test_get_current_epoch() {
154        let ticker_service = MithrilTickerService::new(
155            Arc::new(DumbChainObserver {}),
156            Arc::new(DumbImmutableFileObserver::default()),
157        );
158        let epoch = ticker_service.get_current_epoch().await.unwrap();
159
160        assert_eq!(Epoch(42), epoch);
161    }
162
163    #[tokio::test]
164    async fn test_happy_path() {
165        let ticker_service = MithrilTickerService::new(
166            Arc::new(DumbChainObserver {}),
167            Arc::new(DumbImmutableFileObserver::default()),
168        );
169        let time_point = ticker_service.get_current_time_point().await.unwrap();
170
171        assert_eq!(
172            TimePoint::new(
173                42,
174                500,
175                ChainPoint {
176                    slot_number: SlotNumber(800),
177                    block_number: BlockNumber(51),
178                    block_hash: "1b69b3202fbe500".to_string(),
179                },
180            ),
181            time_point
182        );
183    }
184
185    #[tokio::test]
186    async fn test_error_from_dependency() {
187        let immutable_observer = DumbImmutableFileObserver::default();
188        immutable_observer.shall_return(None).await;
189        let ticker_service =
190            MithrilTickerService::new(Arc::new(DumbChainObserver {}), Arc::new(immutable_observer));
191
192        let result = ticker_service.get_current_time_point().await;
193        assert!(result.is_err());
194    }
195}