Skip to main content

mina_node/stats/
stats_block_producer.rs

1use std::collections::{BTreeMap, VecDeque};
2
3use ledger::AccountIndex;
4use mina_core::block::{AppliedBlock, ArcBlockWithHash};
5use mina_p2p_messages::v2;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    block_producer::{BlockProducerWonSlot, BlockProducerWonSlotDiscardReason, BlockWithoutProof},
10    core::block::BlockHash,
11};
12
13const MAX_HISTORY: usize = 2048;
14
15#[derive(Serialize, Deserialize, Debug, Default, Clone)]
16pub struct BlockProducerStats {
17    pub(super) attempts: VecDeque<BlockProductionAttempt>,
18    pub vrf_evaluator: BTreeMap<u32, VrfEvaluatorStats>,
19    pub last_produced_block: Option<ArcBlockWithHash>,
20}
21
22#[derive(Serialize, Deserialize, Debug, Clone)]
23#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
24pub struct BlockProductionAttempt {
25    pub won_slot: BlockProductionAttemptWonSlot,
26    pub block: Option<ProducedBlock>,
27    pub times: BlockProductionTimes,
28    #[serde(flatten)]
29    pub status: BlockProductionStatus,
30}
31
32#[derive(Serialize, Deserialize, Debug, Clone)]
33#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
34pub struct BlockProductionAttemptWonSlot {
35    pub slot_time: redux::Timestamp,
36    pub global_slot: u32,
37    pub epoch: u32,
38    #[cfg_attr(feature = "openapi", schema(value_type = (String, String)))]
39    pub delegator: (v2::NonZeroCurvePoint, AccountIndex),
40    pub value_with_threshold: Option<(f64, f64)>,
41}
42
43#[derive(Serialize, Deserialize, Debug, Clone)]
44#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
45pub struct BlockProductionTimes {
46    pub scheduled: redux::Timestamp,
47    pub staged_ledger_diff_create_start: Option<redux::Timestamp>,
48    pub staged_ledger_diff_create_end: Option<redux::Timestamp>,
49    pub produced: Option<redux::Timestamp>,
50    pub proof_create_start: Option<redux::Timestamp>,
51    pub proof_create_end: Option<redux::Timestamp>,
52    pub block_apply_start: Option<redux::Timestamp>,
53    pub block_apply_end: Option<redux::Timestamp>,
54    pub committed: Option<redux::Timestamp>,
55    pub discarded: Option<redux::Timestamp>,
56}
57
58#[derive(Serialize, Deserialize, Debug, Clone)]
59#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
60#[serde(tag = "status")]
61pub enum BlockProductionStatus {
62    Scheduled,
63    StagedLedgerDiffCreatePending,
64    StagedLedgerDiffCreateSuccess,
65    Produced,
66    ProofCreatePending,
67    ProofCreateSuccess,
68    BlockApplyPending,
69    BlockApplySuccess,
70    Committed,
71    Canonical {
72        last_observed_confirmations: u32,
73    },
74    Orphaned {
75        orphaned_by: BlockHash,
76    },
77    Discarded {
78        discard_reason: BlockProducerWonSlotDiscardReason,
79    },
80}
81
82#[derive(Serialize, Deserialize, Debug, Clone)]
83#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
84pub struct ProducedBlock {
85    pub hash: BlockHash,
86    pub height: u32,
87    pub transactions: ProducedBlockTransactions,
88    pub completed_works_count: usize,
89    pub coinbase: u64,
90    pub fees: u64,
91    pub snark_fees: u64,
92}
93
94#[derive(Serialize, Deserialize, Debug, Default, Clone)]
95#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
96pub struct ProducedBlockTransactions {
97    pub payments: u16,
98    pub delegations: u16,
99    pub zkapps: u16,
100}
101
102#[derive(Serialize, Deserialize, Debug, Clone)]
103#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
104pub struct VrfEvaluatorStats {
105    pub total_slots: u32,
106    pub evaluated_slots: u32,
107}
108
109impl Default for VrfEvaluatorStats {
110    fn default() -> Self {
111        Self {
112            total_slots: 7140,
113            evaluated_slots: 0,
114        }
115    }
116}
117
118impl BlockProducerStats {
119    fn latest_attempt_block_hash_matches(&self, hash: &BlockHash) -> bool {
120        self.attempts
121            .back()
122            .and_then(|v| v.block.as_ref())
123            .is_some_and(|b| &b.hash == hash)
124    }
125
126    pub fn collect_attempts(&self) -> Vec<BlockProductionAttempt> {
127        self.attempts.iter().cloned().collect()
128    }
129
130    pub fn new_best_chain(&mut self, time: redux::Timestamp, chain: &[AppliedBlock]) {
131        let (best_tip, chain) = chain.split_last().unwrap();
132        let root_block = chain.first().unwrap_or(best_tip);
133
134        self.committed(time, best_tip.hash());
135
136        self.attempts
137            .iter_mut()
138            .rev()
139            .take_while(|v| v.won_slot.global_slot >= root_block.global_slot())
140            .filter(|attempt| {
141                matches!(
142                    attempt.status,
143                    BlockProductionStatus::Committed
144                        | BlockProductionStatus::Canonical { .. }
145                        | BlockProductionStatus::Orphaned { .. }
146                )
147            })
148            .for_each(|attempt| {
149                let Some(block) = attempt.block.as_ref() else {
150                    return;
151                };
152                let Some(i) = block.height.checked_sub(root_block.height()) else {
153                    return;
154                };
155
156                match chain.get(i as usize) {
157                    Some(b) if b.hash() == &block.hash => {
158                        attempt.status = BlockProductionStatus::Canonical {
159                            last_observed_confirmations: best_tip
160                                .height()
161                                .saturating_sub(block.height),
162                        };
163                    }
164                    Some(b) => {
165                        attempt.status = BlockProductionStatus::Orphaned {
166                            orphaned_by: b.hash().clone(),
167                        };
168                    }
169                    None => {}
170                }
171            });
172    }
173
174    fn update<F>(&mut self, kind: &'static str, with: F)
175    where
176        F: FnOnce(&mut BlockProductionAttempt) -> bool,
177    {
178        match self.attempts.pop_back() {
179            None => {
180                mina_core::log::error!(mina_core::log::system_time();
181                    kind = "BlockProducerStatsAttemptsEmpty",
182                    summary = "attempts are empty when they aren't expected to be",
183                    update_kind = kind);
184            }
185            Some(mut attempt) => {
186                let was_correct_state = with(&mut attempt);
187
188                if !was_correct_state {
189                    mina_core::log::error!(mina_core::log::system_time();
190                        kind = "BlockProducerStatsAttemptUnexpectedState",
191                        summary = format!("update kind `{kind}` is not applicable to state: {attempt:?}"));
192                }
193                self.attempts.push_back(attempt);
194            }
195        }
196    }
197
198    pub fn scheduled(&mut self, time: redux::Timestamp, won_slot: &BlockProducerWonSlot) {
199        if self.attempts.len() >= MAX_HISTORY {
200            self.attempts.pop_front();
201        }
202        self.attempts.push_back(BlockProductionAttempt {
203            won_slot: won_slot.into(),
204            block: None,
205            times: BlockProductionTimes {
206                scheduled: time,
207                staged_ledger_diff_create_start: None,
208                staged_ledger_diff_create_end: None,
209                produced: None,
210                proof_create_start: None,
211                proof_create_end: None,
212                block_apply_start: None,
213                block_apply_end: None,
214                committed: None,
215                discarded: None,
216            },
217            status: BlockProductionStatus::Scheduled,
218        });
219    }
220
221    pub fn staged_ledger_diff_create_start(&mut self, time: redux::Timestamp) {
222        self.update(
223            "staged_ledger_diff_create_start",
224            move |attempt| match attempt.status {
225                BlockProductionStatus::Scheduled => {
226                    attempt.status = BlockProductionStatus::StagedLedgerDiffCreatePending;
227                    attempt.times.staged_ledger_diff_create_start = Some(time);
228                    true
229                }
230                _ => false,
231            },
232        );
233    }
234
235    pub fn staged_ledger_diff_create_end(&mut self, time: redux::Timestamp) {
236        self.update(
237            "staged_ledger_diff_create_end",
238            move |attempt| match attempt.status {
239                BlockProductionStatus::StagedLedgerDiffCreatePending => {
240                    attempt.status = BlockProductionStatus::StagedLedgerDiffCreateSuccess;
241                    attempt.times.staged_ledger_diff_create_end = Some(time);
242                    true
243                }
244                _ => false,
245            },
246        );
247    }
248
249    pub fn produced(
250        &mut self,
251        time: redux::Timestamp,
252        block_hash: &BlockHash,
253        block: &BlockWithoutProof,
254    ) {
255        self.update("produced", move |attempt| match attempt.status {
256            BlockProductionStatus::StagedLedgerDiffCreateSuccess => {
257                attempt.status = BlockProductionStatus::Produced;
258                attempt.times.produced = Some(time);
259                attempt.block = Some((block_hash, block).into());
260                true
261            }
262            _ => false,
263        });
264    }
265
266    pub fn proof_create_start(&mut self, time: redux::Timestamp) {
267        self.update("proof_create_start", move |attempt| match attempt.status {
268            BlockProductionStatus::Produced => {
269                attempt.status = BlockProductionStatus::ProofCreatePending;
270                attempt.times.proof_create_start = Some(time);
271                true
272            }
273            _ => false,
274        });
275    }
276
277    pub fn proof_create_end(&mut self, time: redux::Timestamp) {
278        self.update("proof_create_end", move |attempt| match attempt.status {
279            BlockProductionStatus::ProofCreatePending => {
280                attempt.status = BlockProductionStatus::ProofCreateSuccess;
281                attempt.times.proof_create_end = Some(time);
282                true
283            }
284            _ => false,
285        });
286    }
287
288    pub fn block_apply_start(&mut self, time: redux::Timestamp, hash: &BlockHash) {
289        if !self.is_our_just_produced_block(hash) {
290            return;
291        }
292
293        self.update("block_apply_start", move |attempt| match attempt.status {
294            BlockProductionStatus::ProofCreateSuccess => {
295                attempt.status = BlockProductionStatus::BlockApplyPending;
296                attempt.times.block_apply_start = Some(time);
297                true
298            }
299            _ => false,
300        });
301    }
302
303    pub fn block_apply_end(&mut self, time: redux::Timestamp, hash: &BlockHash) {
304        if !self.latest_attempt_block_hash_matches(hash) {
305            return;
306        }
307
308        self.update("block_apply_end", move |attempt| match attempt.status {
309            BlockProductionStatus::BlockApplyPending => {
310                attempt.status = BlockProductionStatus::BlockApplySuccess;
311                attempt.times.block_apply_end = Some(time);
312                true
313            }
314            _ => false,
315        });
316    }
317
318    pub fn committed(&mut self, time: redux::Timestamp, hash: &BlockHash) {
319        if !self.latest_attempt_block_hash_matches(hash) {
320            return;
321        }
322
323        self.update("committed", move |attempt| match attempt.status {
324            BlockProductionStatus::BlockApplySuccess => {
325                attempt.status = BlockProductionStatus::Committed;
326                attempt.times.committed = Some(time);
327                true
328            }
329            _ => false,
330        });
331    }
332
333    pub fn discarded(&mut self, time: redux::Timestamp, reason: BlockProducerWonSlotDiscardReason) {
334        self.update("discarded", move |attempt| {
335            attempt.status = BlockProductionStatus::Discarded {
336                discard_reason: reason,
337            };
338            attempt.times.discarded = Some(time);
339            true
340        });
341    }
342
343    /// Returns `true` if this is a block we just produced
344    pub fn is_our_just_produced_block(&self, hash: &BlockHash) -> bool {
345        // For the block to be ours:
346        // - we must have an attempt to produce a block
347        // - we must have just produced the proof for that block
348        // - the hash must match
349        if let Some(attempt) = self.attempts.back() {
350            match (&attempt.status, attempt.block.as_ref()) {
351                (BlockProductionStatus::ProofCreateSuccess, Some(block)) => &block.hash == hash,
352                _ => false,
353            }
354        } else {
355            false
356        }
357    }
358
359    /// In case a new run, when the current epoch has less than `slots_per_epoch` slots to evaluate.
360    pub fn new_epoch_evaluation(&mut self, epoch: u32, remaining_slots: u32) {
361        self.vrf_evaluator.insert(
362            epoch,
363            VrfEvaluatorStats {
364                total_slots: remaining_slots,
365                evaluated_slots: 0,
366            },
367        );
368    }
369
370    pub fn increment_slot_evaluated(&mut self, epoch: u32) {
371        self.vrf_evaluator
372            .entry(epoch)
373            .and_modify(|v| v.evaluated_slots = v.evaluated_slots.checked_add(1).expect("overflow"))
374            .or_insert_with(|| VrfEvaluatorStats {
375                evaluated_slots: 1,
376                ..Default::default()
377            });
378    }
379}
380
381impl From<&BlockProducerWonSlot> for BlockProductionAttemptWonSlot {
382    fn from(won_slot: &BlockProducerWonSlot) -> Self {
383        Self {
384            slot_time: won_slot.slot_time,
385            global_slot: won_slot.global_slot(),
386            epoch: won_slot.epoch(),
387            delegator: won_slot.delegator.clone(),
388            value_with_threshold: won_slot.value_with_threshold,
389        }
390    }
391}
392
393impl From<(&BlockHash, &BlockWithoutProof)> for ProducedBlock {
394    fn from((block_hash, block): (&BlockHash, &BlockWithoutProof)) -> Self {
395        Self {
396            hash: block_hash.clone(),
397            height: block
398                .protocol_state
399                .body
400                .consensus_state
401                .blockchain_length
402                .as_u32(),
403            transactions: block.into(),
404            completed_works_count: block.body.completed_works_count(),
405            coinbase: if block.body.has_coinbase() {
406                mina_core::constants::constraint_constants().coinbase_amount
407            } else {
408                0
409            },
410            fees: block.body.fees_sum(),
411            snark_fees: block.body.snark_fees_sum(),
412        }
413    }
414}
415
416impl From<&BlockWithoutProof> for ProducedBlockTransactions {
417    fn from(block: &BlockWithoutProof) -> Self {
418        block
419            .body
420            .commands_iter()
421            .fold(Self::default(), |mut res, cmd| {
422                match &cmd.data {
423                    v2::MinaBaseUserCommandStableV2::SignedCommand(v) => match &v.payload.body {
424                        v2::MinaBaseSignedCommandPayloadBodyStableV2::Payment(_) => {
425                            res.payments = res.payments.checked_add(1).expect("overflow")
426                        }
427                        v2::MinaBaseSignedCommandPayloadBodyStableV2::StakeDelegation(_) => {
428                            res.delegations = res.delegations.checked_add(1).expect("overflow")
429                        }
430                    },
431                    v2::MinaBaseUserCommandStableV2::ZkappCommand(_) => {
432                        res.zkapps = res.zkapps.checked_add(1).expect("overflow")
433                    }
434                }
435                res
436            })
437    }
438}