node/block_producer/
block_producer_state.rs

1use std::{collections::BTreeSet, sync::Arc, time::Duration};
2
3use ledger::scan_state::transaction_logic::valid;
4use mina_p2p_messages::v2;
5use openmina_core::{
6    block::{AppliedBlock, ArcBlockWithHash},
7    consensus::consensus_take,
8};
9use serde::{Deserialize, Serialize};
10
11use crate::account::AccountPublicKey;
12
13use super::{
14    vrf_evaluator::BlockProducerVrfEvaluatorState, BlockProducerConfig, BlockProducerWonSlot,
15    BlockWithoutProof,
16};
17
18#[derive(Serialize, Deserialize, Debug, Clone)]
19pub struct BlockProducerState(Option<BlockProducerEnabled>);
20
21#[derive(Serialize, Deserialize, Debug, Clone)]
22pub struct BlockProducerEnabled {
23    pub config: BlockProducerConfig,
24    pub vrf_evaluator: BlockProducerVrfEvaluatorState,
25    pub current: BlockProducerCurrentState,
26    /// Blocks that were injected into transition frontier, but hasn't
27    /// become our best tip yet.
28    pub injected_blocks: BTreeSet<v2::StateHash>,
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone)]
32pub enum BlockProducerCurrentState {
33    Idle {
34        time: redux::Timestamp,
35    },
36    WonSlotDiscarded {
37        time: redux::Timestamp,
38        won_slot: BlockProducerWonSlot,
39        reason: BlockProducerWonSlotDiscardReason,
40    },
41    WonSlot {
42        time: redux::Timestamp,
43        won_slot: BlockProducerWonSlot,
44    },
45    WonSlotWait {
46        time: redux::Timestamp,
47        won_slot: BlockProducerWonSlot,
48    },
49    WonSlotProduceInit {
50        time: redux::Timestamp,
51        won_slot: BlockProducerWonSlot,
52        /// Chain that we are extending.
53        chain: Vec<AppliedBlock>,
54    },
55    WonSlotTransactionsGet {
56        time: redux::Timestamp,
57        won_slot: BlockProducerWonSlot,
58        /// Chain that we are extending.
59        chain: Vec<AppliedBlock>,
60    },
61    WonSlotTransactionsSuccess {
62        time: redux::Timestamp,
63        won_slot: BlockProducerWonSlot,
64        /// Chain that we are extending.
65        chain: Vec<AppliedBlock>,
66        transactions_by_fee: Vec<valid::UserCommand>,
67    },
68    StagedLedgerDiffCreatePending {
69        time: redux::Timestamp,
70        won_slot: BlockProducerWonSlot,
71        /// Chain that we are extending.
72        chain: Vec<AppliedBlock>,
73        transactions_by_fee: Vec<valid::UserCommand>,
74    },
75    StagedLedgerDiffCreateSuccess {
76        time: redux::Timestamp,
77        won_slot: BlockProducerWonSlot,
78        /// Chain that we are extending.
79        chain: Vec<AppliedBlock>,
80        diff: v2::StagedLedgerDiffDiffStableV2,
81        /// `protocol_state.blockchain_state.body_reference`
82        diff_hash: v2::ConsensusBodyReferenceStableV1,
83        staged_ledger_hash: v2::MinaBaseStagedLedgerHashStableV1,
84        emitted_ledger_proof: Option<Arc<v2::LedgerProofProdStableV2>>,
85        pending_coinbase_update: v2::MinaBasePendingCoinbaseUpdateStableV1,
86        pending_coinbase_witness: v2::MinaBasePendingCoinbaseWitnessStableV2,
87        stake_proof_sparse_ledger: v2::MinaBaseSparseLedgerBaseStableV2,
88    },
89    BlockUnprovenBuilt {
90        time: redux::Timestamp,
91        won_slot: BlockProducerWonSlot,
92        /// Chain that we are extending.
93        chain: Vec<AppliedBlock>,
94        emitted_ledger_proof: Option<Arc<v2::LedgerProofProdStableV2>>,
95        pending_coinbase_update: v2::MinaBasePendingCoinbaseUpdateStableV1,
96        pending_coinbase_witness: v2::MinaBasePendingCoinbaseWitnessStableV2,
97        stake_proof_sparse_ledger: v2::MinaBaseSparseLedgerBaseStableV2,
98        block: BlockWithoutProof,
99        block_hash: v2::StateHash,
100    },
101    BlockProvePending {
102        time: redux::Timestamp,
103        won_slot: BlockProducerWonSlot,
104        /// Chain that we are extending.
105        chain: Vec<AppliedBlock>,
106        emitted_ledger_proof: Option<Arc<v2::LedgerProofProdStableV2>>,
107        pending_coinbase_update: v2::MinaBasePendingCoinbaseUpdateStableV1,
108        pending_coinbase_witness: v2::MinaBasePendingCoinbaseWitnessStableV2,
109        stake_proof_sparse_ledger: v2::MinaBaseSparseLedgerBaseStableV2,
110        block: BlockWithoutProof,
111        block_hash: v2::StateHash,
112    },
113    BlockProveSuccess {
114        time: redux::Timestamp,
115        won_slot: BlockProducerWonSlot,
116        /// Chain that we are extending.
117        chain: Vec<AppliedBlock>,
118        block: BlockWithoutProof,
119        block_hash: v2::StateHash,
120        proof: Arc<v2::MinaBaseProofStableV2>,
121    },
122    Produced {
123        time: redux::Timestamp,
124        won_slot: BlockProducerWonSlot,
125        /// Chain that we are extending.
126        chain: Vec<AppliedBlock>,
127        block: ArcBlockWithHash,
128    },
129    Injected {
130        time: redux::Timestamp,
131        won_slot: BlockProducerWonSlot,
132        /// Chain that we are extending.
133        chain: Vec<AppliedBlock>,
134        block: ArcBlockWithHash,
135    },
136}
137
138#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Copy)]
139pub enum BlockProducerWonSlotDiscardReason {
140    BestTipStakingLedgerDifferent,
141    BestTipGlobalSlotHigher,
142    BestTipSuperior,
143}
144
145impl BlockProducerState {
146    pub fn new(now: redux::Timestamp, config: Option<BlockProducerConfig>) -> Self {
147        Self(config.map(|config| BlockProducerEnabled {
148            config: config.clone(),
149            vrf_evaluator: BlockProducerVrfEvaluatorState::new(now),
150            current: BlockProducerCurrentState::Idle { time: now },
151            injected_blocks: Default::default(),
152        }))
153    }
154
155    pub fn with<'a, F, R: 'a>(&'a self, default: R, fun: F) -> R
156    where
157        F: FnOnce(&'a BlockProducerEnabled) -> R,
158    {
159        self.0.as_ref().map_or(default, fun)
160    }
161
162    pub fn as_mut(&mut self) -> Option<&mut BlockProducerEnabled> {
163        self.0.as_mut()
164    }
165
166    pub fn as_ref(&self) -> Option<&BlockProducerEnabled> {
167        self.0.as_ref()
168    }
169
170    pub fn is_enabled(&self) -> bool {
171        self.0.is_some()
172    }
173
174    pub fn config(&self) -> Option<&BlockProducerConfig> {
175        self.with(None, |this| Some(&this.config))
176    }
177
178    pub fn is_me(&self, producer: &v2::NonZeroCurvePoint) -> bool {
179        self.with(false, |this| producer == &this.config.pub_key)
180    }
181
182    /// Checks if the block was produced by us recently.
183    pub fn is_produced_by_me(&self, block: &ArcBlockWithHash) -> bool {
184        self.with(false, |this| {
185            block.producer() == &this.config.pub_key && this.injected_blocks.contains(block.hash())
186        })
187    }
188
189    pub fn is_producing(&self) -> bool {
190        self.with(false, |this| this.current.is_producing())
191    }
192
193    pub fn current_won_slot(&self) -> Option<&BlockProducerWonSlot> {
194        self.with(None, |this| this.current.won_slot())
195    }
196
197    pub fn current_parent_chain(&self) -> Option<&[AppliedBlock]> {
198        self.with(None, |this| this.current.parent_chain())
199    }
200
201    /// Won slot that we are in the middle of producing.
202    pub fn producing_won_slot(&self) -> Option<&BlockProducerWonSlot> {
203        self.current_won_slot().filter(|_| self.is_producing())
204    }
205
206    pub fn produced_block(&self) -> Option<&ArcBlockWithHash> {
207        self.with(None, |this| this.current.produced_block())
208    }
209
210    pub fn produced_block_with_chain(&self) -> Option<(&ArcBlockWithHash, &[AppliedBlock])> {
211        self.with(None, |this| this.current.produced_block_with_chain())
212    }
213
214    pub fn vrf_evaluator(&self) -> Option<&BlockProducerVrfEvaluatorState> {
215        self.with(None, |this| Some(&this.vrf_evaluator))
216    }
217
218    pub fn vrf_evaluator_with_config(
219        &self,
220    ) -> Option<(&BlockProducerVrfEvaluatorState, &BlockProducerConfig)> {
221        self.with(None, |this| Some((&this.vrf_evaluator, &this.config)))
222    }
223
224    /// If we need to construct delegator table, get it's inputs.
225    pub fn vrf_delegator_table_inputs(&self) -> Option<(&v2::LedgerHash, &AccountPublicKey)> {
226        self.vrf_evaluator()?.vrf_delegator_table_inputs()
227    }
228
229    pub fn pending_transactions(&self) -> Vec<valid::UserCommand> {
230        self.with(Vec::new(), |this| this.current.pending_transactions())
231    }
232}
233
234impl BlockProducerCurrentState {
235    pub fn won_slot_should_search(&self) -> bool {
236        match self {
237            Self::Idle { .. } | Self::WonSlotDiscarded { .. } | Self::Injected { .. } => true,
238            Self::WonSlot { .. }
239            | Self::WonSlotWait { .. }
240            | Self::WonSlotProduceInit { .. }
241            | Self::WonSlotTransactionsGet { .. }
242            | Self::WonSlotTransactionsSuccess { .. }
243            | Self::StagedLedgerDiffCreatePending { .. }
244            | Self::StagedLedgerDiffCreateSuccess { .. }
245            | Self::BlockUnprovenBuilt { .. }
246            | Self::BlockProvePending { .. }
247            | Self::BlockProveSuccess { .. }
248            | Self::Produced { .. } => false,
249        }
250    }
251
252    pub fn won_slot(&self) -> Option<&BlockProducerWonSlot> {
253        match self {
254            Self::Idle { .. } => None,
255            Self::WonSlotDiscarded { won_slot, .. }
256            | Self::WonSlot { won_slot, .. }
257            | Self::WonSlotWait { won_slot, .. }
258            | Self::WonSlotProduceInit { won_slot, .. }
259            | Self::WonSlotTransactionsGet { won_slot, .. }
260            | Self::WonSlotTransactionsSuccess { won_slot, .. }
261            | Self::StagedLedgerDiffCreatePending { won_slot, .. }
262            | Self::StagedLedgerDiffCreateSuccess { won_slot, .. }
263            | Self::BlockUnprovenBuilt { won_slot, .. }
264            | Self::BlockProvePending { won_slot, .. }
265            | Self::BlockProveSuccess { won_slot, .. }
266            | Self::Produced { won_slot, .. }
267            | Self::Injected { won_slot, .. } => Some(won_slot),
268        }
269    }
270
271    pub fn parent_chain(&self) -> Option<&[AppliedBlock]> {
272        match self {
273            Self::Idle { .. }
274            | Self::WonSlotDiscarded { .. }
275            | Self::WonSlot { .. }
276            | Self::WonSlotWait { .. } => None,
277            Self::WonSlotProduceInit { chain, .. }
278            | Self::WonSlotTransactionsGet { chain, .. }
279            | Self::WonSlotTransactionsSuccess { chain, .. }
280            | Self::StagedLedgerDiffCreatePending { chain, .. }
281            | Self::StagedLedgerDiffCreateSuccess { chain, .. }
282            | Self::BlockUnprovenBuilt { chain, .. }
283            | Self::BlockProvePending { chain, .. }
284            | Self::BlockProveSuccess { chain, .. }
285            | Self::Produced { chain, .. }
286            | Self::Injected { chain, .. } => Some(chain),
287        }
288    }
289
290    pub fn won_slot_should_wait(&self, now: redux::Timestamp) -> bool {
291        matches!(self, Self::WonSlot { .. }) && !self.won_slot_should_produce(now)
292    }
293
294    pub fn won_slot_should_produce(&self, now: redux::Timestamp) -> bool {
295        // TODO(binier): maybe have runtime estimate
296        #[cfg(not(target_arch = "wasm32"))]
297        const BLOCK_PRODUCTION_ESTIMATE: u64 = Duration::from_secs(6).as_nanos() as u64;
298        #[cfg(target_arch = "wasm32")]
299        const BLOCK_PRODUCTION_ESTIMATE: u64 = Duration::from_secs(20).as_nanos() as u64;
300
301        let slot_interval = Duration::from_secs(3 * 60).as_nanos() as u64;
302        match self {
303            Self::WonSlot { won_slot, .. } | Self::WonSlotWait { won_slot, .. } => {
304                // Make sure to only producer blocks when in the slot interval
305                let slot_upper_bound = won_slot
306                    .slot_time
307                    .checked_add(slot_interval)
308                    .expect("overflow");
309                let estimated_produced_time = now
310                    .checked_add(BLOCK_PRODUCTION_ESTIMATE)
311                    .expect("overflow");
312                estimated_produced_time >= won_slot.slot_time && now < slot_upper_bound
313            }
314            _ => false,
315        }
316    }
317
318    pub fn won_slot_should_discard(
319        &self,
320        best_tip: &ArcBlockWithHash,
321    ) -> Option<BlockProducerWonSlotDiscardReason> {
322        if matches!(self, Self::WonSlotDiscarded { .. }) {
323            return None;
324        }
325
326        let won_slot = self.won_slot()?;
327        if won_slot.global_slot() < best_tip.global_slot() {
328            return Some(BlockProducerWonSlotDiscardReason::BestTipGlobalSlotHigher);
329        }
330
331        if &won_slot.staking_ledger_hash != best_tip.staking_epoch_ledger_hash()
332            && &won_slot.staking_ledger_hash != best_tip.next_epoch_ledger_hash()
333        {
334            return Some(BlockProducerWonSlotDiscardReason::BestTipStakingLedgerDifferent);
335        }
336
337        if won_slot < best_tip
338            || self.produced_block().is_some_and(|block| {
339                !consensus_take(
340                    best_tip.consensus_state(),
341                    block.consensus_state(),
342                    best_tip.hash(),
343                    block.hash(),
344                )
345            })
346        {
347            return Some(BlockProducerWonSlotDiscardReason::BestTipSuperior);
348        }
349
350        None
351    }
352
353    pub fn is_producing(&self) -> bool {
354        match self {
355            Self::Idle { .. }
356            | Self::WonSlotDiscarded { .. }
357            | Self::WonSlot { .. }
358            | Self::WonSlotWait { .. }
359            | Self::Injected { .. } => false,
360            Self::WonSlotProduceInit { .. }
361            | Self::WonSlotTransactionsGet { .. }
362            | Self::WonSlotTransactionsSuccess { .. }
363            | Self::StagedLedgerDiffCreatePending { .. }
364            | Self::StagedLedgerDiffCreateSuccess { .. }
365            | Self::BlockUnprovenBuilt { .. }
366            | Self::BlockProvePending { .. }
367            | Self::BlockProveSuccess { .. }
368            | Self::Produced { .. } => true,
369        }
370    }
371
372    pub fn produced_block(&self) -> Option<&ArcBlockWithHash> {
373        match self {
374            Self::Produced { block, .. } => Some(block),
375            _ => None,
376        }
377    }
378
379    pub fn injected_block(&self) -> Option<&ArcBlockWithHash> {
380        match self {
381            Self::Injected { block, .. } => Some(block),
382            _ => None,
383        }
384    }
385
386    pub fn produced_block_with_chain(&self) -> Option<(&ArcBlockWithHash, &[AppliedBlock])> {
387        match self {
388            Self::Produced { chain, block, .. } => Some((block, chain)),
389            _ => None,
390        }
391    }
392
393    pub fn pending_transactions(&self) -> Vec<valid::UserCommand> {
394        match self {
395            Self::WonSlotTransactionsSuccess {
396                transactions_by_fee,
397                ..
398            }
399            | Self::StagedLedgerDiffCreatePending {
400                transactions_by_fee,
401                ..
402            } => transactions_by_fee.to_vec(),
403            _ => vec![],
404        }
405    }
406}
407
408impl Default for BlockProducerCurrentState {
409    fn default() -> Self {
410        Self::Idle {
411            time: redux::Timestamp::ZERO,
412        }
413    }
414}