node/transition_frontier/
transition_frontier_state.rs

1use std::collections::BTreeMap;
2
3use ledger::transaction_pool::diff::BestTipDiff;
4use mina_p2p_messages::v2::{
5    MinaStateProtocolStateBodyValueStableV2, MinaStateProtocolStateValueStableV2, StateHash,
6    TransactionHash,
7};
8use openmina_core::{
9    block::{AppliedBlock, ArcBlockWithHash},
10    bug_condition,
11};
12use serde::{Deserialize, Serialize};
13
14use super::{
15    candidate::TransitionFrontierCandidatesState, genesis::TransitionFrontierGenesisState,
16    sync::TransitionFrontierSyncState, TransitionFrontierConfig,
17};
18
19#[derive(Serialize, Deserialize, Debug, Clone)]
20pub struct TransitionFrontierState {
21    pub config: TransitionFrontierConfig,
22    /// Genesis block generation/proving state
23    pub genesis: TransitionFrontierGenesisState,
24    /// Current best known chain, from root of the transition frontier to best tip
25    pub best_chain: Vec<AppliedBlock>,
26    /// Needed protocol states for applying transactions in the root
27    /// scan state that we don't have in the `best_chain` list.
28    pub needed_protocol_states: BTreeMap<StateHash, MinaStateProtocolStateValueStableV2>,
29    pub candidates: TransitionFrontierCandidatesState,
30    /// Transition frontier synchronization state
31    pub sync: TransitionFrontierSyncState,
32
33    /// Blocks which had valid proof but failed block application or
34    /// other validations after it reached transition frontier.
35    pub blacklist: BTreeMap<StateHash, u32>,
36    /// The diff of `Self::best_chain` with the previous one
37    pub chain_diff: Option<BestTipDiff>,
38    /// Archive mode enabled
39    pub archive_enabled: bool,
40}
41
42impl TransitionFrontierState {
43    pub fn new(config: TransitionFrontierConfig, archive_enabled: bool) -> Self {
44        Self {
45            config,
46            genesis: TransitionFrontierGenesisState::Idle,
47            candidates: TransitionFrontierCandidatesState::new(),
48            best_chain: Vec::with_capacity(290),
49            needed_protocol_states: Default::default(),
50            sync: TransitionFrontierSyncState::Idle,
51            blacklist: Default::default(),
52            chain_diff: None,
53            archive_enabled,
54        }
55    }
56
57    pub fn best_tip(&self) -> Option<&ArcBlockWithHash> {
58        self.best_chain.last().map(|b| &b.block)
59    }
60
61    pub fn root(&self) -> Option<&ArcBlockWithHash> {
62        self.best_chain.first().map(|b| &b.block)
63    }
64
65    pub fn best_tip_breadcrumb(&self) -> Option<&AppliedBlock> {
66        self.best_chain.last()
67    }
68
69    pub fn root_breadcrumb(&self) -> Option<&AppliedBlock> {
70        self.best_chain.first()
71    }
72
73    /// FIXME
74    /// Note(adonagy): This can be expensive, keep a map with all the tx hashis in the best chain
75    pub fn contains_transaction(&self, hash: &TransactionHash) -> bool {
76        self.best_chain.iter().any(|block| {
77            block
78                .body()
79                .transactions()
80                .any(|transaction| transaction.hash().as_ref().ok() == Some(hash))
81        })
82    }
83
84    /// Looks up state body by state hash.
85    pub fn get_state_body(
86        &self,
87        hash: &StateHash,
88    ) -> Option<&MinaStateProtocolStateBodyValueStableV2> {
89        self.best_chain
90            .iter()
91            .find_map(|block| {
92                if block.hash() == hash {
93                    Some(&block.header().protocol_state.body)
94                } else {
95                    None
96                }
97            })
98            .or_else(|| {
99                self.needed_protocol_states
100                    .iter()
101                    .find_map(|(block_hash, state)| {
102                        if block_hash == hash {
103                            Some(&state.body)
104                        } else {
105                            None
106                        }
107                    })
108            })
109    }
110
111    /// Create a diff between the old best chain and the new one
112    /// This is used to update the transaction pool
113    pub fn maybe_make_chain_diff(&self, new_chain: &[AppliedBlock]) -> Option<BestTipDiff> {
114        let old_chain = self.best_chain.as_slice();
115        let new_root = new_chain.first();
116
117        if old_chain.last() == new_chain.last() {
118            // Both chains are equal
119            return None;
120        }
121
122        // Look for the new root in the old chain, get its index
123        let new_chain_start_at = match new_root {
124            None => None,
125            Some(new_root) => old_chain
126                .iter()
127                .enumerate()
128                .rev()
129                .find(|(_index, block)| *block == new_root),
130        };
131
132        let (diff_old_chain, diff_new_chain) = match new_chain_start_at {
133            None => {
134                // The new chain has a root not present in the old chain,
135                // so the diff is the 2 wholes chains
136                (old_chain, new_chain)
137            }
138            Some((new_chain_start_at, _)) => {
139                // `new_chain_start_at` is the index of `new_root` in `old_chain`
140                let Some(old_chain_advanced) = &old_chain.get(new_chain_start_at..) else {
141                    bug_condition!("old_chain[{}] out of bounds", new_chain_start_at);
142                    return None;
143                };
144
145                // Common length
146                let len = old_chain_advanced.len().min(new_chain.len());
147
148                // Find the first different block, search from the end
149                let diff_start_at = old_chain_advanced
150                    .get(..len)
151                    .unwrap() // can't fail because len is the minimum len
152                    .iter()
153                    .rev()
154                    .zip(
155                        new_chain
156                            .get(..len)
157                            .unwrap() // can't fail because len is the minimum len
158                            .iter()
159                            .rev(),
160                    )
161                    .position(|(old_block, new_block)| old_block == new_block)
162                    .map(|index| len.saturating_sub(index)) // we started from the end
163                    .unwrap(); // Never panics because we know there is the common root block
164
165                let Some(diff_old_chain) = old_chain_advanced.get(diff_start_at..) else {
166                    bug_condition!("old_chain[{}] out of bounds", diff_start_at);
167                    return None;
168                };
169                let Some(diff_new_chain) = new_chain.get(diff_start_at..) else {
170                    bug_condition!("new_chain[{}] out of bounds", diff_start_at);
171                    return None;
172                };
173
174                (diff_old_chain, diff_new_chain)
175            }
176        };
177
178        // Collect commands and convert them to type `WithStatus::<UserCommand>`
179        let collect = |chain: &[AppliedBlock]| {
180            chain
181                .iter()
182                .flat_map(|breadcrumb| breadcrumb.commands_iter())
183                .filter_map(|cmd| {
184                    use ledger::scan_state::transaction_logic::{UserCommand, WithStatus};
185                    Some(
186                        WithStatus::<UserCommand>::try_from(cmd)
187                            .ok()?
188                            .into_map(UserCommand::to_valid_unsafe),
189                    )
190                })
191                .collect::<Vec<_>>()
192        };
193
194        let removed_commands = collect(diff_old_chain);
195        let new_commands = collect(diff_new_chain);
196
197        if removed_commands.is_empty() && new_commands.is_empty() {
198            return None;
199        }
200
201        Some(BestTipDiff {
202            new_commands,
203            removed_commands,
204            reorg_best_tip: false, // TODO: Unused for now
205        })
206    }
207
208    pub fn resources_usage(&self) -> serde_json::Value {
209        serde_json::json!({
210            "best_chain_size": self.best_chain.len(),
211            "needed_protocol_states_size": self
212                .needed_protocol_states
213                .len(),
214            "blacklist_size": self.blacklist.len(),
215            "diff_tx_size": self
216                .chain_diff
217                .as_ref()
218                // `saturating_add` is not needed here as collection size cannot overflow usize
219                // but it makes clippy satisfied
220                .map(|d| d.new_commands.len().saturating_add(d.removed_commands.len()))
221                .unwrap_or_default()
222        })
223    }
224}