Changeset View
Changeset View
Standalone View
Standalone View
chronik/chronik-indexer/src/query/blocks.rs
| // Copyright (c) 2023 The Bitcoin developers | // Copyright (c) 2023 The Bitcoin developers | ||||
| // Distributed under the MIT software license, see the accompanying | // Distributed under the MIT software license, see the accompanying | ||||
| // file COPYING or http://www.opensource.org/licenses/mit-license.php. | // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
| //! Module for [`QueryBlocks`], to query blocks. | //! Module for [`QueryBlocks`], to query blocks. | ||||
| use abc_rust_error::{Result, WrapErr}; | use abc_rust_error::{Result, WrapErr}; | ||||
| use bitcoinsuite_core::{ | use bitcoinsuite_core::{ | ||||
| block::BlockHash, | block::BlockHash, | ||||
| hash::{Hashed, Sha256d}, | |||||
| tx::{Tx, TxId}, | tx::{Tx, TxId}, | ||||
| }; | }; | ||||
| use chronik_bridge::ffi; | use chronik_bridge::ffi; | ||||
| use chronik_db::{ | use chronik_db::{ | ||||
| db::Db, | db::Db, | ||||
| io::{ | io::{ | ||||
| BlockHeight, BlockReader, BlockStats, BlockStatsReader, DbBlock, | BlockHeight, BlockReader, BlockStats, BlockStatsReader, DbBlock, | ||||
| SpentByReader, TxNum, TxReader, | SpentByReader, TxNum, TxReader, | ||||
| }, | }, | ||||
| mem::Mempool, | mem::Mempool, | ||||
| }; | }; | ||||
| use chronik_plugin::data::PluginNameMap; | use chronik_plugin::data::PluginNameMap; | ||||
| use chronik_proto::proto; | use chronik_proto::proto; | ||||
| use thiserror::Error; | use thiserror::Error; | ||||
| use tokio::sync::Mutex; | |||||
| use crate::{ | use crate::{ | ||||
| avalanche::Avalanche, | avalanche::Avalanche, | ||||
| indexer::Node, | indexer::Node, | ||||
| merkle::BlockMerkleTree, | |||||
| query::{ | query::{ | ||||
| make_tx_proto, read_plugin_outputs, HashOrHeight, MakeTxProtoParams, | make_tx_proto, read_plugin_outputs, HashOrHeight, MakeTxProtoParams, | ||||
| OutputsSpent, TxTokenData, | OutputsSpent, TxTokenData, | ||||
| }, | }, | ||||
| }; | }; | ||||
| const MAX_BLOCKS_PAGE_SIZE: usize = 500; | const MAX_BLOCKS_PAGE_SIZE: usize = 500; | ||||
| Show All 12 Lines | pub struct QueryBlocks<'a> { | ||||
| /// Mempool | /// Mempool | ||||
| pub mempool: &'a Mempool, | pub mempool: &'a Mempool, | ||||
| /// Access to bitcoind to read txs | /// Access to bitcoind to read txs | ||||
| pub node: &'a Node, | pub node: &'a Node, | ||||
| /// Whether the SLP/ALP token index is enabled | /// Whether the SLP/ALP token index is enabled | ||||
| pub is_token_index_enabled: bool, | pub is_token_index_enabled: bool, | ||||
| /// Map plugin name <-> plugin idx of all loaded plugins | /// Map plugin name <-> plugin idx of all loaded plugins | ||||
| pub plugin_name_map: &'a PluginNameMap, | pub plugin_name_map: &'a PluginNameMap, | ||||
| /// Cached block merkle tree | |||||
| pub block_merkle_tree: &'a Mutex<BlockMerkleTree>, | |||||
| } | } | ||||
| /// Errors indicating something went wrong with querying blocks. | /// Errors indicating something went wrong with querying blocks. | ||||
| #[derive(Debug, Error, PartialEq)] | #[derive(Debug, Error, PartialEq)] | ||||
| pub enum QueryBlockError { | pub enum QueryBlockError { | ||||
| /// Block not found in DB | /// Block not found in DB | ||||
| #[error("404: Block not found: {0}")] | #[error("404: Block not found: {0}")] | ||||
| BlockNotFound(String), | BlockNotFound(String), | ||||
| /// Invalid block start height | /// Invalid block start height | ||||
| #[error("400: Invalid block start height: {0}")] | #[error("400: Invalid block start height: {0}")] | ||||
| InvalidStartHeight(BlockHeight), | InvalidStartHeight(BlockHeight), | ||||
| /// Invalid block end height | /// Invalid block end height | ||||
| #[error("400: Invalid block end height: {0}")] | #[error("400: Invalid block end height: {0}")] | ||||
| InvalidEndHeight(BlockHeight), | InvalidEndHeight(BlockHeight), | ||||
| /// Invalid checkpoint height | |||||
| #[error( | |||||
| "400: Invalid checkpoint height {0}, may not be below queried header \ | |||||
| height {1}" | |||||
| )] | |||||
| InvalidCheckpointHeight(BlockHeight, BlockHeight), | |||||
| /// Blocks page size too large | /// Blocks page size too large | ||||
| #[error( | #[error( | ||||
| "400: Blocks page size too large, may not be above {} but got {0}", | "400: Blocks page size too large, may not be above {} but got {0}", | ||||
| MAX_BLOCKS_PAGE_SIZE | MAX_BLOCKS_PAGE_SIZE | ||||
| )] | )] | ||||
| BlocksPageSizeTooLarge(usize), | BlocksPageSizeTooLarge(usize), | ||||
| /// DB is missing block stats | /// DB is missing block stats | ||||
| ▲ Show 20 Lines • Show All 69 Lines • ▼ Show 20 Lines | ) -> Result<usize> { | ||||
| } | } | ||||
| let num_blocks = end_height as usize - start_height as usize + 1; | let num_blocks = end_height as usize - start_height as usize + 1; | ||||
| if num_blocks > MAX_BLOCKS_PAGE_SIZE { | if num_blocks > MAX_BLOCKS_PAGE_SIZE { | ||||
| return Err(BlocksPageSizeTooLarge(num_blocks).into()); | return Err(BlocksPageSizeTooLarge(num_blocks).into()); | ||||
| } | } | ||||
| Ok(num_blocks) | Ok(num_blocks) | ||||
| } | } | ||||
| /// Check that the block height and checkpoint height are consistent. | |||||
| fn check_checkpoint_height( | |||||
| block_height: BlockHeight, | |||||
| checkpoint_height: BlockHeight, | |||||
| ) -> Result<(), QueryBlockError> { | |||||
| if block_height > checkpoint_height { | |||||
| return Err(InvalidCheckpointHeight( | |||||
| checkpoint_height, | |||||
| block_height, | |||||
| )); | |||||
| } | |||||
| Ok(()) | |||||
| } | |||||
| /// Query blocks by a range of heights. Start and end height are inclusive. | /// Query blocks by a range of heights. Start and end height are inclusive. | ||||
| pub fn by_range( | pub fn by_range( | ||||
| &self, | &self, | ||||
| start_height: BlockHeight, | start_height: BlockHeight, | ||||
| end_height: BlockHeight, | end_height: BlockHeight, | ||||
| ) -> Result<proto::Blocks> { | ) -> Result<proto::Blocks> { | ||||
| let num_blocks = | let num_blocks = | ||||
| Self::check_range_boundaries(start_height, end_height)?; | Self::check_range_boundaries(start_height, end_height)?; | ||||
| ▲ Show 20 Lines • Show All 135 Lines • ▼ Show 20 Lines | ) -> proto::BlockInfo { | ||||
| num_outputs: block_stats.num_outputs, | num_outputs: block_stats.num_outputs, | ||||
| sum_input_sats: block_stats.sum_input_sats, | sum_input_sats: block_stats.sum_input_sats, | ||||
| sum_coinbase_output_sats: block_stats.sum_coinbase_output_sats, | sum_coinbase_output_sats: block_stats.sum_coinbase_output_sats, | ||||
| sum_normal_output_sats: block_stats.sum_normal_output_sats, | sum_normal_output_sats: block_stats.sum_normal_output_sats, | ||||
| sum_burned_sats: block_stats.sum_burned_sats, | sum_burned_sats: block_stats.sum_burned_sats, | ||||
| } | } | ||||
| } | } | ||||
| /// Query a block header | /// Query a block header. Optional checkpoint data is computed and | ||||
| pub fn header(&self, hash_or_height: String) -> Result<proto::BlockHeader> { | /// included in the result if checkpoint_height > 0. | ||||
| pub async fn header( | |||||
| &self, | |||||
| hash_or_height: String, | |||||
| checkpoint_height: i32, | |||||
| ) -> Result<proto::BlockHeader> { | |||||
| let bridge = &self.node.bridge; | let bridge = &self.node.bridge; | ||||
| let block_index = match hash_or_height.parse::<HashOrHeight>()? { | let block_index = match hash_or_height.parse::<HashOrHeight>()? { | ||||
| HashOrHeight::Hash(hash) => { | HashOrHeight::Hash(hash) => { | ||||
| bridge.lookup_block_index(hash.to_bytes()) | bridge.lookup_block_index(hash.to_bytes()) | ||||
| } | } | ||||
| HashOrHeight::Height(height) => { | HashOrHeight::Height(height) => { | ||||
| bridge.lookup_block_index_by_height(height) | bridge.lookup_block_index_by_height(height) | ||||
| } | } | ||||
| }; | }; | ||||
| let block_index = | let block_index = | ||||
| block_index.map_err(|_| BlockNotFound(hash_or_height))?; | block_index.map_err(|_| BlockNotFound(hash_or_height))?; | ||||
| let block_height = ffi::get_block_info(block_index).height; | |||||
| let (root, branch) = self | |||||
| .merkle_root_and_branch(block_height, checkpoint_height) | |||||
| .await?; | |||||
| Ok(proto::BlockHeader { | Ok(proto::BlockHeader { | ||||
| raw_header: ffi::get_block_header(block_index).to_vec(), | raw_header: ffi::get_block_header(block_index).to_vec(), | ||||
| root, | |||||
| branch, | |||||
| }) | }) | ||||
| } | } | ||||
| async fn merkle_root_and_branch( | |||||
| &self, | |||||
| block_height: BlockHeight, | |||||
| checkpoint_height: BlockHeight, | |||||
| ) -> Result<(Vec<u8>, Vec<Vec<u8>>)> { | |||||
| let mut root = Vec::<u8>::new(); | |||||
| let mut branch = Vec::<Vec<u8>>::new(); | |||||
| if checkpoint_height > 0 { | |||||
| Self::check_checkpoint_height(block_height, checkpoint_height)?; | |||||
| let bridge = &self.node.bridge; | |||||
| let hashes: Vec<Sha256d> = bridge | |||||
| .get_block_hashes_by_range(0, checkpoint_height)? | |||||
| .iter() | |||||
| .map(|raw_hash| Sha256d::from_le_bytes(raw_hash.data)) | |||||
| .collect(); | |||||
| let mut block_merkle_tree = self.block_merkle_tree.lock().await; | |||||
| let (root_hash, branch_hashes) = block_merkle_tree | |||||
| .merkle_root_and_branch(&hashes, block_height as usize); | |||||
| root = root_hash.to_le_bytes().to_vec(); | |||||
| branch = branch_hashes | |||||
| .iter() | |||||
| .map(|&hash| hash.to_le_bytes().to_vec()) | |||||
| .collect(); | |||||
| } | |||||
| Ok((root, branch)) | |||||
| } | |||||
| /// Query headers by a range of heights. Start and end height are inclusive. | /// Query headers by a range of heights. Start and end height are inclusive. | ||||
| pub fn headers_by_range( | /// Optional checkpoint data is computed and included with the last header | ||||
| /// if checkpoint_height > 0. | |||||
| pub async fn headers_by_range( | |||||
| &self, | &self, | ||||
| start_height: BlockHeight, | start_height: BlockHeight, | ||||
| end_height: BlockHeight, | end_height: BlockHeight, | ||||
| checkpoint_height: i32, | |||||
| ) -> Result<proto::BlockHeaders> { | ) -> Result<proto::BlockHeaders> { | ||||
| Self::check_range_boundaries(start_height, end_height)?; | Self::check_range_boundaries(start_height, end_height)?; | ||||
| let (root, branch) = self | |||||
| .merkle_root_and_branch(end_height, checkpoint_height) | |||||
| .await?; | |||||
| let headers = self | let headers = self | ||||
| .node | .node | ||||
| .bridge | .bridge | ||||
| .get_block_headers_by_range(start_height, end_height)?; | .get_block_headers_by_range(start_height, end_height)?; | ||||
| let headers = headers | let last_header = headers.last(); | ||||
| let mut headers: Vec<_> = headers | |||||
| .iter() | .iter() | ||||
| .take(headers.len() - 1) | |||||
| .map(|h| proto::BlockHeader { | .map(|h| proto::BlockHeader { | ||||
| raw_header: h.data.to_vec(), | raw_header: h.data.to_vec(), | ||||
| root: Vec::<u8>::new(), | |||||
| branch: Vec::<Vec<u8>>::new(), | |||||
| }) | }) | ||||
| .collect(); | .collect(); | ||||
| if let Some(h) = last_header { | |||||
tobias_ruck: let's use shadowing here, common to unwrap an Option into the same variable name | |||||
| headers.push(proto::BlockHeader { | |||||
| raw_header: h.data.to_vec(), | |||||
| root, | |||||
| branch, | |||||
| }); | |||||
| } | |||||
| Ok(proto::BlockHeaders { headers }) | Ok(proto::BlockHeaders { headers }) | ||||
| } | } | ||||
| } | } | ||||
let's use shadowing here, common to unwrap an Option into the same variable name