Skip to main content

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