mina_producer_dashboard/evaluator/
epoch.rs

1use std::ops::AddAssign;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    archive::{postgres_types::ChainStatus, Block},
7    node::epoch_ledgers::{Balances, NanoMina},
8};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct EpochSlots {
12    inner: Vec<SlotData>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct MergedSummary {
17    epoch_number: u32,
18    summary: Option<EpochSummary>,
19    sub_windows: Vec<EpochSummary>,
20    #[serde(flatten)]
21    balances: Balances,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct AllTimeSummary {
26    #[serde(flatten)]
27    slot_summary: SlotSummary,
28}
29
30impl EpochSlots {
31    pub fn new(inner: Vec<SlotData>) -> Self {
32        Self { inner }
33    }
34
35    pub fn merged_summary(&self, epoch_number: u32, balances: Balances) -> MergedSummary {
36        if self.inner.is_empty() {
37            MergedSummary {
38                epoch_number,
39                summary: None,
40                sub_windows: vec![],
41                balances: Balances::default(),
42            }
43        } else {
44            let summary = self.summary();
45            MergedSummary {
46                epoch_number,
47                summary: Some(summary),
48                sub_windows: self.sub_windows(),
49                balances,
50            }
51        }
52    }
53
54    pub fn sub_windows(&self) -> Vec<EpochSummary> {
55        let chunk_size = self.inner.len() / 15;
56
57        self.inner
58            .chunks_exact(chunk_size)
59            .map(|window| EpochSlots::new(window.to_vec()).summary())
60            .collect()
61    }
62
63    pub fn slot_summary(&self) -> (SlotSummary, bool) {
64        let mut slot_summary = SlotSummary::default();
65        let mut is_current = false;
66        for slot in self.inner.iter() {
67            if slot.is_current_slot {
68                is_current = true;
69            }
70
71            match slot.block_status {
72                SlotStatus::Canonical | SlotStatus::CanonicalPending => {
73                    slot_summary.won_slots += 1;
74                    slot_summary.canonical += 1;
75                    slot_summary.earned_rewards += NanoMina::new(720.into());
76                }
77                SlotStatus::Missed => {
78                    slot_summary.won_slots += 1;
79                    slot_summary.missed += 1;
80                }
81                SlotStatus::Orphaned | SlotStatus::OrphanedPending => {
82                    slot_summary.won_slots += 1;
83                    slot_summary.orphaned += 1;
84                }
85                SlotStatus::ToBeProduced => {
86                    slot_summary.won_slots += 1;
87                    slot_summary.future_rights += 1;
88                }
89                SlotStatus::Pending
90                | SlotStatus::Lost
91                | SlotStatus::Empty
92                | SlotStatus::Foreign
93                | SlotStatus::ForeignToBeProduced => {
94                    // Handle other statuses if necessary
95                }
96            }
97        }
98        slot_summary.expected_rewards = NanoMina::new((slot_summary.won_slots * 720).into());
99        (slot_summary, is_current)
100    }
101
102    fn summary(&self) -> EpochSummary {
103        let (slot_summary, is_current) = self.slot_summary();
104
105        let slot_start = self.inner.first().unwrap().global_slot.to_u32();
106        let slot_end = self.inner.last().unwrap().global_slot.to_u32();
107
108        EpochSummary {
109            max: slot_summary.won_slots,
110            slot_summary,
111            slot_start,
112            slot_end,
113            is_current,
114        }
115    }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct EpochSummary {
120    max: usize,
121    #[serde(flatten)]
122    slot_summary: SlotSummary,
123    slot_start: u32,
124    slot_end: u32,
125    is_current: bool,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
129pub struct SlotSummary {
130    won_slots: usize,
131    canonical: usize,
132    orphaned: usize,
133    missed: usize,
134    future_rights: usize,
135    expected_rewards: NanoMina,
136    earned_rewards: NanoMina,
137}
138
139impl AddAssign for SlotSummary {
140    fn add_assign(&mut self, rhs: Self) {
141        *self = Self {
142            won_slots: self.won_slots + rhs.won_slots,
143            canonical: self.canonical + rhs.canonical,
144            orphaned: self.orphaned + rhs.orphaned,
145            missed: self.missed + rhs.missed,
146            future_rights: self.future_rights + rhs.future_rights,
147            expected_rewards: self.expected_rewards.clone() + rhs.expected_rewards,
148            earned_rewards: self.earned_rewards.clone() + rhs.earned_rewards,
149        }
150    }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub enum SlotStatus {
155    Canonical,
156    CanonicalPending,
157    Missed,
158    Orphaned,
159    OrphanedPending,
160    Pending,
161    ToBeProduced,
162    Lost,
163    Empty,
164    Foreign,
165    ForeignToBeProduced,
166}
167
168impl From<ChainStatus> for SlotStatus {
169    fn from(value: ChainStatus) -> Self {
170        match value {
171            ChainStatus::Canonical => SlotStatus::Canonical,
172            ChainStatus::Orphaned => SlotStatus::Orphaned,
173            ChainStatus::Pending => SlotStatus::Pending,
174        }
175    }
176}
177
178impl SlotStatus {
179    pub fn in_transition_frontier(&self) -> bool {
180        matches!(
181            self,
182            SlotStatus::CanonicalPending | SlotStatus::OrphanedPending
183        )
184    }
185}
186
187// TODO: better naming
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct RawSlot(u32);
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
192pub struct RawGlobalSlot(u32);
193
194impl From<RawGlobalSlot> for RawSlot {
195    fn from(value: RawGlobalSlot) -> Self {
196        RawSlot(value.0 % 7140)
197    }
198}
199
200impl From<u32> for RawSlot {
201    fn from(value: u32) -> Self {
202        RawSlot(value)
203    }
204}
205
206impl From<u32> for RawGlobalSlot {
207    fn from(value: u32) -> Self {
208        RawGlobalSlot(value)
209    }
210}
211
212impl RawGlobalSlot {
213    pub fn to_u32(&self) -> u32 {
214        self.0
215    }
216
217    pub fn epoch(&self) -> u32 {
218        self.0 / 7140
219    }
220}
221
222// TODO(adonagy): move to its own module
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct SlotData {
225    slot: RawSlot,
226    global_slot: RawGlobalSlot,
227    block_status: SlotStatus,
228    timestamp: i64,
229    state_hash: Option<String>,
230    height: Option<u32>,
231    is_current_slot: bool,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct SlotBlockUpdate {
236    height: u32,
237    state_hash: String,
238    block_status: SlotStatus,
239}
240
241impl SlotBlockUpdate {
242    pub fn new(height: u32, state_hash: String, block_status: SlotStatus) -> Self {
243        Self {
244            height,
245            state_hash,
246            block_status,
247        }
248    }
249}
250
251impl From<Block> for SlotBlockUpdate {
252    fn from(value: Block) -> Self {
253        Self {
254            height: value.height as u32,
255            state_hash: value.state_hash,
256            block_status: value.chain_status.into(),
257        }
258    }
259}
260
261impl From<&Block> for SlotBlockUpdate {
262    fn from(value: &Block) -> Self {
263        value.clone().into()
264    }
265}
266
267impl SlotData {
268    pub fn new(global_slot: u32, timestamp: i64, block: Option<SlotBlockUpdate>) -> Self {
269        let block_status = block
270            .clone()
271            .map_or(SlotStatus::ToBeProduced, |block| block.block_status);
272        let state_hash = block.clone().map(|block| block.state_hash);
273        let height = block.map(|block| block.height);
274        let global_slot: RawGlobalSlot = global_slot.into();
275
276        Self {
277            slot: global_slot.clone().into(),
278            global_slot,
279            block_status,
280            state_hash,
281            height,
282            timestamp,
283            is_current_slot: false,
284        }
285    }
286
287    pub fn global_slot(&self) -> RawGlobalSlot {
288        self.global_slot.clone()
289    }
290
291    pub fn has_block(&self) -> bool {
292        self.state_hash.is_some()
293    }
294
295    pub fn block_status(&self) -> SlotStatus {
296        self.block_status.clone()
297    }
298
299    pub fn new_lost(global_slot: u32, timestamp: i64) -> Self {
300        let global_slot: RawGlobalSlot = global_slot.into();
301        Self {
302            slot: global_slot.clone().into(),
303            global_slot,
304            block_status: SlotStatus::Empty,
305            timestamp,
306            state_hash: None,
307            height: None,
308            is_current_slot: false,
309        }
310    }
311
312    pub fn add_block(&mut self, block: SlotBlockUpdate) {
313        self.state_hash = Some(block.state_hash);
314        self.height = Some(block.height);
315        self.block_status = block.block_status;
316    }
317
318    pub fn update_block_status(&mut self, block_status: SlotStatus) {
319        self.block_status = block_status;
320    }
321
322    pub fn set_as_current(&mut self) {
323        self.is_current_slot = true;
324    }
325
326    pub fn unset_as_current(&mut self) {
327        self.is_current_slot = false;
328    }
329}