node/transition_frontier/candidate/
transition_frontier_candidate_reducer.rs

1use openmina_core::{
2    block::{ArcBlockWithHash, BlockHash},
3    bug_condition,
4};
5use snark::block_verify::{SnarkBlockVerifyAction, SnarkBlockVerifyError, SnarkBlockVerifyId};
6
7use crate::{
8    transition_frontier::sync::{
9        ledger::{
10            snarked::TransitionFrontierSyncLedgerSnarkedAction,
11            staged::TransitionFrontierSyncLedgerStagedAction,
12        },
13        TransitionFrontierSyncAction,
14    },
15    WatchedAccountsAction,
16};
17
18use super::{
19    TransitionFrontierCandidateAction, TransitionFrontierCandidateActionWithMetaRef,
20    TransitionFrontierCandidateStatus, TransitionFrontierCandidatesState,
21};
22
23impl TransitionFrontierCandidatesState {
24    pub fn reducer(
25        mut state_context: crate::Substate<Self>,
26        action: TransitionFrontierCandidateActionWithMetaRef<'_>,
27    ) {
28        let Ok(state) = state_context.get_substate_mut() else {
29            // TODO: log or propagate
30            return;
31        };
32        let (action, meta) = action.split();
33
34        match action {
35            TransitionFrontierCandidateAction::P2pBestTipUpdate { best_tip } => {
36                let dispatcher = state_context.into_dispatcher();
37                dispatcher.push(TransitionFrontierCandidateAction::BlockReceived {
38                    block: best_tip.clone(),
39                    chain_proof: None,
40                });
41
42                dispatcher.push(TransitionFrontierSyncLedgerSnarkedAction::PeersQuery);
43                dispatcher.push(TransitionFrontierSyncLedgerStagedAction::PartsPeerFetchInit);
44                dispatcher.push(TransitionFrontierSyncAction::BlocksPeersQuery);
45            }
46            TransitionFrontierCandidateAction::BlockReceived { block, chain_proof } => {
47                state.add(meta.time(), block.clone(), chain_proof.clone());
48
49                // Dispatch
50                let (dispatcher, state) = state_context.into_dispatcher_and_state();
51
52                let allow_block_too_late = allow_block_too_late(state, block);
53
54                match state.prevalidate_block(block, allow_block_too_late) {
55                    Ok(()) => {
56                        dispatcher.push(
57                            TransitionFrontierCandidateAction::BlockPrevalidateSuccess {
58                                hash: block.hash().clone(),
59                            },
60                        );
61                    }
62                    Err(error) => {
63                        dispatcher.push(TransitionFrontierCandidateAction::BlockPrevalidateError {
64                            hash: block.hash().clone(),
65                            error,
66                        });
67                    }
68                }
69            }
70            TransitionFrontierCandidateAction::BlockPrevalidateError { hash, error } => {
71                state.invalidate(hash, error.is_forever_invalid());
72            }
73            TransitionFrontierCandidateAction::BlockPrevalidateSuccess { hash } => {
74                state.update_status(hash, |_| TransitionFrontierCandidateStatus::Prevalidated);
75                let Some(block) = state.get(hash).map(|s| s.block.clone()) else {
76                    bug_condition!("TransitionFrontierCandidateAction::BlockPrevalidateSuccess block not found but action enabled");
77                    return;
78                };
79
80                // Dispatch
81                let dispatcher = state_context.into_dispatcher();
82                dispatcher.push(SnarkBlockVerifyAction::Init {
83                    block: block.into(),
84                    on_init: redux::callback!(
85                        on_received_block_snark_verify_init((hash: BlockHash, req_id: SnarkBlockVerifyId)) -> crate::Action {
86                            TransitionFrontierCandidateAction::BlockSnarkVerifyPending { hash, req_id }
87                        }),
88                    on_success: redux::callback!(
89                        on_received_block_snark_verify_success(hash: BlockHash) -> crate::Action {
90                            TransitionFrontierCandidateAction::BlockSnarkVerifySuccess { hash }
91                        }),
92                    on_error: redux::callback!(
93                        on_received_block_snark_verify_error((hash: BlockHash, error: SnarkBlockVerifyError)) -> crate::Action {
94                            TransitionFrontierCandidateAction::BlockSnarkVerifyError { hash, error }
95                        }),
96                });
97            }
98            TransitionFrontierCandidateAction::BlockChainProofUpdate { hash, chain_proof } => {
99                state.set_chain_proof(hash, chain_proof.clone());
100
101                let dispatcher = state_context.into_dispatcher();
102                dispatcher
103                    .push(TransitionFrontierCandidateAction::TransitionFrontierSyncTargetUpdate);
104            }
105            TransitionFrontierCandidateAction::BlockSnarkVerifyPending { req_id, hash } => {
106                state.update_status(hash, |_| {
107                    TransitionFrontierCandidateStatus::SnarkVerifyPending {
108                        time: meta.time(),
109                        req_id: *req_id,
110                    }
111                });
112            }
113            TransitionFrontierCandidateAction::BlockSnarkVerifyError { hash, .. } => {
114                state.invalidate(hash, true);
115            }
116            TransitionFrontierCandidateAction::BlockSnarkVerifySuccess { hash } => {
117                state.update_status(hash, |_| {
118                    TransitionFrontierCandidateStatus::SnarkVerifySuccess { time: meta.time() }
119                });
120
121                // Dispatch
122                let (dispatcher, global_state) = state_context.into_dispatcher_and_state();
123                let Some(block) = global_state
124                    .transition_frontier
125                    .candidates
126                    .best_verified_block()
127                else {
128                    return;
129                };
130                for pub_key in global_state.watched_accounts.accounts() {
131                    dispatcher.push(WatchedAccountsAction::LedgerInitialStateGetInit {
132                        pub_key: pub_key.clone(),
133                    });
134                    dispatcher.push(WatchedAccountsAction::TransactionsIncludedInBlock {
135                        pub_key,
136                        block: block.clone(),
137                    });
138                }
139
140                dispatcher
141                    .push(TransitionFrontierCandidateAction::TransitionFrontierSyncTargetUpdate);
142            }
143            TransitionFrontierCandidateAction::TransitionFrontierSyncTargetUpdate => {
144                let (dispatcher, state) = state_context.into_dispatcher_and_state();
145                let Some(best_tip) = state.transition_frontier.candidates.best_verified_block()
146                else {
147                    bug_condition!(
148                        "ConsensusAction::TransitionFrontierSyncTargetUpdate | no chosen best tip"
149                    );
150                    return;
151                };
152
153                let Some((blocks_inbetween, root_block)) = state
154                    .transition_frontier
155                    .candidates
156                    .best_verified_block_chain_proof(&state.transition_frontier)
157                else {
158                    bug_condition!("ConsensusAction::TransitionFrontierSyncTargetUpdate | no best tip chain proof");
159                    return;
160                };
161
162                let previous_root_snarked_ledger_hash = state
163                    .transition_frontier
164                    .root()
165                    .map(|b| b.snarked_ledger_hash().clone());
166
167                dispatcher.push(TransitionFrontierSyncAction::BestTipUpdate {
168                    previous_root_snarked_ledger_hash,
169                    best_tip: best_tip.clone(),
170                    root_block,
171                    blocks_inbetween,
172                    on_success: None,
173                });
174            }
175            TransitionFrontierCandidateAction::Prune => {
176                state.prune();
177            }
178        }
179    }
180}
181
182/// Decide if the time-reception check should be done for this block or not.
183///
184/// The check is skipped if the block's global_slot is greater than the
185/// current best tip and the difference greater than 2.
186///
187/// Ideally we would differentiate between requested blocks and blocks
188/// received from gossip, but this difference doesn't really exist
189/// in the WebRTC transport, hence this heuristic.
190pub fn allow_block_too_late(state: &crate::State, block: &ArcBlockWithHash) -> bool {
191    let (has_greater_blobal_slot, diff_with_best_tip) = state
192        .transition_frontier
193        .best_tip()
194        .map(|b| {
195            (
196                block.global_slot() > b.global_slot(),
197                b.global_slot().abs_diff(block.global_slot()),
198            )
199        })
200        .unwrap_or((false, 0));
201
202    has_greater_blobal_slot && diff_with_best_tip > 2
203}