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 pub fn is_our_just_produced_block(&self, hash: &BlockHash) -> bool {
345 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 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}