1use 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#[allow(unused_imports)]
31use crate::service::{
32 chain_tip::{ChainTipChange, LatestChainTip},
33 non_finalized_state::Chain,
34};
35
36const PARENT_ERROR_MAP_LIMIT: usize = MAX_BLOCK_REORG_HEIGHT as usize * 2;
40
41#[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#[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 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
125struct 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 non_finalized_rejected_sender: UnboundedSender<block::Hash>,
141 chain_tip_sender: ChainTipSender,
142 non_finalized_state_sender: watch::Sender<NonFinalizedState>,
143 backup_dir_path: Option<PathBuf>,
146}
147
148pub enum NonFinalizedWriteMessage {
150 Commit(QueuedSemanticallyVerified),
153 Invalidate {
156 hash: block::Hash,
157 rsp_tx: oneshot::Sender<Result<block::Hash, InvalidateError>>,
158 },
159 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#[derive(Clone, Debug)]
177pub(super) struct BlockWriteSender {
178 pub non_finalized: Option<tokio::sync::mpsc::UnboundedSender<NonFinalizedWriteMessage>>,
181
182 pub finalized: Option<tokio::sync::mpsc::UnboundedSender<QueuedCheckpointVerified>>,
188}
189
190impl BlockWriteSender {
191 #[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 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 #[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 while let Some(ordered_block) = finalized_block_write_receiver.blocking_recv() {
283 if invalid_block_reset_sender.is_closed() {
286 info!("StateService closed the block reset channel. Is Zebra shutting down?");
287 return;
288 }
289
290 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 std::mem::drop(ordered_block);
313 continue;
314 }
315
316 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 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 if invalid_block_reset_sender.is_closed() {
354 info!("StateService closed the block reset channel. Is Zebra shutting down?");
355 return;
356 }
357
358 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 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 if let Err(ref error) = result {
417 parent_error_map.insert(child_hash, error.clone());
419
420 if parent_error_map.len() > PARENT_ERROR_MAP_LIMIT {
422 parent_error_map.shift_remove_index(0);
424 }
425
426 let _ = non_finalized_rejected_sender.send(child_hash);
436
437 let _ = rsp_tx.send(result.map(|()| child_hash).map_err(Into::into));
439
440 continue;
442 }
443
444 parent_error_map.shift_remove(&child_hash);
447
448 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 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 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 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 finalized_state.db.shutdown(true);
500 std::mem::drop(self.finalized_state);
501 }
502}
503
504#[cfg(test)]
505mod tests;