Skip to main content

zebra_state/service/finalized_state/zebra_db/
block.rs

1//! Provides high-level access to database [`Block`]s and [`Transaction`]s.
2//!
3//! This module makes sure that:
4//! - all disk writes happen inside a RocksDB transaction, and
5//! - format-specific invariants are maintained.
6//!
7//! # Correctness
8//!
9//! [`crate::constants::state_database_format_version_in_code()`] must be incremented
10//! each time the database format (column, serialization, etc) changes.
11
12use std::{
13    collections::{BTreeMap, HashMap, HashSet},
14    ops::RangeBounds,
15    sync::Arc,
16};
17
18use chrono::{DateTime, Utc};
19use itertools::Itertools;
20
21use zebra_chain::{
22    amount::NonNegative,
23    block::{self, Block, Height},
24    orchard,
25    parallel::tree::NoteCommitmentTrees,
26    parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH},
27    sapling,
28    serialization::{CompactSizeMessage, TrustedPreallocate, ZcashSerialize as _},
29    transaction::{self, Transaction},
30    transparent,
31    value_balance::ValueBalance,
32};
33
34use crate::{
35    error::CommitCheckpointVerifiedError,
36    request::FinalizedBlock,
37    service::finalized_state::{
38        disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk},
39        disk_format::{
40            block::TransactionLocation,
41            transparent::{AddressBalanceLocationUpdates, OutputLocation},
42        },
43        zebra_db::{metrics::block_precommit_metrics, ZebraDb},
44        FromDisk, RawBytes,
45    },
46    HashOrHeight,
47};
48
49#[cfg(feature = "indexer")]
50use crate::request::Spend;
51
52#[cfg(test)]
53mod tests;
54
55impl ZebraDb {
56    // Read block methods
57
58    /// Returns true if the database is empty.
59    //
60    // TODO: move this method to the tip section
61    pub fn is_empty(&self) -> bool {
62        let hash_by_height = self.db.cf_handle("hash_by_height").unwrap();
63        self.db.zs_is_empty(&hash_by_height)
64    }
65
66    /// Returns the tip height and hash, if there is one.
67    //
68    // TODO: rename to finalized_tip()
69    //       move this method to the tip section
70    #[allow(clippy::unwrap_in_result)]
71    pub fn tip(&self) -> Option<(block::Height, block::Hash)> {
72        let hash_by_height = self.db.cf_handle("hash_by_height").unwrap();
73        self.db.zs_last_key_value(&hash_by_height)
74    }
75
76    /// Returns `true` if `height` is present in the finalized state.
77    #[allow(clippy::unwrap_in_result)]
78    pub fn contains_height(&self, height: block::Height) -> bool {
79        let hash_by_height = self.db.cf_handle("hash_by_height").unwrap();
80
81        self.db.zs_contains(&hash_by_height, &height)
82    }
83
84    /// Returns the finalized hash for a given `block::Height` if it is present.
85    #[allow(clippy::unwrap_in_result)]
86    pub fn hash(&self, height: block::Height) -> Option<block::Hash> {
87        let hash_by_height = self.db.cf_handle("hash_by_height").unwrap();
88        self.db.zs_get(&hash_by_height, &height)
89    }
90
91    /// Returns `true` if `hash` is present in the finalized state.
92    #[allow(clippy::unwrap_in_result)]
93    pub fn contains_hash(&self, hash: block::Hash) -> bool {
94        let height_by_hash = self.db.cf_handle("height_by_hash").unwrap();
95
96        self.db.zs_contains(&height_by_hash, &hash)
97    }
98
99    /// Returns the height of the given block if it exists.
100    #[allow(clippy::unwrap_in_result)]
101    pub fn height(&self, hash: block::Hash) -> Option<block::Height> {
102        let height_by_hash = self.db.cf_handle("height_by_hash").unwrap();
103        self.db.zs_get(&height_by_hash, &hash)
104    }
105
106    /// Returns the previous block hash for the given block hash in the finalized state.
107    #[allow(dead_code)]
108    pub fn prev_block_hash_for_hash(&self, hash: block::Hash) -> Option<block::Hash> {
109        let height = self.height(hash)?;
110        let prev_height = height.previous().ok()?;
111
112        self.hash(prev_height)
113    }
114
115    /// Returns the previous block height for the given block hash in the finalized state.
116    #[allow(dead_code)]
117    pub fn prev_block_height_for_hash(&self, hash: block::Hash) -> Option<block::Height> {
118        let height = self.height(hash)?;
119
120        height.previous().ok()
121    }
122
123    /// Returns the [`block::Header`] with [`block::Hash`] or
124    /// [`Height`], if it exists in the finalized chain.
125    //
126    // TODO: move this method to the start of the section
127    #[allow(clippy::unwrap_in_result)]
128    pub fn block_header(&self, hash_or_height: HashOrHeight) -> Option<Arc<block::Header>> {
129        // Block Header
130        let block_header_by_height = self.db.cf_handle("block_header_by_height").unwrap();
131
132        let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
133        let header = self.db.zs_get(&block_header_by_height, &height)?;
134
135        Some(header)
136    }
137
138    /// Returns the raw [`block::Header`] with [`block::Hash`] or [`Height`], if
139    /// it exists in the finalized chain.
140    #[allow(clippy::unwrap_in_result)]
141    fn raw_block_header(&self, hash_or_height: HashOrHeight) -> Option<RawBytes> {
142        // Block Header
143        let block_header_by_height = self.db.cf_handle("block_header_by_height").unwrap();
144
145        let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
146        let header: RawBytes = self.db.zs_get(&block_header_by_height, &height)?;
147
148        Some(header)
149    }
150
151    /// Returns the [`Block`] with [`block::Hash`] or
152    /// [`Height`], if it exists in the finalized chain.
153    //
154    // TODO: move this method to the start of the section
155    #[allow(clippy::unwrap_in_result)]
156    pub fn block(&self, hash_or_height: HashOrHeight) -> Option<Arc<Block>> {
157        // Block
158        let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
159        let header = self.block_header(height.into())?;
160
161        // Transactions
162
163        // TODO:
164        // - split disk reads from deserialization, and run deserialization in parallel,
165        //   this improves performance for blocks with multiple large shielded transactions
166        // - is this loop more efficient if we store the number of transactions?
167        // - is the difference large enough to matter?
168        let transactions = self
169            .transactions_by_height(height)
170            .map(|(_, tx)| tx)
171            .map(Arc::new)
172            .collect();
173
174        Some(Arc::new(Block {
175            header,
176            transactions,
177        }))
178    }
179
180    /// Returns the [`Block`] with [`block::Hash`] or [`Height`], if it exists
181    /// in the finalized chain, and its serialized size.
182    #[allow(clippy::unwrap_in_result)]
183    pub fn block_and_size(&self, hash_or_height: HashOrHeight) -> Option<(Arc<Block>, usize)> {
184        let (raw_header, raw_txs) = self.raw_block(hash_or_height)?;
185
186        let header = Arc::<block::Header>::from_bytes(raw_header.raw_bytes());
187        let txs: Vec<_> = raw_txs
188            .iter()
189            .map(|raw_tx| Arc::<Transaction>::from_bytes(raw_tx.raw_bytes()))
190            .collect();
191
192        // Compute the size of the block from the size of header and size of
193        // transactions. This requires summing them all and also adding the
194        // size of the CompactSize-encoded transaction count.
195        // See https://developer.bitcoin.org/reference/block_chain.html#serialized-blocks
196        let tx_count = CompactSizeMessage::try_from(txs.len())
197            .expect("must work for a previously serialized block");
198        let tx_raw = tx_count
199            .zcash_serialize_to_vec()
200            .expect("must work for a previously serialized block");
201        let size = raw_header.raw_bytes().len()
202            + raw_txs
203                .iter()
204                .map(|raw_tx| raw_tx.raw_bytes().len())
205                .sum::<usize>()
206            + tx_raw.len();
207
208        let block = Block {
209            header,
210            transactions: txs,
211        };
212        Some((Arc::new(block), size))
213    }
214
215    /// Returns the raw [`Block`] with [`block::Hash`] or
216    /// [`Height`], if it exists in the finalized chain.
217    #[allow(clippy::unwrap_in_result)]
218    fn raw_block(&self, hash_or_height: HashOrHeight) -> Option<(RawBytes, Vec<RawBytes>)> {
219        // Block
220        let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
221        let header = self.raw_block_header(height.into())?;
222
223        // Transactions
224
225        let transactions = self
226            .raw_transactions_by_height(height)
227            .map(|(_, tx)| tx)
228            .collect();
229
230        Some((header, transactions))
231    }
232
233    /// Returns the Sapling [`note commitment tree`](sapling::tree::NoteCommitmentTree) specified by
234    /// a hash or height, if it exists in the finalized state.
235    #[allow(clippy::unwrap_in_result)]
236    pub fn sapling_tree_by_hash_or_height(
237        &self,
238        hash_or_height: HashOrHeight,
239    ) -> Option<Arc<sapling::tree::NoteCommitmentTree>> {
240        let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
241
242        self.sapling_tree_by_height(&height)
243    }
244
245    /// Returns the Orchard [`note commitment tree`](orchard::tree::NoteCommitmentTree) specified by
246    /// a hash or height, if it exists in the finalized state.
247    #[allow(clippy::unwrap_in_result)]
248    pub fn orchard_tree_by_hash_or_height(
249        &self,
250        hash_or_height: HashOrHeight,
251    ) -> Option<Arc<orchard::tree::NoteCommitmentTree>> {
252        let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
253
254        self.orchard_tree_by_height(&height)
255    }
256
257    /// Returns the Ironwood [`note commitment tree`](orchard::tree::NoteCommitmentTree) specified by
258    /// a hash or height, if it exists in the finalized state.
259    #[allow(clippy::unwrap_in_result)]
260    pub fn ironwood_tree_by_hash_or_height(
261        &self,
262        hash_or_height: HashOrHeight,
263    ) -> Option<Arc<orchard::tree::NoteCommitmentTree>> {
264        let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
265
266        self.ironwood_tree_by_height(&height)
267    }
268
269    // Read tip block methods
270
271    /// Returns the hash of the current finalized tip block.
272    pub fn finalized_tip_hash(&self) -> block::Hash {
273        self.tip()
274            .map(|(_, hash)| hash)
275            // if the state is empty, return the genesis previous block hash
276            .unwrap_or(GENESIS_PREVIOUS_BLOCK_HASH)
277    }
278
279    /// Returns the height of the current finalized tip block.
280    pub fn finalized_tip_height(&self) -> Option<block::Height> {
281        self.tip().map(|(height, _)| height)
282    }
283
284    /// Returns the tip block, if there is one.
285    pub fn tip_block(&self) -> Option<Arc<Block>> {
286        let (height, _hash) = self.tip()?;
287        self.block(height.into())
288    }
289
290    // Read transaction methods
291
292    /// Returns the [`Transaction`] with [`transaction::Hash`], and its [`Height`],
293    /// if a transaction with that hash exists in the finalized chain.
294    #[allow(clippy::unwrap_in_result)]
295    pub fn transaction(
296        &self,
297        hash: transaction::Hash,
298    ) -> Option<(Arc<Transaction>, Height, DateTime<Utc>)> {
299        let tx_by_loc = self.db.cf_handle("tx_by_loc").unwrap();
300
301        let transaction_location = self.transaction_location(hash)?;
302
303        let block_time = self
304            .block_header(transaction_location.height.into())
305            .map(|header| header.time);
306
307        self.db
308            .zs_get(&tx_by_loc, &transaction_location)
309            .and_then(|tx| block_time.map(|time| (tx, transaction_location.height, time)))
310    }
311
312    /// Returns an iterator of all [`Transaction`]s for a provided block height in finalized state.
313    #[allow(clippy::unwrap_in_result)]
314    pub fn transactions_by_height(
315        &self,
316        height: Height,
317    ) -> impl Iterator<Item = (TransactionLocation, Transaction)> + '_ {
318        self.transactions_by_location_range(
319            TransactionLocation::min_for_height(height)
320                ..=TransactionLocation::max_for_height(height),
321        )
322    }
323
324    /// Returns an iterator of all raw [`Transaction`]s for a provided block
325    /// height in finalized state.
326    #[allow(clippy::unwrap_in_result)]
327    fn raw_transactions_by_height(
328        &self,
329        height: Height,
330    ) -> impl Iterator<Item = (TransactionLocation, RawBytes)> + '_ {
331        self.raw_transactions_by_location_range(
332            TransactionLocation::min_for_height(height)
333                ..=TransactionLocation::max_for_height(height),
334        )
335    }
336
337    /// Returns an iterator of all [`Transaction`]s in the provided range
338    /// of [`TransactionLocation`]s in finalized state.
339    #[allow(clippy::unwrap_in_result)]
340    pub fn transactions_by_location_range<R>(
341        &self,
342        range: R,
343    ) -> impl Iterator<Item = (TransactionLocation, Transaction)> + '_
344    where
345        R: RangeBounds<TransactionLocation>,
346    {
347        let tx_by_loc = self.db.cf_handle("tx_by_loc").unwrap();
348        self.db.zs_forward_range_iter(tx_by_loc, range)
349    }
350
351    /// Returns an iterator of all raw [`Transaction`]s in the provided range
352    /// of [`TransactionLocation`]s in finalized state.
353    #[allow(clippy::unwrap_in_result)]
354    pub fn raw_transactions_by_location_range<R>(
355        &self,
356        range: R,
357    ) -> impl Iterator<Item = (TransactionLocation, RawBytes)> + '_
358    where
359        R: RangeBounds<TransactionLocation>,
360    {
361        let tx_by_loc = self.db.cf_handle("tx_by_loc").unwrap();
362        self.db.zs_forward_range_iter(tx_by_loc, range)
363    }
364
365    /// Returns the [`TransactionLocation`] for [`transaction::Hash`],
366    /// if it exists in the finalized chain.
367    #[allow(clippy::unwrap_in_result)]
368    pub fn transaction_location(&self, hash: transaction::Hash) -> Option<TransactionLocation> {
369        let tx_loc_by_hash = self.db.cf_handle("tx_loc_by_hash").unwrap();
370        self.db.zs_get(&tx_loc_by_hash, &hash)
371    }
372
373    /// Returns the [`transaction::Hash`] for [`TransactionLocation`],
374    /// if it exists in the finalized chain.
375    #[allow(clippy::unwrap_in_result)]
376    #[allow(dead_code)]
377    pub fn transaction_hash(&self, location: TransactionLocation) -> Option<transaction::Hash> {
378        let hash_by_tx_loc = self.db.cf_handle("hash_by_tx_loc").unwrap();
379        self.db.zs_get(&hash_by_tx_loc, &location)
380    }
381
382    /// Returns the [`transaction::Hash`] of the transaction that spent or revealed the given
383    /// [`transparent::OutPoint`] or nullifier, if it is spent or revealed in the finalized state.
384    #[cfg(feature = "indexer")]
385    pub fn spending_transaction_hash(&self, spend: &Spend) -> Option<transaction::Hash> {
386        let tx_loc = match spend {
387            Spend::OutPoint(outpoint) => self.spending_tx_loc(outpoint)?,
388            Spend::Sprout(nullifier) => self.sprout_revealing_tx_loc(nullifier)?,
389            Spend::Sapling(nullifier) => self.sapling_revealing_tx_loc(nullifier)?,
390            Spend::Orchard(nullifier) => self.orchard_revealing_tx_loc(nullifier)?,
391            Spend::Ironwood(nullifier) => self.ironwood_revealing_tx_loc(nullifier)?,
392        };
393
394        self.transaction_hash(tx_loc)
395    }
396
397    /// Returns the [`transaction::Hash`]es in the block with `hash_or_height`,
398    /// if it exists in this chain.
399    ///
400    /// Hashes are returned in block order.
401    ///
402    /// Returns `None` if the block is not found.
403    #[allow(clippy::unwrap_in_result)]
404    pub fn transaction_hashes_for_block(
405        &self,
406        hash_or_height: HashOrHeight,
407    ) -> Option<Arc<[transaction::Hash]>> {
408        // Block
409        let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
410
411        // Transaction hashes
412        let hash_by_tx_loc = self.db.cf_handle("hash_by_tx_loc").unwrap();
413
414        // Manually fetch the entire block's transaction hashes
415        let mut transaction_hashes = Vec::new();
416
417        for tx_index in 0..=Transaction::max_allocation() {
418            let tx_loc = TransactionLocation::from_u64(height, tx_index);
419
420            if let Some(tx_hash) = self.db.zs_get(&hash_by_tx_loc, &tx_loc) {
421                transaction_hashes.push(tx_hash);
422            } else {
423                break;
424            }
425        }
426
427        Some(transaction_hashes.into())
428    }
429
430    // Write block methods
431
432    /// Write `finalized` to the finalized state.
433    ///
434    /// Uses:
435    /// - `history_tree`: the current tip's history tree
436    /// - `network`: the configured network
437    /// - `source`: the source of the block in log messages
438    ///
439    /// # Errors
440    ///
441    /// - Propagates any errors from writing to the DB
442    /// - Propagates any errors from computing the block's chain value balance change or
443    ///   from applying the change to the chain value balance
444    #[allow(clippy::unwrap_in_result)]
445    pub(in super::super) fn write_block(
446        &mut self,
447        finalized: FinalizedBlock,
448        prev_note_commitment_trees: Option<NoteCommitmentTrees>,
449        network: &Network,
450        source: &str,
451    ) -> Result<block::Hash, CommitCheckpointVerifiedError> {
452        let tx_hash_indexes: HashMap<transaction::Hash, usize> = finalized
453            .transaction_hashes
454            .iter()
455            .enumerate()
456            .map(|(index, hash)| (*hash, index))
457            .collect();
458
459        // Get a list of the new UTXOs in the format we need for database updates.
460        //
461        // TODO: index new_outputs by TransactionLocation,
462        //       simplify the spent_utxos location lookup code,
463        //       and remove the extra new_outputs_by_out_loc argument
464        let new_outputs_by_out_loc: BTreeMap<OutputLocation, transparent::Utxo> = finalized
465            .new_outputs
466            .iter()
467            .map(|(outpoint, ordered_utxo)| {
468                (
469                    lookup_out_loc(finalized.height, outpoint, &tx_hash_indexes),
470                    ordered_utxo.utxo.clone(),
471                )
472            })
473            .collect();
474
475        // Get a list of the spent UTXOs, before we delete any from the database
476        let spent_utxos: Vec<(transparent::OutPoint, OutputLocation, transparent::Utxo)> =
477            finalized
478                .block
479                .transactions
480                .iter()
481                .flat_map(|tx| tx.inputs().iter())
482                .flat_map(|input| input.outpoint())
483                .map(|outpoint| {
484                    (
485                        outpoint,
486                        // Some utxos are spent in the same block, so they will be in
487                        // `tx_hash_indexes` and `new_outputs`
488                        self.output_location(&outpoint).unwrap_or_else(|| {
489                            lookup_out_loc(finalized.height, &outpoint, &tx_hash_indexes)
490                        }),
491                        self.utxo(&outpoint)
492                            .map(|ordered_utxo| ordered_utxo.utxo)
493                            .or_else(|| {
494                                finalized
495                                    .new_outputs
496                                    .get(&outpoint)
497                                    .map(|ordered_utxo| ordered_utxo.utxo.clone())
498                            })
499                            .expect("already checked UTXO was in state or block"),
500                    )
501                })
502                .collect();
503
504        let spent_utxos_by_outpoint: HashMap<transparent::OutPoint, transparent::Utxo> =
505            spent_utxos
506                .iter()
507                .map(|(outpoint, _output_loc, utxo)| (*outpoint, utxo.clone()))
508                .collect();
509
510        // TODO: Add `OutputLocation`s to the values in `spent_utxos_by_outpoint` to avoid creating a second hashmap with the same keys
511        #[cfg(feature = "indexer")]
512        let out_loc_by_outpoint: HashMap<transparent::OutPoint, OutputLocation> = spent_utxos
513            .iter()
514            .map(|(outpoint, out_loc, _utxo)| (*outpoint, *out_loc))
515            .collect();
516        let spent_utxos_by_out_loc: BTreeMap<OutputLocation, transparent::Utxo> = spent_utxos
517            .into_iter()
518            .map(|(_outpoint, out_loc, utxo)| (out_loc, utxo))
519            .collect();
520
521        // Get the transparent addresses with changed balances/UTXOs
522        let changed_addresses: HashSet<transparent::Address> = spent_utxos_by_out_loc
523            .values()
524            .chain(
525                finalized
526                    .new_outputs
527                    .values()
528                    .map(|ordered_utxo| &ordered_utxo.utxo),
529            )
530            .filter_map(|utxo| utxo.output.address(network))
531            .unique()
532            .collect();
533
534        // Get the current address balances, before the transactions in this block
535
536        fn read_addr_locs<T, F: Fn(&transparent::Address) -> Option<T>>(
537            changed_addresses: HashSet<transparent::Address>,
538            f: F,
539        ) -> HashMap<transparent::Address, T> {
540            changed_addresses
541                .into_iter()
542                .filter_map(|address| Some((address, f(&address)?)))
543                .collect()
544        }
545
546        // # Performance
547        //
548        // It's better to update entries in RocksDB with insertions over merge operations when there is no risk that
549        // insertions may overwrite values that are updated concurrently in database format upgrades as inserted values
550        // are quicker to read and require less background compaction.
551        //
552        // Reading entries that have been updated with merge ops often requires reading the latest fully-merged value,
553        // reading all of the pending merge operands (potentially hundreds), and applying pending merge operands to the
554        // fully-merged value such that it's much faster to read entries that have been updated with insertions than it
555        // is to read entries that have been updated with merge operations.
556        let address_balances: AddressBalanceLocationUpdates = if self.finished_format_upgrades() {
557            AddressBalanceLocationUpdates::Insert(read_addr_locs(changed_addresses, |addr| {
558                self.address_balance_location(addr)
559            }))
560        } else {
561            AddressBalanceLocationUpdates::Merge(read_addr_locs(changed_addresses, |addr| {
562                Some(self.address_balance_location(addr)?.into_new_change())
563            }))
564        };
565
566        let mut batch = DiskWriteBatch::new();
567
568        // In case of errors, propagate and do not write the batch.
569        batch.prepare_block_batch(
570            self,
571            network,
572            &finalized,
573            new_outputs_by_out_loc,
574            spent_utxos_by_outpoint,
575            spent_utxos_by_out_loc,
576            #[cfg(feature = "indexer")]
577            out_loc_by_outpoint,
578            address_balances,
579            self.finalized_value_pool(),
580            prev_note_commitment_trees,
581        )?;
582
583        // Track batch commit latency for observability
584        let batch_start = std::time::Instant::now();
585        if let Err(error) = self.db.write(batch) {
586            panic!("unexpected rocksdb error while writing block: {error}");
587        }
588        metrics::histogram!("zebra.state.rocksdb.batch_commit.duration_seconds")
589            .record(batch_start.elapsed().as_secs_f64());
590
591        tracing::trace!(?source, "committed block from");
592
593        Ok(finalized.hash)
594    }
595
596    /// Writes the given batch to the database.
597    pub fn write_batch(&self, batch: DiskWriteBatch) -> Result<(), rocksdb::Error> {
598        self.db.write(batch)
599    }
600}
601
602/// Lookup the output location for an outpoint.
603///
604/// `tx_hash_indexes` must contain `outpoint.hash` and that transaction's index in its block.
605fn lookup_out_loc(
606    height: Height,
607    outpoint: &transparent::OutPoint,
608    tx_hash_indexes: &HashMap<transaction::Hash, usize>,
609) -> OutputLocation {
610    let tx_index = tx_hash_indexes
611        .get(&outpoint.hash)
612        .expect("already checked UTXO was in state or block");
613
614    let tx_loc = TransactionLocation::from_usize(height, *tx_index);
615
616    OutputLocation::from_outpoint(tx_loc, outpoint)
617}
618
619impl DiskWriteBatch {
620    // Write block methods
621
622    /// Prepare a database batch containing `finalized.block`,
623    /// and return it (without actually writing anything).
624    ///
625    /// If this method returns an error, it will be propagated,
626    /// and the batch should not be written to the database.
627    ///
628    /// # Errors
629    ///
630    /// - Propagates any errors from computing the block's chain value balance change or
631    ///   from applying the change to the chain value balance
632    #[allow(clippy::too_many_arguments)]
633    pub fn prepare_block_batch(
634        &mut self,
635        zebra_db: &ZebraDb,
636        network: &Network,
637        finalized: &FinalizedBlock,
638        new_outputs_by_out_loc: BTreeMap<OutputLocation, transparent::Utxo>,
639        spent_utxos_by_outpoint: HashMap<transparent::OutPoint, transparent::Utxo>,
640        spent_utxos_by_out_loc: BTreeMap<OutputLocation, transparent::Utxo>,
641        #[cfg(feature = "indexer")] out_loc_by_outpoint: HashMap<
642            transparent::OutPoint,
643            OutputLocation,
644        >,
645        address_balances: AddressBalanceLocationUpdates,
646        value_pool: ValueBalance<NonNegative>,
647        prev_note_commitment_trees: Option<NoteCommitmentTrees>,
648    ) -> Result<(), CommitCheckpointVerifiedError> {
649        let db = &zebra_db.db;
650
651        // Commit block, transaction, and note commitment tree data.
652        self.prepare_block_header_and_transaction_data_batch(db, finalized);
653
654        // The consensus rules are silent on shielded transactions in the genesis block,
655        // because there aren't any in the mainnet or testnet genesis blocks.
656        // So this means the genesis anchor is the same as the empty anchor,
657        // which is already present from height 1 to the first shielded transaction.
658        //
659        // In Zebra we include the nullifiers and note commitments in the genesis block because it simplifies our code.
660        self.prepare_shielded_transaction_batch(zebra_db, finalized);
661        self.prepare_trees_batch(zebra_db, finalized, prev_note_commitment_trees);
662
663        // # Consensus
664        //
665        // > A transaction MUST NOT spend an output of the genesis block coinbase transaction.
666        // > (There is one such zero-valued output, on each of Testnet and Mainnet.)
667        //
668        // https://zips.z.cash/protocol/protocol.pdf#txnconsensus
669        //
670        // So we ignore the genesis UTXO, transparent address index, and value pool updates
671        // for the genesis block. This also ignores genesis shielded value pool updates, but there
672        // aren't any of those on mainnet or testnet.
673        if !finalized.height.is_min() {
674            // Commit transaction indexes
675            self.prepare_transparent_transaction_batch(
676                zebra_db,
677                network,
678                finalized,
679                &new_outputs_by_out_loc,
680                &spent_utxos_by_outpoint,
681                &spent_utxos_by_out_loc,
682                #[cfg(feature = "indexer")]
683                &out_loc_by_outpoint,
684                address_balances,
685            );
686        }
687
688        // Commit UTXOs and value pools
689        self.prepare_chain_value_pools_batch(
690            zebra_db,
691            finalized,
692            spent_utxos_by_outpoint,
693            value_pool,
694        )?;
695
696        // The block has passed contextual validation, so update the metrics
697        block_precommit_metrics(&finalized.block, finalized.hash, finalized.height);
698
699        Ok(())
700    }
701
702    /// Prepare a database batch containing the block header and transaction data
703    /// from `finalized.block`, and return it (without actually writing anything).
704    #[allow(clippy::unwrap_in_result)]
705    pub fn prepare_block_header_and_transaction_data_batch(
706        &mut self,
707        db: &DiskDb,
708        finalized: &FinalizedBlock,
709    ) {
710        // Blocks
711        let block_header_by_height = db.cf_handle("block_header_by_height").unwrap();
712        let hash_by_height = db.cf_handle("hash_by_height").unwrap();
713        let height_by_hash = db.cf_handle("height_by_hash").unwrap();
714
715        // Transactions
716        let tx_by_loc = db.cf_handle("tx_by_loc").unwrap();
717        let hash_by_tx_loc = db.cf_handle("hash_by_tx_loc").unwrap();
718        let tx_loc_by_hash = db.cf_handle("tx_loc_by_hash").unwrap();
719
720        let FinalizedBlock {
721            block,
722            hash,
723            height,
724            transaction_hashes,
725            ..
726        } = finalized;
727
728        // Commit block header data
729        self.zs_insert(&block_header_by_height, height, &block.header);
730
731        // Index the block hash and height
732        self.zs_insert(&hash_by_height, height, hash);
733        self.zs_insert(&height_by_hash, hash, height);
734
735        for (transaction_index, (transaction, transaction_hash)) in block
736            .transactions
737            .iter()
738            .zip(transaction_hashes.iter())
739            .enumerate()
740        {
741            let transaction_location = TransactionLocation::from_usize(*height, transaction_index);
742
743            // Commit each transaction's data
744            self.zs_insert(&tx_by_loc, transaction_location, transaction);
745
746            // Index each transaction hash and location
747            self.zs_insert(&hash_by_tx_loc, transaction_location, transaction_hash);
748            self.zs_insert(&tx_loc_by_hash, transaction_hash, transaction_location);
749        }
750    }
751}