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,
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}