Skip to main content

zebra_state/service/
write.rs

1//! Writing blocks to the finalized and non-finalized states.
2
3use std::{
4    path::{Path, PathBuf},
5    sync::Arc,
6};
7
8use indexmap::IndexMap;
9use tokio::sync::{
10    mpsc::{UnboundedReceiver, UnboundedSender},
11    oneshot, watch,
12};
13
14use tracing::Span;
15use zebra_chain::block::{self, Height};
16
17use crate::{
18    constants::MAX_BLOCK_REORG_HEIGHT,
19    service::{
20        check,
21        finalized_state::{FinalizedState, ZebraDb},
22        non_finalized_state::NonFinalizedState,
23        queued_blocks::{QueuedCheckpointVerified, QueuedSemanticallyVerified},
24        ChainTipBlock, ChainTipSender, InvalidateError, ReconsiderError,
25    },
26    SemanticallyVerifiedBlock, ValidateContextError,
27};
28
29// These types are used in doc links
30#[allow(unused_imports)]
31use crate::service::{
32    chain_tip::{ChainTipChange, LatestChainTip},
33    non_finalized_state::Chain,
34};
35
36/// The maximum size of the parent error map.
37///
38/// We allow enough space for multiple concurrent chain forks with errors.
39const PARENT_ERROR_MAP_LIMIT: usize = MAX_BLOCK_REORG_HEIGHT as usize * 2;
40
41/// Run contextual validation on the prepared block and add it to the
42/// non-finalized state if it is contextually valid.
43#[tracing::instrument(
44    level = "debug",
45    skip(finalized_state, non_finalized_state, prepared),
46    fields(
47        height = ?prepared.height,
48        hash = %prepared.hash,
49        chains = non_finalized_state.chain_count()
50    )
51)]
52pub(crate) fn validate_and_commit_non_finalized(
53    finalized_state: &ZebraDb,
54    non_finalized_state: &mut NonFinalizedState,
55    prepared: SemanticallyVerifiedBlock,
56) -> Result<(), ValidateContextError> {
57    check::initial_contextual_validity(finalized_state, non_finalized_state, &prepared)?;
58    let parent_hash = prepared.block.header.previous_block_hash;
59
60    if finalized_state.finalized_tip_hash() == parent_hash {
61        non_finalized_state.commit_new_chain(prepared, finalized_state)?;
62    } else {
63        non_finalized_state.commit_block(prepared, finalized_state)?;
64    }
65
66    Ok(())
67}
68
69/// Update the [`LatestChainTip`], [`ChainTipChange`], and `non_finalized_state_sender`
70/// channels with the latest state. If there is no non-finalized chain, use the
71/// finalized tip.
72///
73/// If `backup_dir_path` is `Some`, the non-finalized state is written to the backup
74/// directory before updating the channels.
75///
76/// Returns the latest chain tip height, or `None` if both states are empty.
77#[instrument(
78    level = "debug",
79    skip(
80        non_finalized_state,
81        finalized_state,
82        chain_tip_sender,
83        non_finalized_state_sender,
84        backup_dir_path,
85    ),
86    fields(chains = non_finalized_state.chain_count())
87)]
88fn update_latest_chain_channels(
89    non_finalized_state: &NonFinalizedState,
90    finalized_state: &ZebraDb,
91    chain_tip_sender: &mut ChainTipSender,
92    non_finalized_state_sender: &watch::Sender<NonFinalizedState>,
93    backup_dir_path: Option<&Path>,
94) -> Option<block::Height> {
95    let non_finalized_tip = non_finalized_state
96        .best_tip_block()
97        .cloned()
98        .map(ChainTipBlock::from);
99    let tip_block_height = if let Some(tip) = &non_finalized_tip {
100        Some(tip.height)
101    } else {
102        finalized_state.finalized_tip_height()
103    };
104
105    if let Some(backup_dir_path) = backup_dir_path {
106        non_finalized_state.write_to_backup(backup_dir_path);
107    }
108
109    // If the final receiver was just dropped, ignore the error.
110    let _ = non_finalized_state_sender.send(non_finalized_state.clone());
111
112    if let Some(tip) = non_finalized_tip {
113        chain_tip_sender.set_best_non_finalized_tip(tip);
114    } else {
115        let finalized_tip = finalized_state
116            .tip_block()
117            .map(crate::CheckpointVerifiedBlock::from)
118            .map(ChainTipBlock::from);
119        chain_tip_sender.clear_best_non_finalized_tip(finalized_tip);
120    }
121
122    tip_block_height
123}
124
125/// A worker task that reads, validates, and writes blocks to the
126/// `finalized_state` or `non_finalized_state`.
127struct WriteBlockWorkerTask {
128    finalized_block_write_receiver: UnboundedReceiver<QueuedCheckpointVerified>,
129    non_finalized_block_write_receiver: UnboundedReceiver<NonFinalizedWriteMessage>,
130    finalized_state: FinalizedState,
131    non_finalized_state: NonFinalizedState,
132    invalid_block_reset_sender: UnboundedSender<block::Hash>,
133    /// Signals the [`crate::service::StateService`] that a non-finalized block was rejected by
134    /// the write task, so its hash should be removed from
135    /// `non_finalized_block_write_sent_hashes`.
136    ///
137    /// Without this, a rejected same-hash block locks out a later honest
138    /// re-delivery of a block at the same hash as a "duplicate" until restart
139    /// or reorg.
140    non_finalized_rejected_sender: UnboundedSender<block::Hash>,
141    chain_tip_sender: ChainTipSender,
142    non_finalized_state_sender: watch::Sender<NonFinalizedState>,
143    /// If `Some`, the non-finalized state is written to this backup directory
144    /// synchronously before each channel update, instead of via the async backup task.
145    backup_dir_path: Option<PathBuf>,
146}
147
148/// The message type for the non-finalized block write task channel.
149pub enum NonFinalizedWriteMessage {
150    /// A newly downloaded and semantically verified block prepared for
151    /// contextual validation and insertion into the non-finalized state.
152    Commit(QueuedSemanticallyVerified),
153    /// The hash of a block that should be invalidated and removed from
154    /// the non-finalized state, if present.
155    Invalidate {
156        hash: block::Hash,
157        rsp_tx: oneshot::Sender<Result<block::Hash, InvalidateError>>,
158    },
159    /// The hash of a block that was previously invalidated but should be
160    /// reconsidered and reinserted into the non-finalized state.
161    Reconsider {
162        hash: block::Hash,
163        rsp_tx: oneshot::Sender<Result<Vec<block::Hash>, ReconsiderError>>,
164    },
165}
166
167impl From<QueuedSemanticallyVerified> for NonFinalizedWriteMessage {
168    fn from(block: QueuedSemanticallyVerified) -> Self {
169        NonFinalizedWriteMessage::Commit(block)
170    }
171}
172
173/// A worker with a task that reads, validates, and writes blocks to the
174/// `finalized_state` or `non_finalized_state` and channels for sending
175/// it blocks.
176#[derive(Clone, Debug)]
177pub(super) struct BlockWriteSender {
178    /// A channel to send blocks to the `block_write_task`,
179    /// so they can be written to the [`NonFinalizedState`].
180    pub non_finalized: Option<tokio::sync::mpsc::UnboundedSender<NonFinalizedWriteMessage>>,
181
182    /// A channel to send blocks to the `block_write_task`,
183    /// so they can be written to the [`FinalizedState`].
184    ///
185    /// This sender is dropped after the state has finished sending all the checkpointed blocks,
186    /// and the lowest semantically verified block arrives.
187    pub finalized: Option<tokio::sync::mpsc::UnboundedSender<QueuedCheckpointVerified>>,
188}
189
190impl BlockWriteSender {
191    /// Creates a new [`BlockWriteSender`] with the given receivers and states.
192    #[instrument(
193        level = "debug",
194        skip_all,
195        fields(
196            network = %non_finalized_state.network
197        )
198    )]
199    pub fn spawn(
200        finalized_state: FinalizedState,
201        non_finalized_state: NonFinalizedState,
202        chain_tip_sender: ChainTipSender,
203        non_finalized_state_sender: watch::Sender<NonFinalizedState>,
204        should_use_finalized_block_write_sender: bool,
205        backup_dir_path: Option<PathBuf>,
206    ) -> (
207        Self,
208        tokio::sync::mpsc::UnboundedReceiver<block::Hash>,
209        tokio::sync::mpsc::UnboundedReceiver<block::Hash>,
210        Option<Arc<std::thread::JoinHandle<()>>>,
211    ) {
212        // Security: The number of blocks in these channels is limited by
213        //           the syncer and inbound lookahead limits.
214        let (non_finalized_block_write_sender, non_finalized_block_write_receiver) =
215            tokio::sync::mpsc::unbounded_channel();
216        let (finalized_block_write_sender, finalized_block_write_receiver) =
217            tokio::sync::mpsc::unbounded_channel();
218        let (invalid_block_reset_sender, invalid_block_write_reset_receiver) =
219            tokio::sync::mpsc::unbounded_channel();
220        let (non_finalized_rejected_sender, non_finalized_rejected_receiver) =
221            tokio::sync::mpsc::unbounded_channel();
222
223        let span = Span::current();
224        let task = std::thread::spawn(move || {
225            span.in_scope(|| {
226                WriteBlockWorkerTask {
227                    finalized_block_write_receiver,
228                    non_finalized_block_write_receiver,
229                    finalized_state,
230                    non_finalized_state,
231                    invalid_block_reset_sender,
232                    non_finalized_rejected_sender,
233                    chain_tip_sender,
234                    non_finalized_state_sender,
235                    backup_dir_path,
236                }
237                .run()
238            })
239        });
240
241        (
242            Self {
243                non_finalized: Some(non_finalized_block_write_sender),
244                finalized: should_use_finalized_block_write_sender
245                    .then_some(finalized_block_write_sender),
246            },
247            invalid_block_write_reset_receiver,
248            non_finalized_rejected_receiver,
249            Some(Arc::new(task)),
250        )
251    }
252}
253
254impl WriteBlockWorkerTask {
255    /// Reads blocks from the channels, writes them to the `finalized_state` or `non_finalized_state`,
256    /// sends any errors on the `invalid_block_reset_sender`, then updates the `chain_tip_sender` and
257    /// `non_finalized_state_sender`.
258    #[instrument(
259        level = "debug",
260        skip(self),
261        fields(
262            network = %self.non_finalized_state.network
263        )
264    )]
265    pub fn run(mut self) {
266        let Self {
267            finalized_block_write_receiver,
268            non_finalized_block_write_receiver,
269            finalized_state,
270            non_finalized_state,
271            invalid_block_reset_sender,
272            non_finalized_rejected_sender,
273            chain_tip_sender,
274            non_finalized_state_sender,
275            backup_dir_path,
276        } = &mut self;
277
278        let mut prev_finalized_note_commitment_trees = None;
279
280        // Write all the finalized blocks sent by the state,
281        // until the state closes the finalized block channel's sender.
282        while let Some(ordered_block) = finalized_block_write_receiver.blocking_recv() {
283            // TODO: split these checks into separate functions
284
285            if invalid_block_reset_sender.is_closed() {
286                info!("StateService closed the block reset channel. Is Zebra shutting down?");
287                return;
288            }
289
290            // Discard any children of invalid blocks in the channel
291            //
292            // `commit_finalized()` requires blocks in height order.
293            // So if there has been a block commit error,
294            // we need to drop all the descendants of that block,
295            // until we receive a block at the required next height.
296            let next_valid_height = finalized_state
297                .db
298                .finalized_tip_height()
299                .map(|height| (height + 1).expect("committed heights are valid"))
300                .unwrap_or(Height(0));
301
302            if ordered_block.0.height != next_valid_height {
303                debug!(
304                    ?next_valid_height,
305                    invalid_height = ?ordered_block.0.height,
306                    invalid_hash = ?ordered_block.0.hash,
307                    "got a block that was the wrong height. \
308                     Assuming a parent block failed, and dropping this block",
309                );
310
311                // We don't want to send a reset here, because it could overwrite a valid sent hash
312                std::mem::drop(ordered_block);
313                continue;
314            }
315
316            // Try committing the block
317            match finalized_state
318                .commit_finalized(ordered_block, prev_finalized_note_commitment_trees.take())
319            {
320                Ok((finalized, note_commitment_trees)) => {
321                    let tip_block = ChainTipBlock::from(finalized);
322                    prev_finalized_note_commitment_trees = Some(note_commitment_trees);
323                    chain_tip_sender.set_finalized_tip(tip_block);
324                }
325                Err(error) => {
326                    let finalized_tip = finalized_state.db.tip();
327
328                    // The last block in the queue failed, so we can't commit the next block.
329                    // Instead, we need to reset the state queue,
330                    // and discard any children of the invalid block in the channel.
331                    info!(
332                        ?error,
333                        last_valid_height = ?finalized_tip.map(|tip| tip.0),
334                        last_valid_hash = ?finalized_tip.map(|tip| tip.1),
335                        "committing a block to the finalized state failed, resetting state queue",
336                    );
337
338                    let send_result =
339                        invalid_block_reset_sender.send(finalized_state.db.finalized_tip_hash());
340
341                    if send_result.is_err() {
342                        info!(
343                            "StateService closed the block reset channel. Is Zebra shutting down?"
344                        );
345                        return;
346                    }
347                }
348            }
349        }
350
351        // Do this check even if the channel got closed before any finalized blocks were sent.
352        // This can happen if we're past the finalized tip.
353        if invalid_block_reset_sender.is_closed() {
354            info!("StateService closed the block reset channel. Is Zebra shutting down?");
355            return;
356        }
357
358        // Save any errors to propagate down to queued child blocks
359        let mut parent_error_map: IndexMap<block::Hash, ValidateContextError> = IndexMap::new();
360
361        while let Some(msg) = non_finalized_block_write_receiver.blocking_recv() {
362            let queued_child_and_rsp_tx = match msg {
363                NonFinalizedWriteMessage::Commit(queued_child) => Some(queued_child),
364                NonFinalizedWriteMessage::Invalidate { hash, rsp_tx } => {
365                    tracing::info!(?hash, "invalidating a block in the non-finalized state");
366                    let _ = rsp_tx.send(non_finalized_state.invalidate_block(hash));
367                    None
368                }
369                NonFinalizedWriteMessage::Reconsider { hash, rsp_tx } => {
370                    tracing::info!(?hash, "reconsidering a block in the non-finalized state");
371                    let _ = rsp_tx
372                        .send(non_finalized_state.reconsider_block(hash, &finalized_state.db));
373                    None
374                }
375            };
376
377            let Some((queued_child, rsp_tx)) = queued_child_and_rsp_tx else {
378                update_latest_chain_channels(
379                    non_finalized_state,
380                    &finalized_state.db,
381                    chain_tip_sender,
382                    non_finalized_state_sender,
383                    backup_dir_path.as_deref(),
384                );
385                continue;
386            };
387
388            let child_hash = queued_child.hash;
389            let parent_hash = queued_child.block.header.previous_block_hash;
390            let parent_error = parent_error_map.get(&parent_hash);
391
392            // If the parent block was marked as rejected, also reject all its children.
393            //
394            // At this point, we know that all the block's descendants
395            // are invalid, because we checked all the consensus rules before
396            // committing the failing ancestor block to the non-finalized state.
397            //
398            // The children are served by other peers, which often queue them before
399            // the parent arrives, so they get a distinct error that doesn't carry the
400            // parent's misbehaviour score.
401            let result = if let Some(parent_error) = parent_error {
402                Err(parent_error.for_descendant(parent_hash))
403            } else {
404                tracing::trace!(?child_hash, "validating queued child");
405                validate_and_commit_non_finalized(
406                    &finalized_state.db,
407                    non_finalized_state,
408                    queued_child,
409                )
410            };
411
412            // TODO: fix the test timing bugs that require the result to be sent
413            //       after `update_latest_chain_channels()`,
414            //       and send the result on rsp_tx here
415
416            if let Err(ref error) = result {
417                // If the block is invalid, mark any descendant blocks as rejected.
418                parent_error_map.insert(child_hash, error.clone());
419
420                // Make sure the error map doesn't get too big.
421                if parent_error_map.len() > PARENT_ERROR_MAP_LIMIT {
422                    // We only add one hash at a time, so we only need to remove one extra here.
423                    parent_error_map.shift_remove_index(0);
424                }
425
426                // Signal the StateService to drop this hash from
427                // `non_finalized_block_write_sent_hashes`, so a subsequent
428                // re-delivery of a block at the same hash is not short-circuited
429                // as a "duplicate" against a rejected variant that never reached
430                // any chain.
431                //
432                // If the receiver was dropped (the StateService is shutting
433                // down), ignore the error: the lockout cannot matter once the
434                // service exits.
435                let _ = non_finalized_rejected_sender.send(child_hash);
436
437                // Update the caller with the error.
438                let _ = rsp_tx.send(result.map(|()| child_hash).map_err(Into::into));
439
440                // Skip the things we only need to do for successfully committed blocks
441                continue;
442            }
443
444            // A successfully committed block supersedes any contextual error
445            // recorded for a different block body with the same header hash.
446            parent_error_map.shift_remove(&child_hash);
447
448            // Committing blocks to the finalized state keeps the same chain,
449            // so we can update the chain seen by the rest of the application now.
450            //
451            // TODO: if this causes state request errors due to chain conflicts,
452            //       fix the `service::read` bugs,
453            //       or do the channel update after the finalized state commit
454            let tip_block_height = update_latest_chain_channels(
455                non_finalized_state,
456                &finalized_state.db,
457                chain_tip_sender,
458                non_finalized_state_sender,
459                backup_dir_path.as_deref(),
460            )
461            .expect("just successfully inserted a non-finalized block above");
462
463            // Update the caller with the result.
464            let _ = rsp_tx.send(result.map(|()| child_hash).map_err(Into::into));
465
466            while non_finalized_state
467                .best_chain_len()
468                .expect("just successfully inserted a non-finalized block above")
469                > MAX_BLOCK_REORG_HEIGHT
470            {
471                tracing::trace!("finalizing block past the reorg limit");
472                let contextually_verified_with_trees = non_finalized_state.finalize();
473                prev_finalized_note_commitment_trees = finalized_state
474                            .commit_finalized_direct(contextually_verified_with_trees, prev_finalized_note_commitment_trees.take(), "commit contextually-verified request")
475                            .expect(
476                                "unexpected finalized block commit error: note commitment and history trees were already checked by the non-finalized state",
477                            ).1.into();
478            }
479
480            // Update the metrics if semantic and contextual validation passes
481            //
482            // TODO: split this out into a function?
483            metrics::counter!("state.full_verifier.committed.block.count").increment(1);
484            metrics::counter!("zcash.chain.verified.block.total").increment(1);
485
486            metrics::gauge!("state.full_verifier.committed.block.height")
487                .set(tip_block_height.0 as f64);
488
489            // This height gauge is updated for both fully verified and checkpoint blocks.
490            // These updates can't conflict, because this block write task makes sure that blocks
491            // are committed in order.
492            metrics::gauge!("zcash.chain.verified.block.height").set(tip_block_height.0 as f64);
493
494            tracing::trace!("finished processing queued block");
495        }
496
497        // We're finished receiving non-finalized blocks from the state, and
498        // done writing to the finalized state, so we can force it to shut down.
499        finalized_state.db.shutdown(true);
500        std::mem::drop(self.finalized_state);
501    }
502}
503
504#[cfg(test)]
505mod tests;