zebra_state/service/finalized_state.rs
1//! The primary implementation of the `zebra_state::Service` built upon rocksdb.
2//!
3//! Zebra's database is implemented in 4 layers:
4//! - [`FinalizedState`]: queues, validates, and commits blocks, using...
5//! - [`ZebraDb`]: reads and writes [`zebra_chain`] types to the state database, using...
6//! - [`DiskDb`]: reads and writes generic types to any column family in the database, using...
7//! - [`disk_format`]: converts types to raw database bytes.
8//!
9//! These layers allow us to split [`zebra_chain`] types for efficient database storage.
10//! They reduce the risk of data corruption bugs, runtime inconsistencies, and panics.
11//!
12//! # Correctness
13//!
14//! [`crate::constants::state_database_format_version_in_code()`] must be incremented
15//! each time the database format (column, serialization, etc) changes.
16
17use std::{
18 io::{stderr, stdout, Write},
19 sync::Arc,
20};
21
22use zebra_chain::{
23 amount::DeferredPoolBalanceChange,
24 block,
25 parallel::tree::NoteCommitmentTrees,
26 parameters::{subsidy::block_subsidy, Network},
27 primitives::zcash_history::BlockCommitmentTreeRoots,
28};
29use zebra_db::{
30 chain::BLOCK_INFO,
31 transparent::{BALANCE_BY_TRANSPARENT_ADDR, TX_LOC_BY_SPENT_OUT_LOC},
32};
33
34use crate::{
35 constants::{state_database_format_version_in_code, STATE_DATABASE_KIND},
36 error::CommitCheckpointVerifiedError,
37 request::{FinalizableBlock, FinalizedBlock, Treestate},
38 service::{check, QueuedCheckpointVerified},
39 CheckpointVerifiedBlock, Config, StateInitError, ValidateContextError,
40};
41
42pub mod column_family;
43
44mod disk_db;
45mod disk_format;
46mod zebra_db;
47
48#[cfg(any(test, feature = "proptest-impl"))]
49mod arbitrary;
50
51#[cfg(test)]
52mod tests;
53
54#[allow(unused_imports)]
55pub use column_family::{TypedColumnFamily, WriteTypedBatch};
56#[allow(unused_imports)]
57pub use disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk};
58#[allow(unused_imports)]
59pub use disk_format::{
60 FromDisk, IntoDisk, OutputLocation, RawBytes, TransactionIndex, TransactionLocation,
61 MAX_ON_DISK_HEIGHT,
62};
63pub use zebra_db::ZebraDb;
64
65#[cfg(any(test, feature = "proptest-impl"))]
66pub use disk_format::KV;
67
68pub use disk_format::upgrade::restorable_db_versions;
69
70/// The column families supported by the running `zebra-state` database code.
71///
72/// Existing column families that aren't listed here are preserved when the database is opened.
73pub const STATE_COLUMN_FAMILIES_IN_CODE: &[&str] = &[
74 // Blocks
75 "hash_by_height",
76 "height_by_hash",
77 "block_header_by_height",
78 // Transactions
79 "tx_by_loc",
80 "hash_by_tx_loc",
81 "tx_loc_by_hash",
82 // Transparent
83 BALANCE_BY_TRANSPARENT_ADDR,
84 "tx_loc_by_transparent_addr_loc",
85 "utxo_by_out_loc",
86 "utxo_loc_by_transparent_addr_loc",
87 TX_LOC_BY_SPENT_OUT_LOC,
88 // Sprout
89 "sprout_nullifiers",
90 "sprout_anchors",
91 "sprout_note_commitment_tree",
92 // Sapling
93 "sapling_nullifiers",
94 "sapling_anchors",
95 "sapling_note_commitment_tree",
96 "sapling_note_commitment_subtree",
97 // Orchard
98 "orchard_nullifiers",
99 "orchard_anchors",
100 "orchard_note_commitment_tree",
101 "orchard_note_commitment_subtree",
102 // Ironwood (NU6.3). Always registered so the database format is stable across build
103 // flags; these stay empty until NU6.3 transactions appear.
104 "ironwood_nullifiers",
105 "ironwood_anchors",
106 "ironwood_note_commitment_tree",
107 "ironwood_note_commitment_subtree",
108 // Chain
109 "history_tree",
110 "tip_chain_value_pool",
111 BLOCK_INFO,
112];
113
114/// The finalized part of the chain state, stored in the db.
115///
116/// `rocksdb` allows concurrent writes through a shared reference,
117/// so clones of the finalized state represent the same database instance.
118/// When the final clone is dropped, the database is closed.
119///
120/// This is different from `NonFinalizedState::clone()`,
121/// which returns an independent copy of the chains.
122#[derive(Clone, Debug)]
123pub struct FinalizedState {
124 // Configuration
125 //
126 // This configuration cannot be modified after the database is initialized,
127 // because some clones would have different values.
128 //
129 /// The configured stop height.
130 ///
131 /// Commit blocks to the finalized state up to this height, then exit Zebra.
132 debug_stop_at_height: Option<block::Height>,
133
134 // Owned State
135 //
136 // Everything contained in this state must be shared by all clones, or read-only.
137 //
138 /// The underlying database.
139 ///
140 /// `rocksdb` allows reads and writes via a shared reference,
141 /// so this database object can be freely cloned.
142 /// The last instance that is dropped will close the underlying database.
143 pub db: ZebraDb,
144
145 #[cfg(feature = "elasticsearch")]
146 /// The elasticsearch handle.
147 pub elastic_db: Option<elasticsearch::Elasticsearch>,
148
149 #[cfg(feature = "elasticsearch")]
150 /// A collection of blocks to be sent to elasticsearch as a bulk.
151 pub elastic_blocks: Vec<String>,
152}
153
154impl FinalizedState {
155 /// Returns an on-disk database instance for `config`, `network`, and `elastic_db`.
156 /// If there is no existing database, creates a new database on disk.
157 pub fn new(
158 config: &Config,
159 network: &Network,
160 #[cfg(feature = "elasticsearch")] enable_elastic_db: bool,
161 ) -> Result<Self, StateInitError> {
162 Self::new_with_debug(
163 config,
164 network,
165 false,
166 #[cfg(feature = "elasticsearch")]
167 enable_elastic_db,
168 false,
169 )
170 }
171
172 /// Returns an on-disk database instance with the supplied production and debug settings.
173 /// If there is no existing database, creates a new database on disk.
174 ///
175 /// This method is intended for use in tests.
176 #[allow(clippy::unwrap_in_result)]
177 pub(crate) fn new_with_debug(
178 config: &Config,
179 network: &Network,
180 debug_skip_format_upgrades: bool,
181 #[cfg(feature = "elasticsearch")] enable_elastic_db: bool,
182 read_only: bool,
183 ) -> Result<Self, StateInitError> {
184 #[cfg(feature = "elasticsearch")]
185 let elastic_db = if enable_elastic_db {
186 use elasticsearch::{
187 auth::Credentials::Basic,
188 cert::CertificateValidation,
189 http::transport::{SingleNodeConnectionPool, TransportBuilder},
190 http::Url,
191 Elasticsearch,
192 };
193
194 let conn_pool = SingleNodeConnectionPool::new(
195 Url::parse(config.elasticsearch_url.as_str())
196 .expect("configured elasticsearch url is invalid"),
197 );
198 let transport = TransportBuilder::new(conn_pool)
199 .cert_validation(CertificateValidation::None)
200 .auth(Basic(
201 config.clone().elasticsearch_username,
202 config.clone().elasticsearch_password,
203 ))
204 .build()
205 .expect("elasticsearch transport builder should not fail");
206
207 Some(Elasticsearch::new(transport))
208 } else {
209 None
210 };
211
212 let db = ZebraDb::new(
213 config,
214 STATE_DATABASE_KIND,
215 &state_database_format_version_in_code(),
216 network,
217 debug_skip_format_upgrades,
218 STATE_COLUMN_FAMILIES_IN_CODE
219 .iter()
220 .map(ToString::to_string),
221 read_only,
222 )?;
223
224 #[cfg(feature = "elasticsearch")]
225 let new_state = Self {
226 debug_stop_at_height: config.debug_stop_at_height.map(block::Height),
227 db,
228 elastic_db,
229 elastic_blocks: vec![],
230 };
231
232 #[cfg(not(feature = "elasticsearch"))]
233 let new_state = Self {
234 debug_stop_at_height: config.debug_stop_at_height.map(block::Height),
235 db,
236 };
237
238 // TODO: move debug_stop_at_height into a task in the start command (#3442)
239 if let Some(tip_height) = new_state.db.finalized_tip_height() {
240 if new_state.is_at_stop_height(tip_height) {
241 let debug_stop_at_height = new_state
242 .debug_stop_at_height
243 .expect("true from `is_at_stop_height` implies `debug_stop_at_height` is Some");
244 let tip_hash = new_state.db.finalized_tip_hash();
245
246 if tip_height > debug_stop_at_height {
247 tracing::error!(
248 ?debug_stop_at_height,
249 ?tip_height,
250 ?tip_hash,
251 "previous state height is greater than the stop height",
252 );
253 }
254
255 tracing::info!(
256 ?debug_stop_at_height,
257 ?tip_height,
258 ?tip_hash,
259 "state is already at the configured height"
260 );
261
262 // RocksDB can do a cleanup when column families are opened.
263 // So we want to drop it before we exit.
264 std::mem::drop(new_state);
265
266 // Drops tracing log output that's hasn't already been written to stdout
267 // since this exits before calling drop on the WorkerGuard for the logger thread.
268 // This is okay for now because this is test-only code
269 //
270 // TODO: Call ZebradApp.shutdown or drop its Tracing component before calling exit_process to flush logs to stdout
271 Self::exit_process();
272 }
273 }
274
275 Ok(new_state)
276 }
277
278 /// Returns the configured network for this database.
279 pub fn network(&self) -> Network {
280 self.db.network()
281 }
282
283 /// Commit a checkpoint-verified block to the state.
284 ///
285 /// It's the caller's responsibility to ensure that blocks are committed in
286 /// order.
287 pub fn commit_finalized(
288 &mut self,
289 ordered_block: QueuedCheckpointVerified,
290 prev_note_commitment_trees: Option<NoteCommitmentTrees>,
291 ) -> Result<(CheckpointVerifiedBlock, NoteCommitmentTrees), CommitCheckpointVerifiedError> {
292 let (checkpoint_verified, rsp_tx) = ordered_block;
293 let result = self.commit_finalized_direct(
294 checkpoint_verified.clone().into(),
295 prev_note_commitment_trees,
296 "commit checkpoint-verified request",
297 );
298
299 if result.is_ok() {
300 metrics::counter!("state.checkpoint.finalized.block.count").increment(1);
301 metrics::gauge!("state.checkpoint.finalized.block.height")
302 .set(checkpoint_verified.height.0 as f64);
303
304 // This height gauge is updated for both fully verified and checkpoint blocks.
305 // These updates can't conflict, because the state makes sure that blocks
306 // are committed in order.
307 metrics::gauge!("zcash.chain.verified.block.height")
308 .set(checkpoint_verified.height.0 as f64);
309 metrics::counter!("zcash.chain.verified.block.total").increment(1);
310 } else {
311 metrics::counter!("state.checkpoint.error.block.count").increment(1);
312 metrics::gauge!("state.checkpoint.error.block.height")
313 .set(checkpoint_verified.height.0 as f64);
314 };
315
316 let _ = rsp_tx.send(result.clone().map(|(hash, _)| hash));
317
318 result.map(|(_hash, note_commitment_trees)| (checkpoint_verified, note_commitment_trees))
319 }
320
321 /// Immediately commit a `finalized` block to the finalized state.
322 ///
323 /// This can be called either by the non-finalized state (when finalizing
324 /// a block) or by the checkpoint verifier.
325 ///
326 /// Use `source` as the source of the block in log messages.
327 ///
328 /// # Errors
329 ///
330 /// - Propagates any errors from writing to the DB
331 /// - Propagates any errors from updating history and note commitment trees
332 /// - If `hashFinalSaplingRoot` / `hashLightClientRoot` / `hashBlockCommitments`
333 /// does not match the expected value
334 #[allow(clippy::unwrap_in_result)]
335 pub fn commit_finalized_direct(
336 &mut self,
337 finalizable_block: FinalizableBlock,
338 prev_note_commitment_trees: Option<NoteCommitmentTrees>,
339 source: &str,
340 ) -> Result<(block::Hash, NoteCommitmentTrees), CommitCheckpointVerifiedError> {
341 let (height, hash, finalized, prev_note_commitment_trees) = match finalizable_block {
342 FinalizableBlock::Checkpoint {
343 checkpoint_verified,
344 } => {
345 // Checkpoint-verified blocks don't have an associated treestate, so we retrieve the
346 // treestate of the finalized tip from the database and update it for the block
347 // being committed, assuming the retrieved treestate is the parent block's
348 // treestate. Later on, this function proves this assumption by asserting that the
349 // finalized tip is the parent block of the block being committed.
350
351 let block = checkpoint_verified.block.clone();
352 let mut history_tree = self.db.history_tree();
353 let prev_note_commitment_trees = prev_note_commitment_trees
354 .unwrap_or_else(|| self.db.note_commitment_trees_for_tip());
355
356 // Update the note commitment trees.
357 let mut note_commitment_trees = prev_note_commitment_trees.clone();
358 note_commitment_trees
359 .update_trees_parallel(&block)
360 .map_err(ValidateContextError::from)?;
361
362 // Check the block commitment if the history tree was not
363 // supplied by the non-finalized state. Note that we don't do
364 // this check for history trees supplied by the non-finalized
365 // state because the non-finalized state checks the block
366 // commitment.
367 //
368 // For Nu5-onward, the block hash commits only to
369 // non-authorizing data (see ZIP-244). This checks the
370 // authorizing data commitment, making sure the entire block
371 // contents were committed to. The test is done here (and not
372 // during semantic validation) because it needs the history tree
373 // root. While it _is_ checked during contextual validation,
374 // that is not called by the checkpoint verifier, and keeping a
375 // history tree there would be harder to implement.
376 //
377 // TODO: run this CPU-intensive cryptography in a parallel rayon
378 // thread, if it shows up in profiles
379 check::block_commitment_is_valid_for_chain_history(
380 block.clone(),
381 &self.network(),
382 &history_tree,
383 )?;
384
385 // Update the history tree.
386 //
387 // TODO: run this CPU-intensive cryptography in a parallel rayon
388 // thread, if it shows up in profiles
389 let history_tree_mut = Arc::make_mut(&mut history_tree);
390 let sapling_root = note_commitment_trees.sapling.root();
391 let orchard_root = note_commitment_trees.orchard.root();
392 let ironwood_root = note_commitment_trees.ironwood.root();
393 history_tree_mut
394 .push(
395 &self.network(),
396 block.clone(),
397 BlockCommitmentTreeRoots {
398 sapling: &sapling_root,
399 orchard: &orchard_root,
400 ironwood: &ironwood_root,
401 },
402 )
403 .map_err(Arc::new)
404 .map_err(ValidateContextError::from)?;
405
406 let treestate = Treestate {
407 note_commitment_trees,
408 history_tree,
409 };
410
411 let height = checkpoint_verified.height;
412
413 (
414 height,
415 checkpoint_verified.hash,
416 FinalizedBlock::from_checkpoint_verified(
417 checkpoint_verified,
418 treestate,
419 calculate_deferred_pool_balance_change(height, &self.network()),
420 ),
421 Some(prev_note_commitment_trees),
422 )
423 }
424 FinalizableBlock::Contextual {
425 contextually_verified,
426 treestate,
427 } => {
428 let height = contextually_verified.height;
429 (
430 height,
431 contextually_verified.hash,
432 FinalizedBlock::from_contextually_verified(
433 contextually_verified,
434 treestate,
435 calculate_deferred_pool_balance_change(height, &self.network()),
436 ),
437 prev_note_commitment_trees,
438 )
439 }
440 };
441
442 let committed_tip_hash = self.db.finalized_tip_hash();
443 let committed_tip_height = self.db.finalized_tip_height();
444
445 // Assert that callers (including unit tests) get the chain order correct
446 if self.db.is_empty() {
447 assert_eq!(
448 committed_tip_hash, finalized.block.header.previous_block_hash,
449 "the first block added to an empty state must be a genesis block, source: {source}",
450 );
451 assert_eq!(
452 block::Height(0),
453 height,
454 "cannot commit genesis: invalid height, source: {source}",
455 );
456 } else {
457 assert_eq!(
458 committed_tip_height.expect("state must have a genesis block committed") + 1,
459 Some(height),
460 "committed block height must be 1 more than the finalized tip height, source: {source}",
461 );
462
463 assert_eq!(
464 committed_tip_hash, finalized.block.header.previous_block_hash,
465 "committed block must be a child of the finalized tip, source: {source}",
466 );
467 }
468
469 #[cfg(feature = "elasticsearch")]
470 let finalized_inner_block = finalized.block.clone();
471 let note_commitment_trees = finalized.treestate.note_commitment_trees.clone();
472
473 let result = self.db.write_block(
474 finalized,
475 prev_note_commitment_trees,
476 &self.network(),
477 source,
478 );
479
480 if result.is_ok() {
481 // Save blocks to elasticsearch if the feature is enabled.
482 #[cfg(feature = "elasticsearch")]
483 self.elasticsearch(&finalized_inner_block);
484
485 // TODO: move the stop height check to the syncer (#3442)
486 if self.is_at_stop_height(height) {
487 tracing::info!(
488 ?height,
489 ?hash,
490 block_source = ?source,
491 "stopping at configured height, flushing database to disk"
492 );
493
494 // We're just about to do a forced exit, so it's ok to do a forced db shutdown
495 self.db.shutdown(true);
496
497 // Drops tracing log output that's hasn't already been written to stdout
498 // since this exits before calling drop on the WorkerGuard for the logger thread.
499 // This is okay for now because this is test-only code
500 //
501 // TODO: Call ZebradApp.shutdown or drop its Tracing component before calling exit_process to flush logs to stdout
502 Self::exit_process();
503 }
504 }
505
506 result.map(|hash| (hash, note_commitment_trees))
507 }
508
509 #[cfg(feature = "elasticsearch")]
510 /// Store finalized blocks into an elasticsearch database.
511 ///
512 /// We use the elasticsearch bulk api to index multiple blocks at a time while we are
513 /// synchronizing the chain, when we get close to tip we index blocks one by one.
514 pub fn elasticsearch(&mut self, block: &Arc<block::Block>) {
515 if let Some(client) = self.elastic_db.clone() {
516 let block_time = block.header.time.timestamp();
517 let local_time = chrono::Utc::now().timestamp();
518
519 // Bulk size is small enough to avoid the elasticsearch 100mb content length limitation.
520 // MAX_BLOCK_BYTES = 2MB but each block use around 4.1 MB of JSON.
521 // Each block count as 2 as we send them with a operation/header line. A value of 48
522 // is 24 blocks.
523 const AWAY_FROM_TIP_BULK_SIZE: usize = 48;
524
525 // The number of blocks the bulk will have when we are in sync.
526 // A value of 2 means only 1 block as we want to insert them as soon as we get
527 // them for a real time experience. This is the same for mainnet and testnet.
528 const CLOSE_TO_TIP_BULK_SIZE: usize = 2;
529
530 // We consider in sync when the local time and the blockchain time difference is
531 // less than this number of seconds.
532 const CLOSE_TO_TIP_SECONDS: i64 = 14400; // 4 hours
533
534 let mut blocks_size_to_dump = AWAY_FROM_TIP_BULK_SIZE;
535
536 // If we are close to the tip, index one block per bulk call.
537 if local_time - block_time < CLOSE_TO_TIP_SECONDS {
538 blocks_size_to_dump = CLOSE_TO_TIP_BULK_SIZE;
539 }
540
541 // Insert the operation line.
542 let height_number = block.coinbase_height().unwrap_or(block::Height(0)).0;
543 self.elastic_blocks.push(
544 serde_json::json!({
545 "index": {
546 "_id": height_number.to_string().as_str()
547 }
548 })
549 .to_string(),
550 );
551
552 // Insert the block itself.
553 self.elastic_blocks
554 .push(serde_json::json!(block).to_string());
555
556 // We are in bulk time, insert to ES all we have.
557 if self.elastic_blocks.len() >= blocks_size_to_dump {
558 let rt = tokio::runtime::Runtime::new()
559 .expect("runtime creation for elasticsearch should not fail.");
560 let blocks = self.elastic_blocks.clone();
561 let network = self.network();
562
563 rt.block_on(async move {
564 // Send a ping to the server to check if it is available before inserting.
565 if client.ping().send().await.is_err() {
566 tracing::error!("Elasticsearch is not available, skipping block indexing");
567 return;
568 }
569
570 let response = client
571 .bulk(elasticsearch::BulkParts::Index(
572 format!("zcash_{}", network.to_string().to_lowercase()).as_str(),
573 ))
574 .body(blocks)
575 .send()
576 .await
577 .expect("ES Request should never fail");
578
579 // Make sure no errors ever.
580 let response_body = response
581 .json::<serde_json::Value>()
582 .await
583 .expect("ES response parsing error. Maybe we are sending more than 100 mb of data (`http.max_content_length`)");
584 let errors = response_body["errors"].as_bool().unwrap_or(true);
585 assert!(!errors, "{}", format!("ES error: {response_body}"));
586 });
587
588 // Clean the block storage.
589 self.elastic_blocks.clear();
590 }
591 }
592 }
593
594 /// Stop the process if `block_height` is greater than or equal to the
595 /// configured stop height.
596 fn is_at_stop_height(&self, block_height: block::Height) -> bool {
597 let debug_stop_at_height = match self.debug_stop_at_height {
598 Some(debug_stop_at_height) => debug_stop_at_height,
599 None => return false,
600 };
601
602 if block_height < debug_stop_at_height {
603 return false;
604 }
605
606 true
607 }
608
609 /// Exit the host process.
610 ///
611 /// Designed for debugging and tests.
612 ///
613 /// TODO: move the stop height check to the syncer (#3442)
614 fn exit_process() -> ! {
615 tracing::info!("exiting Zebra");
616
617 // Some OSes require a flush to send all output to the terminal.
618 // Zebra's logging doesn't depend on `tokio`, so we flush the stdlib sync streams.
619 //
620 // TODO: if this doesn't work, send an empty line as well.
621 let _ = stdout().lock().flush();
622 let _ = stderr().lock().flush();
623
624 // Give some time to logger thread to flush out any remaining lines to stdout
625 // and yield so that tests pass on MacOS
626 std::thread::sleep(std::time::Duration::from_secs(3));
627
628 // Exits before calling drop on the WorkerGuard for the logger thread,
629 // dropping any lines that haven't already been written to stdout.
630 // This is okay for now because this is test-only code
631 std::process::exit(0);
632 }
633}
634
635/// Calculates the deferred pool balance change for a given height and network.
636///
637/// Returns a deferred pool balance change of zero if it cannot be calculated.
638pub(crate) fn calculate_deferred_pool_balance_change(
639 height: block::Height,
640 network: &Network,
641) -> DeferredPoolBalanceChange {
642 if height > network.slow_start_interval() {
643 zebra_chain::parameters::subsidy::funding_stream_values(
644 height,
645 network,
646 block_subsidy(height, network).unwrap_or_default(),
647 )
648 .unwrap_or_default()
649 .remove(&zebra_chain::parameters::subsidy::FundingStreamReceiver::Deferred)
650 .unwrap_or_default()
651 .checked_sub(network.lockbox_disbursement_total_amount(height))
652 .map(DeferredPoolBalanceChange::new)
653 .unwrap_or_default()
654 } else {
655 DeferredPoolBalanceChange::zero()
656 }
657}