1use 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
74pub const STATE_COLUMN_FAMILIES_IN_CODE: &[&str] = &[
78 "hash_by_height",
80 "height_by_hash",
81 "block_header_by_height",
82 "tx_by_loc",
84 "hash_by_tx_loc",
85 "tx_loc_by_hash",
86 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_nullifiers",
94 "sprout_anchors",
95 "sprout_note_commitment_tree",
96 "sapling_nullifiers",
98 "sapling_anchors",
99 "sapling_note_commitment_tree",
100 "sapling_note_commitment_subtree",
101 "orchard_nullifiers",
103 "orchard_anchors",
104 "orchard_note_commitment_tree",
105 "orchard_note_commitment_subtree",
106 "ironwood_nullifiers",
109 "ironwood_anchors",
110 "ironwood_note_commitment_tree",
111 "ironwood_note_commitment_subtree",
112 "history_tree",
114 "tip_chain_value_pool",
115 BLOCK_INFO,
116];
117
118#[derive(Clone, Debug)]
127pub struct FinalizedState {
128 debug_stop_at_height: Option<block::Height>,
137
138 pub db: ZebraDb,
148
149 #[cfg(feature = "elasticsearch")]
150 pub elastic_db: Option<elasticsearch::Elasticsearch>,
152
153 #[cfg(feature = "elasticsearch")]
154 pub elastic_blocks: Vec<String>,
156}
157
158impl FinalizedState {
159 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 #[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 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 std::mem::drop(new_state);
269
270 Self::exit_process();
276 }
277 }
278
279 Ok(new_state)
280 }
281
282 pub fn network(&self) -> Network {
284 self.db.network()
285 }
286
287 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 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 #[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 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 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::block_commitment_is_valid_for_chain_history(
384 block.clone(),
385 &self.network(),
386 &history_tree,
387 )?;
388
389 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 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 #[cfg(feature = "elasticsearch")]
508 self.elasticsearch(&finalized_inner_block);
509
510 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 self.db.shutdown(true);
521
522 Self::exit_process();
528 }
529 }
530
531 result.map(|hash| (hash, note_commitment_trees))
532 }
533
534 #[cfg(feature = "elasticsearch")]
535 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 const AWAY_FROM_TIP_BULK_SIZE: usize = 48;
549
550 const CLOSE_TO_TIP_BULK_SIZE: usize = 2;
554
555 const CLOSE_TO_TIP_SECONDS: i64 = 14400; let mut blocks_size_to_dump = AWAY_FROM_TIP_BULK_SIZE;
560
561 if local_time - block_time < CLOSE_TO_TIP_SECONDS {
563 blocks_size_to_dump = CLOSE_TO_TIP_BULK_SIZE;
564 }
565
566 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 self.elastic_blocks
579 .push(serde_json::json!(block).to_string());
580
581 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 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 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 self.elastic_blocks.clear();
615 }
616 }
617 }
618
619 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 fn exit_process() -> ! {
636 tracing::info!("exiting Zebra");
637
638 let _ = stdout().lock().flush();
643 let _ = stderr().lock().flush();
644
645 std::thread::sleep(std::time::Duration::from_secs(3));
648
649 std::process::exit(0);
653 }
654}
655
656pub(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}