From 678c5190320c7ced2f59024828524a0abd22ebef Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Wed, 7 Dec 2022 19:39:11 -0300 Subject: [PATCH] change(rpc): Populate `blockcommitmenthash` and `defaultroot` fields in the getblocktemplate RPC (#5751) * populate `blockcommitmenthash` and `defaultroot` missing fields * remove assertion line manually from snaps * fix some imports and docs * fix some docs * add a consistency check * Rename a constant to FINALIZED_STATE_QUERY_RETRIES and use it everywhere * Move tip query inside retry, split tip into tip_height and tip_hash * Return retry failures rather than panicking * Query relevant chain inside the retry * Check the entire state for consistency, not just the finalized tip Co-authored-by: teor --- .../src/methods/get_block_template_rpcs.rs | 36 +++-- zebra-rpc/src/methods/tests.rs | 2 + .../tests/snapshot/get_block_template_rpcs.rs | 9 +- .../get_block_template@mainnet_10.snap | 10 +- .../get_block_template@testnet_10.snap | 10 +- zebra-rpc/src/methods/tests/utils.rs | 39 +++++ zebra-rpc/src/methods/tests/vectors.rs | 15 +- zebra-state/src/request.rs | 17 +- zebra-state/src/response.rs | 15 +- zebra-state/src/service.rs | 15 +- zebra-state/src/service/read.rs | 9 +- zebra-state/src/service/read/address.rs | 7 - .../src/service/read/address/balance.rs | 10 +- zebra-state/src/service/read/address/tx_id.rs | 10 +- zebra-state/src/service/read/address/utxo.rs | 10 +- zebra-state/src/service/read/difficulty.rs | 148 +++++++++++++----- zebra-state/src/service/read/tree.rs | 15 ++ 17 files changed, 267 insertions(+), 110 deletions(-) create mode 100644 zebra-rpc/src/methods/tests/utils.rs diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index ce1ce99c..c6c767d3 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -9,11 +9,10 @@ use tower::{buffer::Buffer, Service, ServiceExt}; use zebra_chain::{ amount::{self, Amount, NonNegative}, - block::Height, block::{ self, merkle::{self, AuthDataRoot}, - Block, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION, + Block, ChainHistoryBlockTxAuthCommitmentHash, Height, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION, }, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, @@ -338,7 +337,7 @@ where // The tip estimate may not be the same as the one coming from the state // but this is ok for an estimate - let (estimated_distance_to_chain_tip, tip_height) = latest_chain_tip + let (estimated_distance_to_chain_tip, estimated_tip_height) = latest_chain_tip .estimate_distance_to_network_chain_tip(network) .ok_or_else(|| Error { code: ErrorCode::ServerError(0), @@ -349,7 +348,7 @@ where if !sync_status.is_close_to_tip() || estimated_distance_to_chain_tip > MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP { tracing::info!( estimated_distance_to_chain_tip, - ?tip_height, + ?estimated_tip_height, "Zebra has not synced to the chain tip" ); @@ -377,15 +376,12 @@ where })?; let chain_info = match response { - ReadResponse::ChainInfo(Some(chain_info)) => chain_info, + ReadResponse::ChainInfo(chain_info) => chain_info, _ => unreachable!("we should always have enough state data here to get a `GetBlockTemplateChainInfo`"), }; // Get the tip data from the state call - let tip_height = chain_info.tip.0; - let tip_hash = chain_info.tip.1; - - let block_height = (tip_height + 1).expect("tip is far below Height::MAX"); + let block_height = (chain_info.tip_height + 1).expect("tip is far below Height::MAX"); let outputs = standard_coinbase_outputs(network, block_height, miner_address, miner_fee); @@ -394,6 +390,16 @@ where let (merkle_root, auth_data_root) = calculate_transaction_roots(&coinbase_tx, &mempool_txs); + let history_tree = chain_info.history_tree; + // TODO: move expensive cryptography to a rayon thread? + let chain_history_root = history_tree.hash().expect("history tree can't be empty"); + + // TODO: move expensive cryptography to a rayon thread? + let block_commitments_hash = ChainHistoryBlockTxAuthCommitmentHash::from_commitments( + &chain_history_root, + &auth_data_root, + ); + // Convert into TransactionTemplates let mempool_txs = mempool_txs.iter().map(Into::into).collect(); @@ -404,15 +410,15 @@ where version: ZCASH_BLOCK_VERSION, - previous_block_hash: GetBlockHash(tip_hash), - block_commitments_hash: [0; 32].into(), - light_client_root_hash: [0; 32].into(), - final_sapling_root_hash: [0; 32].into(), + previous_block_hash: GetBlockHash(chain_info.tip_hash), + block_commitments_hash, + light_client_root_hash: block_commitments_hash, + final_sapling_root_hash: block_commitments_hash, default_roots: DefaultRoots { merkle_root, - chain_history_root: [0; 32].into(), + chain_history_root, auth_data_root, - block_commitments_hash: [0; 32].into(), + block_commitments_hash, }, transactions: mempool_txs, diff --git a/zebra-rpc/src/methods/tests.rs b/zebra-rpc/src/methods/tests.rs index 64df2ae9..f98d41d6 100644 --- a/zebra-rpc/src/methods/tests.rs +++ b/zebra-rpc/src/methods/tests.rs @@ -2,4 +2,6 @@ mod prop; mod snapshot; +#[cfg(feature = "getblocktemplate-rpcs")] +pub mod utils; mod vectors; diff --git a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs index aae7c363..760c54d2 100644 --- a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs @@ -31,6 +31,7 @@ use crate::methods::{ self, types::{get_block_template::GetBlockTemplate, hex_data::HexData, submit_block}, }, + tests::utils::fake_history_tree, GetBlockHash, GetBlockTemplateRpc, GetBlockTemplateRpcImpl, }; @@ -154,13 +155,15 @@ pub async fn test_responses( .clone() .expect_request_that(|req| matches!(req, ReadRequest::ChainInfo)) .await - .respond(ReadResponse::ChainInfo(Some(GetBlockTemplateChainInfo { + .respond(ReadResponse::ChainInfo(GetBlockTemplateChainInfo { expected_difficulty: CompactDifficulty::from(ExpandedDifficulty::from(U256::one())), - tip: (fake_tip_height, fake_tip_hash), + tip_height: fake_tip_height, + tip_hash: fake_tip_hash, cur_time: fake_cur_time, min_time: fake_min_time, max_time: fake_max_time, - }))); + history_tree: fake_history_tree(network), + })); }); let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template(None)); diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@mainnet_10.snap index b1ba9fa2..1984ee99 100644 --- a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@mainnet_10.snap @@ -6,14 +6,14 @@ expression: block_template "capabilities": [], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", - "blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000", - "lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000", - "finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000", + "blockcommitmentshash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82", + "lightclientroothash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82", + "finalsaplingroothash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82", "defaultroots": { "merkleroot": "6b370584714ab567c9c014ce72d325ab6c5927e181ac891acb35e6d4b6cc19a1", - "chainhistoryroot": "0000000000000000000000000000000000000000000000000000000000000000", + "chainhistoryroot": "94470fa66ebd1a5fdb109a5aa3f3204f14de3a42135e71aa7f4c44055847e0b5", "authdataroot": "0dbb78de9fdcd494307971e36dd049fc82d0ee9ee53aec8fd2a54dc0e426289b", - "blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000" + "blockcommitmentshash": "fe03d8236b0835c758f59d279230ebaee2128754413103b9edb17c07451c2c82" }, "transactions": [], "coinbasetxn": { diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@testnet_10.snap index cdc0d129..34cd7eb2 100644 --- a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_block_template@testnet_10.snap @@ -6,14 +6,14 @@ expression: block_template "capabilities": [], "version": 4, "previousblockhash": "0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8", - "blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000", - "lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000", - "finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000", + "blockcommitmentshash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422", + "lightclientroothash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422", + "finalsaplingroothash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422", "defaultroots": { "merkleroot": "623400cc122baa015d3a4209f5903ebe215170c7e6e74831dce8372c5fd5b3cc", - "chainhistoryroot": "0000000000000000000000000000000000000000000000000000000000000000", + "chainhistoryroot": "03bc75f00c307a05aed2023819e18c2672cbe15fbd3200944997def141967387", "authdataroot": "a44375f0c0dd5ba612bd7b0efd77683cde8edf5055aff9fbfda443cc8d46bd3e", - "blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000" + "blockcommitmentshash": "cb1f1c6a5ad5ff9c4a170e3b747a24f3aec79817adba9a9451f19914481bb422" }, "transactions": [], "coinbasetxn": { diff --git a/zebra-rpc/src/methods/tests/utils.rs b/zebra-rpc/src/methods/tests/utils.rs new file mode 100644 index 00000000..797d712a --- /dev/null +++ b/zebra-rpc/src/methods/tests/utils.rs @@ -0,0 +1,39 @@ +//! Utility functions for RPC method tests. + +use std::sync::Arc; +use zebra_chain::{ + block::Block, + history_tree::{HistoryTree, NonEmptyHistoryTree}, + parameters::Network, + sapling::tree::Root, + serialization::ZcashDeserialize, +}; + +use zebra_test::vectors; + +/// Create a history tree with one single block for a network by using Zebra test vectors. +pub fn fake_history_tree(network: Network) -> Arc { + let (block, sapling_root) = match network { + Network::Mainnet => ( + &vectors::BLOCK_MAINNET_1046400_BYTES[..], + *vectors::SAPLING_FINAL_ROOT_MAINNET_1046400_BYTES, + ), + Network::Testnet => ( + &vectors::BLOCK_TESTNET_1116000_BYTES[..], + *vectors::SAPLING_FINAL_ROOT_TESTNET_1116000_BYTES, + ), + }; + + let block = Arc::::zcash_deserialize(block).expect("block should deserialize"); + let first_sapling_root = Root::try_from(sapling_root).unwrap(); + + let history_tree = NonEmptyHistoryTree::from_block( + Network::Mainnet, + block, + &first_sapling_root, + &Default::default(), + ) + .unwrap(); + + Arc::new(HistoryTree::from(history_tree)) +} diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index a7b976cd..406fe99e 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -795,8 +795,11 @@ async fn rpc_getblocktemplate() { use chrono::{TimeZone, Utc}; - use crate::methods::get_block_template_rpcs::constants::{ - GET_BLOCK_TEMPLATE_MUTABLE_FIELD, GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD, + use crate::methods::{ + get_block_template_rpcs::constants::{ + GET_BLOCK_TEMPLATE_MUTABLE_FIELD, GET_BLOCK_TEMPLATE_NONCE_RANGE_FIELD, + }, + tests::utils::fake_history_tree, }; use zebra_chain::{ amount::{Amount, NonNegative}, @@ -856,13 +859,15 @@ async fn rpc_getblocktemplate() { .clone() .expect_request_that(|req| matches!(req, ReadRequest::ChainInfo)) .await - .respond(ReadResponse::ChainInfo(Some(GetBlockTemplateChainInfo { + .respond(ReadResponse::ChainInfo(GetBlockTemplateChainInfo { expected_difficulty: CompactDifficulty::from(ExpandedDifficulty::from(U256::one())), - tip: (fake_tip_height, fake_tip_hash), + tip_height: fake_tip_height, + tip_hash: fake_tip_hash, cur_time: fake_cur_time, min_time: fake_min_time, max_time: fake_max_time, - }))); + history_tree: fake_history_tree(Mainnet), + })); }); let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template(None)); diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 44715164..289b8d46 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -61,8 +61,21 @@ impl From for HashOrHeight { } impl From for HashOrHeight { - fn from(hash: block::Height) -> Self { - Self::Height(hash) + fn from(height: block::Height) -> Self { + Self::Height(height) + } +} + +impl From<(block::Height, block::Hash)> for HashOrHeight { + fn from((_height, hash): (block::Height, block::Hash)) -> Self { + // Hash is more specific than height for the non-finalized state + hash.into() + } +} + +impl From<(block::Hash, block::Height)> for HashOrHeight { + fn from((hash, _height): (block::Hash, block::Height)) -> Self { + hash.into() } } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 40d88977..c1e0802e 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -132,16 +132,20 @@ pub enum ReadResponse { #[cfg(feature = "getblocktemplate-rpcs")] /// Response to [`ReadRequest::ChainInfo`](crate::ReadRequest::ChainInfo) with the state /// information needed by the `getblocktemplate` RPC method. - ChainInfo(Option), + ChainInfo(GetBlockTemplateChainInfo), } #[cfg(feature = "getblocktemplate-rpcs")] /// A structure with the information needed from the state to build a `getblocktemplate` RPC response. #[derive(Clone, Debug, Eq, PartialEq)] pub struct GetBlockTemplateChainInfo { - /// The current state tip height and hash. - /// The block template for the candidate block is the next block after this block. - pub tip: (block::Height, block::Hash), + /// The current state tip height. + /// The block template OAfor the candidate block is the next block after this block. + pub tip_height: block::Height, + + /// The current state tip height. + /// The block template for the candidate block has this hash as the previous block hash. + pub tip_hash: block::Hash, /// The expected difficulty of the candidate block. pub expected_difficulty: CompactDifficulty, @@ -154,6 +158,9 @@ pub struct GetBlockTemplateChainInfo { /// The maximum time the miner can use in this block. pub max_time: chrono::DateTime, + + /// The history tree of the current best chain. + pub history_tree: Arc, } /// Conversion from read-only [`ReadResponse`]s to read-write [`Response`]s. diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 8b9b9087..239dbc6b 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1607,21 +1607,16 @@ impl Service for ReadStateService { // In that case, the `getblocktemplate` RPC will return an error because Zebra // is not synced to the tip. That check happens before the RPC makes this request. let get_block_template_info = - read::tip(latest_non_finalized_state.best_chain(), &state.db).map( - |tip| { - read::difficulty::difficulty_and_time_info( - &latest_non_finalized_state, - &state.db, - tip, - state.network, - ) - }, + read::difficulty::get_block_template_chain_info( + &latest_non_finalized_state, + &state.db, + state.network, ); // The work is done in the future. timer.finish(module_path!(), line!(), "ReadRequest::ChainInfo"); - Ok(ReadResponse::ChainInfo(get_block_template_info)) + get_block_template_info.map(ReadResponse::ChainInfo) }) }) .map(|join_result| join_result.expect("panic in ReadRequest::ChainInfo")) diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index 2507daa7..f14c8bdb 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -35,10 +35,17 @@ pub use block::{ }; #[cfg(feature = "getblocktemplate-rpcs")] -pub use {block::hash, difficulty::difficulty_and_time_info}; +pub use {block::hash, difficulty::get_block_template_chain_info}; pub use find::{ best_tip, block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers, hash_by_height, height_by_hash, tip, tip_height, }; pub use tree::{orchard_tree, sapling_tree}; + +/// If a finalized state query is interrupted by a new finalized block, +/// retry this many times. +/// +/// Once we're at the tip, we expect up to 2 blocks to arrive at the same time. +/// If any more arrive, the client should wait until we're synchronised with our peers. +pub const FINALIZED_STATE_QUERY_RETRIES: usize = 3; diff --git a/zebra-state/src/service/read/address.rs b/zebra-state/src/service/read/address.rs index 9e0f528e..873d22a2 100644 --- a/zebra-state/src/service/read/address.rs +++ b/zebra-state/src/service/read/address.rs @@ -3,10 +3,3 @@ pub mod balance; pub mod tx_id; pub mod utxo; - -/// If the transparent address index queries are interrupted by a new finalized block, -/// retry this many times. -/// -/// Once we're at the tip, we expect up to 2 blocks to arrive at the same time. -/// If any more arrive, the client should wait until we're synchronised with our peers. -const FINALIZED_ADDRESS_INDEX_RETRIES: usize = 3; diff --git a/zebra-state/src/service/read/address/balance.rs b/zebra-state/src/service/read/address/balance.rs index cff2cf22..14ec49fc 100644 --- a/zebra-state/src/service/read/address/balance.rs +++ b/zebra-state/src/service/read/address/balance.rs @@ -20,12 +20,12 @@ use zebra_chain::{ }; use crate::{ - service::{finalized_state::ZebraDb, non_finalized_state::Chain}, + service::{ + finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES, + }, BoxError, }; -use super::FINALIZED_ADDRESS_INDEX_RETRIES; - /// Returns the total transparent balance for the supplied [`transparent::Address`]es. /// /// If the addresses do not exist in the non-finalized `chain` or finalized `db`, returns zero. @@ -37,7 +37,9 @@ pub fn transparent_balance( let mut balance_result = finalized_transparent_balance(db, &addresses); // Retry the finalized balance query if it was interrupted by a finalizing block - for _ in 0..FINALIZED_ADDRESS_INDEX_RETRIES { + // + // TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn + for _ in 0..FINALIZED_STATE_QUERY_RETRIES { if balance_result.is_ok() { break; } diff --git a/zebra-state/src/service/read/address/tx_id.rs b/zebra-state/src/service/read/address/tx_id.rs index f35bb391..560f7b10 100644 --- a/zebra-state/src/service/read/address/tx_id.rs +++ b/zebra-state/src/service/read/address/tx_id.rs @@ -19,12 +19,12 @@ use std::{ use zebra_chain::{block::Height, transaction, transparent}; use crate::{ - service::{finalized_state::ZebraDb, non_finalized_state::Chain}, + service::{ + finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES, + }, BoxError, TransactionLocation, }; -use super::FINALIZED_ADDRESS_INDEX_RETRIES; - /// Returns the transaction IDs that sent or received funds from the supplied [`transparent::Address`]es, /// within `query_height_range`, in chain order. /// @@ -44,7 +44,9 @@ where // Retry the finalized tx ID query if it was interrupted by a finalizing block, // and the non-finalized chain doesn't overlap the changed heights. - for _ in 0..=FINALIZED_ADDRESS_INDEX_RETRIES { + // + // TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn + for _ in 0..=FINALIZED_STATE_QUERY_RETRIES { let (finalized_tx_ids, finalized_tip_range) = finalized_transparent_tx_ids(db, &addresses, query_height_range.clone()); diff --git a/zebra-state/src/service/read/address/utxo.rs b/zebra-state/src/service/read/address/utxo.rs index 2e12213d..7ee5cb4f 100644 --- a/zebra-state/src/service/read/address/utxo.rs +++ b/zebra-state/src/service/read/address/utxo.rs @@ -19,12 +19,12 @@ use std::{ use zebra_chain::{block::Height, parameters::Network, transaction, transparent}; use crate::{ - service::{finalized_state::ZebraDb, non_finalized_state::Chain}, + service::{ + finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES, + }, BoxError, OutputLocation, TransactionLocation, }; -use super::FINALIZED_ADDRESS_INDEX_RETRIES; - /// The full range of address heights. /// /// The genesis coinbase transactions are ignored by a consensus rule, @@ -108,7 +108,9 @@ where // Retry the finalized UTXO query if it was interrupted by a finalizing block, // and the non-finalized chain doesn't overlap the changed heights. - for attempt in 0..=FINALIZED_ADDRESS_INDEX_RETRIES { + // + // TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn + for attempt in 0..=FINALIZED_STATE_QUERY_RETRIES { debug!(?attempt, ?address_count, "starting address UTXO query"); let (finalized_utxos, finalized_tip_range) = finalized_address_utxos(db, &addresses); diff --git a/zebra-state/src/service/read/difficulty.rs b/zebra-state/src/service/read/difficulty.rs index 2d723775..a18f3717 100644 --- a/zebra-state/src/service/read/difficulty.rs +++ b/zebra-state/src/service/read/difficulty.rs @@ -1,11 +1,12 @@ //! Get context and calculate difficulty for the next block. -use std::borrow::Borrow; +use std::sync::Arc; use chrono::{DateTime, Duration, TimeZone, Utc}; use zebra_chain::{ - block::{Block, Hash, Height}, + block::{self, Block, Height}, + history_tree::HistoryTree, parameters::{Network, NetworkUpgrade, POST_BLOSSOM_POW_TARGET_SPACING}, work::difficulty::CompactDifficulty, }; @@ -18,70 +19,133 @@ use crate::{ AdjustedDifficulty, }, finalized_state::ZebraDb, + read::{self, tree::history_tree, FINALIZED_STATE_QUERY_RETRIES}, NonFinalizedState, }, - GetBlockTemplateChainInfo, + BoxError, GetBlockTemplateChainInfo, }; /// Returns the [`GetBlockTemplateChainInfo`] for the current best chain. /// /// # Panics /// -/// If we don't have enough blocks in the state. -pub fn difficulty_and_time_info( +/// - If we don't have enough blocks in the state. +/// - If a consistency check fails `RETRIES` times. +pub fn get_block_template_chain_info( non_finalized_state: &NonFinalizedState, db: &ZebraDb, - tip: (Height, Hash), network: Network, -) -> GetBlockTemplateChainInfo { - let relevant_chain = any_ancestor_blocks(non_finalized_state, db, tip.1); - difficulty_and_time(relevant_chain, tip, network) +) -> Result { + let mut relevant_chain_and_history_tree_result = + relevant_chain_and_history_tree(non_finalized_state, db); + + // Retry the finalized state query if it was interrupted by a finalizing block. + // + // TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn + for _ in 0..FINALIZED_STATE_QUERY_RETRIES { + if relevant_chain_and_history_tree_result.is_ok() { + break; + } + + relevant_chain_and_history_tree_result = + relevant_chain_and_history_tree(non_finalized_state, db); + } + + let (tip_height, tip_hash, relevant_chain, history_tree) = + relevant_chain_and_history_tree_result?; + + Ok(difficulty_time_and_history_tree( + relevant_chain, + tip_height, + tip_hash, + network, + history_tree, + )) } -/// Returns the [`GetBlockTemplateChainInfo`] for the current best chain. +/// Do a consistency check by checking the finalized tip before and after all other database queries. +/// Returns and error if the tip obtained before and after is not the same. /// -/// See [`difficulty_and_time_info()`] for details. -fn difficulty_and_time( - relevant_chain: C, - tip: (Height, Hash), - network: Network, -) -> GetBlockTemplateChainInfo -where - C: IntoIterator, - C::Item: Borrow, - C::IntoIter: ExactSizeIterator, -{ +/// # Panics +/// +/// - If we don't have enough blocks in the state. +fn relevant_chain_and_history_tree( + non_finalized_state: &NonFinalizedState, + db: &ZebraDb, +) -> Result< + ( + Height, + block::Hash, + [Arc; POW_ADJUSTMENT_BLOCK_SPAN], + Arc, + ), + BoxError, +> { + let state_tip_before_queries = read::best_tip(non_finalized_state, db).ok_or_else(|| { + BoxError::from("Zebra's state is empty, wait until it syncs to the chain tip") + })?; + + let relevant_chain = any_ancestor_blocks(non_finalized_state, db, state_tip_before_queries.1); let relevant_chain: Vec<_> = relevant_chain .into_iter() .take(POW_ADJUSTMENT_BLOCK_SPAN) .collect(); + let relevant_chain = relevant_chain.try_into().map_err(|_error| { + "Zebra's state only has a few blocks, wait until it syncs to the chain tip" + })?; + let history_tree = history_tree( + non_finalized_state.best_chain(), + db, + state_tip_before_queries.into(), + ) + .expect("tip hash should exist in the chain"); + + let state_tip_after_queries = + read::best_tip(non_finalized_state, db).expect("already checked for an empty tip"); + + if state_tip_before_queries != state_tip_after_queries { + return Err("Zebra is committing too many blocks to the state, \ + wait until it syncs to the chain tip" + .into()); + } + + Ok(( + state_tip_before_queries.0, + state_tip_before_queries.1, + relevant_chain, + history_tree, + )) +} + +/// Returns the [`GetBlockTemplateChainInfo`] for the current best chain. +/// +/// See [`get_block_template_chain_info()`] for details. +fn difficulty_time_and_history_tree( + relevant_chain: [Arc; POW_ADJUSTMENT_BLOCK_SPAN], + tip_height: Height, + tip_hash: block::Hash, + network: Network, + history_tree: Arc, +) -> GetBlockTemplateChainInfo { let relevant_data: Vec<(CompactDifficulty, DateTime)> = relevant_chain .iter() - .map(|block| { - ( - block.borrow().header.difficulty_threshold, - block.borrow().header.time, - ) - }) + .map(|block| (block.header.difficulty_threshold, block.header.time)) .collect(); - // The getblocktemplate RPC returns an error if Zebra is not synced to the tip. - // So this will never happen in production code. - assert_eq!( - relevant_data.len(), - POW_ADJUSTMENT_BLOCK_SPAN, - "getblocktemplate RPC called with a near-empty state: should have returned an error", - ); - let cur_time = chrono::Utc::now(); // Get the median-time-past, which doesn't depend on the time or the previous block height. + // `context` will always have the correct length, because this function takes an array. // // TODO: split out median-time-past into its own struct? - let median_time_past = - AdjustedDifficulty::new_from_header_time(cur_time, tip.0, network, relevant_data.clone()) - .median_time_past(); + let median_time_past = AdjustedDifficulty::new_from_header_time( + cur_time, + tip_height, + network, + relevant_data.clone(), + ) + .median_time_past(); // > For each block other than the genesis block , nTime MUST be strictly greater than // > the median-time-past of that block. @@ -110,20 +174,22 @@ where // Now that we have a valid time, get the difficulty for that time. let difficulty_adjustment = AdjustedDifficulty::new_from_header_time( cur_time, - tip.0, + tip_height, network, relevant_data.iter().cloned(), ); let mut result = GetBlockTemplateChainInfo { - tip, + tip_height, + tip_hash, expected_difficulty: difficulty_adjustment.expected_difficulty_threshold(), min_time, cur_time, max_time, + history_tree, }; - adjust_difficulty_and_time_for_testnet(&mut result, network, tip.0, relevant_data); + adjust_difficulty_and_time_for_testnet(&mut result, network, tip_height, relevant_data); result } diff --git a/zebra-state/src/service/read/tree.rs b/zebra-state/src/service/read/tree.rs index 09897a93..704637d3 100644 --- a/zebra-state/src/service/read/tree.rs +++ b/zebra-state/src/service/read/tree.rs @@ -61,3 +61,18 @@ where .and_then(|chain| chain.as_ref().orchard_tree(hash_or_height)) .or_else(|| db.orchard_tree(hash_or_height)) } + +#[cfg(feature = "getblocktemplate-rpcs")] +/// Get the history tree of the provided chain. +pub fn history_tree( + chain: Option, + db: &ZebraDb, + hash_or_height: HashOrHeight, +) -> Option> +where + C: AsRef, +{ + chain + .and_then(|chain| chain.as_ref().history_tree(hash_or_height)) + .or_else(|| Some(db.history_tree())) +}