1use 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[allow(clippy::unwrap_in_result)]
128 pub fn block_header(&self, hash_or_height: HashOrHeight) -> Option<Arc<block::Header>> {
129 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 #[allow(clippy::unwrap_in_result)]
141 fn raw_block_header(&self, hash_or_height: HashOrHeight) -> Option<RawBytes> {
142 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 #[allow(clippy::unwrap_in_result)]
156 pub fn block(&self, hash_or_height: HashOrHeight) -> Option<Arc<Block>> {
157 let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
159 let header = self.block_header(height.into())?;
160
161 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 #[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 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 #[allow(clippy::unwrap_in_result)]
218 fn raw_block(&self, hash_or_height: HashOrHeight) -> Option<(RawBytes, Vec<RawBytes>)> {
219 let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
221 let header = self.raw_block_header(height.into())?;
222
223 let transactions = self
226 .raw_transactions_by_height(height)
227 .map(|(_, tx)| tx)
228 .collect();
229
230 Some((header, transactions))
231 }
232
233 #[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 #[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 #[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 pub fn finalized_tip_hash(&self) -> block::Hash {
273 self.tip()
274 .map(|(_, hash)| hash)
275 .unwrap_or(GENESIS_PREVIOUS_BLOCK_HASH)
277 }
278
279 pub fn finalized_tip_height(&self) -> Option<block::Height> {
281 self.tip().map(|(height, _)| height)
282 }
283
284 pub fn tip_block(&self) -> Option<Arc<Block>> {
286 let (height, _hash) = self.tip()?;
287 self.block(height.into())
288 }
289
290 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 let height = hash_or_height.height_or_else(|hash| self.height(hash))?;
410
411 let hash_by_tx_loc = self.db.cf_handle("hash_by_tx_loc").unwrap();
413
414 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 #[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 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 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 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 #[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 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 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 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 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 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 pub fn write_batch(&self, batch: DiskWriteBatch) -> Result<(), rocksdb::Error> {
598 self.db.write(batch)
599 }
600}
601
602fn 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 #[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 self.prepare_block_header_and_transaction_data_batch(db, finalized);
653
654 self.prepare_shielded_transaction_batch(zebra_db, finalized);
661 self.prepare_trees_batch(zebra_db, finalized, prev_note_commitment_trees);
662
663 if !finalized.height.is_min() {
674 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 self.prepare_chain_value_pools_batch(
690 zebra_db,
691 finalized,
692 spent_utxos_by_outpoint,
693 value_pool,
694 )?;
695
696 block_precommit_metrics(&finalized.block, finalized.hash, finalized.height);
698
699 Ok(())
700 }
701
702 #[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 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 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 self.zs_insert(&block_header_by_height, height, &block.header);
730
731 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 self.zs_insert(&tx_by_loc, transaction_location, transaction);
745
746 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}