Skip to main content

zebra_state/service/
queued_blocks.rs

1//! Queued blocks that are awaiting their parent block for verification.
2
3use std::{
4    collections::{hash_map::Drain, BTreeMap, HashMap, HashSet, VecDeque},
5    iter, mem,
6};
7
8use tokio::sync::oneshot;
9use tracing::instrument;
10
11use zebra_chain::{block, transparent};
12
13use crate::{
14    error::{CommitBlockError, CommitCheckpointVerifiedError},
15    CheckpointVerifiedBlock, CommitSemanticallyVerifiedError, KnownBlock, NonFinalizedState,
16    SemanticallyVerifiedBlock,
17};
18
19#[cfg(test)]
20mod tests;
21
22/// A queued checkpoint verified block, and its corresponding [`Result`] channel.
23pub type QueuedCheckpointVerified = (
24    CheckpointVerifiedBlock,
25    oneshot::Sender<Result<block::Hash, CommitCheckpointVerifiedError>>,
26);
27
28/// A queued semantically verified block, and its corresponding [`Result`] channel.
29pub type QueuedSemanticallyVerified = (
30    SemanticallyVerifiedBlock,
31    oneshot::Sender<Result<block::Hash, CommitSemanticallyVerifiedError>>,
32);
33
34/// A queue of blocks, awaiting the arrival of parent blocks.
35#[derive(Debug, Default)]
36pub struct QueuedBlocks {
37    /// Blocks awaiting their parent blocks for contextual verification.
38    blocks: HashMap<block::Hash, QueuedSemanticallyVerified>,
39    /// Hashes from `queued_blocks`, indexed by parent hash.
40    by_parent: HashMap<block::Hash, HashSet<block::Hash>>,
41    /// Hashes from `queued_blocks`, indexed by block height.
42    by_height: BTreeMap<block::Height, HashSet<block::Hash>>,
43    /// Known UTXOs.
44    known_utxos: HashMap<transparent::OutPoint, transparent::Utxo>,
45}
46
47impl QueuedBlocks {
48    /// Queue a block for eventual verification and commit.
49    ///
50    /// # Panics
51    ///
52    /// - if a block with the same `block::Hash` has already been queued.
53    #[instrument(skip(self), fields(height = ?new.0.height, hash = %new.0.hash))]
54    pub fn queue(&mut self, new: QueuedSemanticallyVerified) {
55        let new_hash = new.0.hash;
56        let new_height = new.0.height;
57        let parent_hash = new.0.block.header.previous_block_hash;
58
59        if self.blocks.contains_key(&new_hash) {
60            // Skip queueing the block and return early if the hash is not unique
61            return;
62        }
63
64        // Track known UTXOs in queued blocks.
65        for (outpoint, ordered_utxo) in new.0.new_outputs.iter() {
66            self.known_utxos
67                .insert(*outpoint, ordered_utxo.utxo.clone());
68        }
69
70        self.blocks.insert(new_hash, new);
71        self.by_height
72            .entry(new_height)
73            .or_default()
74            .insert(new_hash);
75        self.by_parent
76            .entry(parent_hash)
77            .or_default()
78            .insert(new_hash);
79
80        tracing::trace!(%parent_hash, queued = %self.blocks.len(), "queued block");
81        self.update_metrics();
82    }
83
84    /// Returns `true` if there are any queued children of `parent_hash`.
85    #[instrument(skip(self), fields(%parent_hash))]
86    pub fn has_queued_children(&self, parent_hash: block::Hash) -> bool {
87        self.by_parent.contains_key(&parent_hash)
88    }
89
90    /// Returns `true` if there is a block with the provided hash present.
91    #[instrument(skip(self), fields(%hash))]
92    pub fn has(&self, hash: block::Hash) -> bool {
93        self.blocks.contains_key(&hash)
94    }
95
96    /// Dequeue and return all blocks that were waiting for the arrival of
97    /// `parent`.
98    #[instrument(skip(self), fields(%parent_hash))]
99    pub fn dequeue_children(
100        &mut self,
101        parent_hash: block::Hash,
102    ) -> Vec<QueuedSemanticallyVerified> {
103        let queued_children = self
104            .by_parent
105            .remove(&parent_hash)
106            .unwrap_or_default()
107            .into_iter()
108            .map(|hash| {
109                self.blocks
110                    .remove(&hash)
111                    .expect("block is present if its hash is in by_parent")
112            })
113            .collect::<Vec<_>>();
114
115        for queued in &queued_children {
116            if let Some(hashes) = self.by_height.get_mut(&queued.0.height) {
117                hashes.remove(&queued.0.hash);
118
119                if hashes.is_empty() {
120                    self.by_height.remove(&queued.0.height);
121                }
122            }
123
124            // TODO: only remove UTXOs if there are no queued blocks with that UTXO
125            //       (known_utxos is best-effort, so this is ok for now)
126            for outpoint in queued.0.new_outputs.keys() {
127                self.known_utxos.remove(outpoint);
128            }
129        }
130
131        tracing::trace!(
132            dequeued = queued_children.len(),
133            remaining = self.blocks.len(),
134            "dequeued blocks"
135        );
136        self.update_metrics();
137
138        queued_children
139    }
140
141    /// Remove all queued blocks whose height is less than or equal to the given
142    /// `finalized_tip_height`.
143    #[instrument(skip(self))]
144    pub fn prune_by_height(&mut self, finalized_tip_height: block::Height) {
145        // split_off returns the values _greater than or equal to_ the key. What
146        // we need is the keys that are less than or equal to
147        // `finalized_tip_height`. To get this we have split at
148        // `finalized_tip_height + 1` and swap the removed portion of the list
149        // with the remainder.
150        let split_height = finalized_tip_height + 1;
151        let split_height =
152            split_height.expect("height after finalized tip won't exceed max height");
153        let mut by_height = self.by_height.split_off(&split_height);
154        mem::swap(&mut self.by_height, &mut by_height);
155
156        for hash in by_height.into_values().flatten() {
157            let (expired_block, expired_sender) =
158                self.blocks.remove(&hash).expect("block is present");
159            let parent_hash = &expired_block.block.header.previous_block_hash;
160
161            // we don't care if the receiver was dropped
162            let _ = expired_sender.send(Err(CommitBlockError::new_duplicate(
163                Some(expired_block.height.into()),
164                KnownBlock::Finalized,
165            )
166            .into()));
167
168            // TODO: only remove UTXOs if there are no queued blocks with that UTXO
169            //       (known_utxos is best-effort, so this is ok for now)
170            for outpoint in expired_block.new_outputs.keys() {
171                self.known_utxos.remove(outpoint);
172            }
173
174            let parent_list = self
175                .by_parent
176                .get_mut(parent_hash)
177                .expect("parent is present");
178
179            if parent_list.len() == 1 {
180                let removed = self
181                    .by_parent
182                    .remove(parent_hash)
183                    .expect("parent is present");
184                assert!(
185                    removed.contains(&hash),
186                    "hash must be present in parent hash list"
187                );
188            } else {
189                assert!(
190                    parent_list.remove(&hash),
191                    "hash must be present in parent hash list"
192                );
193            }
194        }
195
196        tracing::trace!(num_blocks = %self.blocks.len(), "Finished pruning blocks at or beneath the finalized tip height");
197        self.update_metrics();
198    }
199
200    /// Return the queued block if it has already been registered
201    pub fn get_mut(&mut self, hash: &block::Hash) -> Option<&mut QueuedSemanticallyVerified> {
202        self.blocks.get_mut(hash)
203    }
204
205    /// Update metrics after the queue is modified
206    fn update_metrics(&self) {
207        if let Some(min_height) = self.by_height.keys().next() {
208            metrics::gauge!("state.memory.queued.min.height").set(min_height.0 as f64);
209        } else {
210            // use f64::NAN as a sentinel value for "None", because 0 is a valid height
211            metrics::gauge!("state.memory.queued.min.height").set(f64::NAN);
212        }
213        if let Some(max_height) = self.by_height.keys().next_back() {
214            metrics::gauge!("state.memory.queued.max.height").set(max_height.0 as f64);
215        } else {
216            // use f64::NAN as a sentinel value for "None", because 0 is a valid height
217            metrics::gauge!("state.memory.queued.max.height").set(f64::NAN);
218        }
219
220        metrics::gauge!("state.memory.queued.block.count").set(self.blocks.len() as f64);
221    }
222
223    /// Try to look up this UTXO in any queued block.
224    #[instrument(skip(self))]
225    pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Utxo> {
226        self.known_utxos.get(outpoint).cloned()
227    }
228
229    /// Clears known_utxos, by_parent, and by_height, then drains blocks.
230    /// Returns all key-value pairs of blocks as an iterator.
231    ///
232    /// Doesn't update the metrics, because it is only used when the state is being dropped.
233    pub fn drain(&mut self) -> Drain<'_, block::Hash, QueuedSemanticallyVerified> {
234        self.known_utxos.clear();
235        self.known_utxos.shrink_to_fit();
236        self.by_parent.clear();
237        self.by_parent.shrink_to_fit();
238        self.by_height.clear();
239
240        self.blocks.drain()
241    }
242}
243
244#[derive(Debug, Default)]
245pub(crate) struct SentHashes {
246    /// A list of previously sent block batches, each batch is in increasing height order.
247    /// We use this list to efficiently prune outdated hashes that are at or below the finalized tip.
248    bufs: Vec<VecDeque<(block::Hash, block::Height)>>,
249
250    /// The list of blocks sent in the current batch, in increasing height order.
251    curr_buf: VecDeque<(block::Hash, block::Height)>,
252
253    /// Stores a set of hashes that have been sent to the block write task but
254    /// may not be in the finalized state yet.
255    pub sent: HashMap<block::Hash, Vec<transparent::OutPoint>>,
256
257    /// Known UTXOs.
258    known_utxos: HashMap<transparent::OutPoint, transparent::Utxo>,
259
260    /// Whether the hashes in this struct can be used check if the chain can be forked.
261    /// This is set to false until all checkpoint-verified block hashes have been pruned.
262    pub(crate) can_fork_chain_at_hashes: bool,
263}
264
265impl SentHashes {
266    /// Creates a new [`SentHashes`] with the block hashes and UTXOs in the provided non-finalized state.
267    pub fn new(non_finalized_state: &NonFinalizedState) -> Self {
268        let mut sent_hashes = Self::default();
269        for (_, block) in non_finalized_state
270            .chain_iter()
271            .flat_map(|c| c.blocks.clone())
272        {
273            sent_hashes.add(&block.into());
274        }
275
276        if !sent_hashes.sent.is_empty() {
277            sent_hashes.can_fork_chain_at_hashes = true;
278        }
279
280        sent_hashes
281    }
282
283    /// Stores the `block`'s hash, height, and UTXOs, so they can be used to check if a block or UTXO
284    /// is available in the state.
285    ///
286    /// Assumes that blocks are added in the order of their height between `finish_batch` calls
287    /// for efficient pruning.
288    pub fn add(&mut self, block: &SemanticallyVerifiedBlock) {
289        // Track known UTXOs in sent blocks.
290        let outpoints = block
291            .new_outputs
292            .iter()
293            .map(|(outpoint, ordered_utxo)| {
294                self.known_utxos
295                    .insert(*outpoint, ordered_utxo.utxo.clone());
296                outpoint
297            })
298            .cloned()
299            .collect();
300
301        self.curr_buf.push_back((block.hash, block.height));
302        self.sent.insert(block.hash, outpoints);
303
304        self.update_metrics_for_block(block.height);
305    }
306
307    /// Stores the checkpoint verified `block`'s hash, height, and UTXOs, so they can be used to check if a
308    /// block or UTXO is available in the state.
309    ///
310    /// Used for checkpoint verified blocks close to the final checkpoint, so the semantic block verifier can look up
311    /// their UTXOs.
312    ///
313    /// Assumes that blocks are added in the order of their height between `finish_batch` calls
314    /// for efficient pruning.
315    ///
316    /// For more details see `add()`.
317    pub fn add_finalized(&mut self, block: &CheckpointVerifiedBlock) {
318        // Track known UTXOs in sent blocks.
319        let outpoints = block
320            .new_outputs
321            .iter()
322            .map(|(outpoint, ordered_utxo)| {
323                self.known_utxos
324                    .insert(*outpoint, ordered_utxo.utxo.clone());
325                outpoint
326            })
327            .cloned()
328            .collect();
329
330        self.curr_buf.push_back((block.hash, block.height));
331        self.sent.insert(block.hash, outpoints);
332
333        self.update_metrics_for_block(block.height);
334    }
335
336    /// Try to look up this UTXO in any sent block.
337    #[instrument(skip(self))]
338    pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Utxo> {
339        self.known_utxos.get(outpoint).cloned()
340    }
341
342    /// Finishes the current block batch, and stores it for efficient pruning.
343    pub fn finish_batch(&mut self) {
344        if !self.curr_buf.is_empty() {
345            self.bufs.push(std::mem::take(&mut self.curr_buf));
346        }
347    }
348
349    /// Prunes sent blocks at or below `height_bound`.
350    ///
351    /// Finishes the batch if `finish_batch()` hasn't been called already.
352    ///
353    /// Assumes that blocks will be added in order of their heights between each `finish_batch()` call,
354    /// so that blocks can be efficiently and reliably removed by height.
355    pub fn prune_by_height(&mut self, height_bound: block::Height) {
356        self.finish_batch();
357
358        // Iterates over each buf in `sent_bufs`, removing sent blocks until reaching
359        // the first block with a height above the `height_bound`.
360        self.bufs.retain_mut(|buf| {
361            while let Some((hash, height)) = buf.pop_front() {
362                if height > height_bound {
363                    buf.push_front((hash, height));
364                    return true;
365                } else if let Some(expired_outpoints) = self.sent.remove(&hash) {
366                    // TODO: only remove UTXOs if there are no queued blocks with that UTXO
367                    //       (known_utxos is best-effort, so this is ok for now)
368                    for outpoint in expired_outpoints.iter() {
369                        self.known_utxos.remove(outpoint);
370                    }
371                }
372            }
373
374            false
375        });
376
377        self.sent.shrink_to_fit();
378        self.known_utxos.shrink_to_fit();
379        self.bufs.shrink_to_fit();
380
381        self.update_metrics_for_cache();
382    }
383
384    /// Returns true if SentHashes contains the `hash`
385    pub fn contains(&self, hash: &block::Hash) -> bool {
386        self.sent.contains_key(hash)
387    }
388
389    /// Removes a `hash` from `SentHashes`, dropping its outpoints from `known_utxos`
390    /// and its entry from whichever batch buffer holds it.
391    ///
392    /// Called when the block write task rejects a block, so that a subsequent
393    /// re-delivery of a block with the same hash is not short-circuited as a
394    /// "duplicate" against a rejected variant that never reached any chain.
395    pub fn remove(&mut self, hash: &block::Hash) {
396        let Some(outpoints) = self.sent.remove(hash) else {
397            return;
398        };
399
400        for outpoint in &outpoints {
401            self.known_utxos.remove(outpoint);
402        }
403
404        self.curr_buf.retain(|(h, _)| h != hash);
405        for buf in &mut self.bufs {
406            buf.retain(|(h, _)| h != hash);
407        }
408    }
409
410    /// Returns true if the chain can be forked at the provided hash
411    pub fn can_fork_chain_at(&self, hash: &block::Hash) -> bool {
412        self.can_fork_chain_at_hashes && self.contains(hash)
413    }
414
415    /// Update sent block metrics after a block is sent.
416    fn update_metrics_for_block(&self, height: block::Height) {
417        metrics::counter!("state.memory.sent.block.count").increment(1);
418        metrics::gauge!("state.memory.sent.block.height").set(height.0 as f64);
419
420        self.update_metrics_for_cache();
421    }
422
423    /// Update sent block cache metrics after the sent blocks are modified.
424    fn update_metrics_for_cache(&self) {
425        let batch_iter = || self.bufs.iter().chain(iter::once(&self.curr_buf));
426
427        if let Some(min_height) = batch_iter()
428            .flat_map(|batch| batch.front().map(|(_hash, height)| height))
429            .min()
430        {
431            metrics::gauge!("state.memory.sent.cache.min.height").set(min_height.0 as f64);
432        } else {
433            // use f64::NAN as a sentinel value for "None", because 0 is a valid height
434            metrics::gauge!("state.memory.sent.cache.min.height").set(f64::NAN);
435        }
436
437        if let Some(max_height) = batch_iter()
438            .flat_map(|batch| batch.back().map(|(_hash, height)| height))
439            .max()
440        {
441            metrics::gauge!("state.memory.sent.cache.max.height").set(max_height.0 as f64);
442        } else {
443            // use f64::NAN as a sentinel value for "None", because 0 is a valid height
444            metrics::gauge!("state.memory.sent.cache.max.height").set(f64::NAN);
445        }
446
447        metrics::gauge!("state.memory.sent.cache.block.count")
448            .set(batch_iter().flatten().count() as f64);
449
450        metrics::gauge!("state.memory.sent.cache.batch.count").set(batch_iter().count() as f64);
451    }
452}