diff --git a/chronik/chronik-indexer/src/query/blocks.rs b/chronik/chronik-indexer/src/query/blocks.rs index 0447185b9..27fc8ecaa 100644 --- a/chronik/chronik-indexer/src/query/blocks.rs +++ b/chronik/chronik-indexer/src/query/blocks.rs @@ -1,165 +1,153 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. //! Module for [`QueryBlocks`], to query blocks. use abc_rust_error::Result; -use bitcoinsuite_core::block::BlockHash; use chronik_db::{ db::Db, io::{BlockHeight, BlockReader, BlockStats, BlockStatsReader, DbBlock}, }; use chronik_proto::proto; use thiserror::Error; -use crate::avalanche::Avalanche; +use crate::{avalanche::Avalanche, query::HashOrHeight}; const MAX_BLOCKS_PAGE_SIZE: usize = 500; /// Struct for querying blocks from the DB. #[derive(Debug)] pub struct QueryBlocks<'a> { /// Db. pub db: &'a Db, /// Avalanche. pub avalanche: &'a Avalanche, } /// Errors indicating something went wrong with querying blocks. #[derive(Debug, Error, PartialEq)] pub enum QueryBlockError { - /// Query is neither a hex hash nor an integer string - #[error("400: Not a hash or height: {0}")] - NotHashOrHeight(String), - /// Block not found in DB #[error("404: Block not found: {0}")] BlockNotFound(String), /// Invalid block start height #[error("400: Invalid block start height: {0}")] InvalidStartHeight(BlockHeight), /// Invalid block end height #[error("400: Invalid block end height: {0}")] InvalidEndHeight(BlockHeight), /// Blocks page size too large #[error( "400: Blocks page size too large, may not be above {} but got {0}", MAX_BLOCKS_PAGE_SIZE )] BlocksPageSizeTooLarge(usize), /// DB is missing block stats #[error("500: Inconsistent DB: Missing block stats for height {0}")] MissingBlockStats(BlockHeight), } use self::QueryBlockError::*; impl<'a> QueryBlocks<'a> { /// Query a block by hash or height from DB. /// /// `height` may not have any leading zeros, because otherwise it might /// become ambiguous with a hash. pub fn by_hash_or_height( &self, hash_or_height: String, ) -> Result { let db_blocks = BlockReader::new(self.db)?; let block_stats_reader = BlockStatsReader::new(self.db)?; - let db_block = if let Ok(hash) = hash_or_height.parse::() { - db_blocks.by_hash(&hash)? - } else { - let height = match hash_or_height.parse::() { - // disallow leading zeros - Ok(0) if hash_or_height.len() == 1 => 0, - Ok(height) if !hash_or_height.starts_with('0') => height, - _ => return Err(NotHashOrHeight(hash_or_height).into()), - }; - db_blocks.by_height(height)? + let db_block = match hash_or_height.parse::()? { + HashOrHeight::Hash(hash) => db_blocks.by_hash(&hash)?, + HashOrHeight::Height(height) => db_blocks.by_height(height)?, }; let db_block = db_block.ok_or(BlockNotFound(hash_or_height))?; let block_stats = block_stats_reader .by_height(db_block.height)? .ok_or(MissingBlockStats(db_block.height))?; Ok(proto::Block { block_info: Some( self.make_block_info_proto(&db_block, &block_stats), ), }) } /// Query blocks by a range of heights. Start and end height are inclusive. pub fn by_range( &self, start_height: BlockHeight, end_height: BlockHeight, ) -> Result { if start_height < 0 { return Err(InvalidStartHeight(start_height).into()); } if end_height < start_height { return Err(InvalidEndHeight(end_height).into()); } let num_blocks = end_height as usize - start_height as usize + 1; if num_blocks > MAX_BLOCKS_PAGE_SIZE { return Err(BlocksPageSizeTooLarge(num_blocks).into()); } let block_reader = BlockReader::new(self.db)?; let block_stats_reader = BlockStatsReader::new(self.db)?; let mut blocks = Vec::with_capacity(num_blocks); for block_height in start_height..=end_height { let block = block_reader.by_height(block_height)?; let block = match block { Some(block) => block, None => break, }; let block_stats = block_stats_reader .by_height(block_height)? .ok_or(MissingBlockStats(block_height))?; blocks.push(self.make_block_info_proto(&block, &block_stats)); } Ok(proto::Blocks { blocks }) } /// Query some info about the blockchain, e.g. the tip hash and height. pub fn blockchain_info(&self) -> Result { let block_reader = BlockReader::new(self.db)?; match block_reader.tip()? { Some(block) => Ok(proto::BlockchainInfo { tip_hash: block.hash.to_vec(), tip_height: block.height, }), None => Ok(proto::BlockchainInfo { tip_hash: vec![0; 32], tip_height: -1, }), } } fn make_block_info_proto( &self, db_block: &DbBlock, block_stats: &BlockStats, ) -> proto::BlockInfo { proto::BlockInfo { hash: db_block.hash.to_vec(), prev_hash: db_block.prev_hash.to_vec(), height: db_block.height, n_bits: db_block.n_bits, timestamp: db_block.timestamp, is_final: self.avalanche.is_final_height(db_block.height), block_size: block_stats.block_size, num_txs: block_stats.num_txs, num_inputs: block_stats.num_inputs, num_outputs: block_stats.num_outputs, sum_input_sats: block_stats.sum_input_sats, sum_coinbase_output_sats: block_stats.sum_coinbase_output_sats, sum_normal_output_sats: block_stats.sum_normal_output_sats, sum_burned_sats: block_stats.sum_burned_sats, } } } diff --git a/chronik/chronik-indexer/src/query/util.rs b/chronik/chronik-indexer/src/query/util.rs index 396e6241f..9c513f76e 100644 --- a/chronik/chronik-indexer/src/query/util.rs +++ b/chronik/chronik-indexer/src/query/util.rs @@ -1,171 +1,208 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -use std::collections::{hash_map::Entry, BTreeMap, HashMap}; +use std::{ + collections::{hash_map::Entry, BTreeMap, HashMap}, + str::FromStr, +}; -use abc_rust_error::Result; +use abc_rust_error::{Report, Result}; use bitcoinsuite_core::{ + block::BlockHash, ser::BitcoinSer, tx::{OutPoint, SpentBy, Tx, TxId}, }; -use chronik_db::io::{DbBlock, SpentByEntry, SpentByReader, TxNum, TxReader}; +use chronik_db::io::{ + BlockHeight, DbBlock, SpentByEntry, SpentByReader, TxNum, TxReader, +}; use chronik_proto::proto; use thiserror::Error; use crate::avalanche::Avalanche; /// Errors indicating something went wrong with reading txs. #[derive(Debug, Error, PartialEq)] pub enum QueryUtilError { /// DB contains a spent-by entry whose referenced tx_num cannot be found. #[error( "500: Inconsistent DB: tx_num = {tx_num} has spent entry {entry:?} in \ the DB, but the tx cannot be not found in the index" )] SpendingTxNotFound { /// Which tx has an output spent by an unknown tx. tx_num: TxNum, /// The offending entry in the DB that references an unknown tx. entry: SpentByEntry, }, + + /// Query is neither a hex hash nor an integer string + #[error("400: Not a hash or height: {0}")] + NotHashOrHeight(String), } use self::QueryUtilError::*; /// Make a [`proto::Tx`]. pub(crate) fn make_tx_proto( tx: &Tx, outputs_spent: &OutputsSpent<'_>, time_first_seen: i64, is_coinbase: bool, block: Option<&DbBlock>, avalanche: &Avalanche, ) -> proto::Tx { proto::Tx { txid: tx.txid().to_vec(), version: tx.version, inputs: tx .inputs .iter() .map(|input| { let coin = input.coin.as_ref(); let (output_script, value) = coin .map(|coin| { (coin.output.script.to_vec(), coin.output.value) }) .unwrap_or_default(); proto::TxInput { prev_out: Some(make_outpoint_proto(&input.prev_out)), input_script: input.script.to_vec(), output_script, value, sequence_no: input.sequence, } }) .collect(), outputs: tx .outputs .iter() .enumerate() .map(|(output_idx, output)| proto::TxOutput { value: output.value, output_script: output.script.to_vec(), spent_by: outputs_spent .spent_by(output_idx as u32) .map(|spent_by| make_spent_by_proto(&spent_by)), }) .collect(), lock_time: tx.locktime, block: block.map(|block| proto::BlockMetadata { hash: block.hash.to_vec(), height: block.height, timestamp: block.timestamp, is_final: avalanche.is_final_height(block.height), }), time_first_seen, size: tx.ser_len() as u32, is_coinbase, } } pub(crate) fn make_outpoint_proto(outpoint: &OutPoint) -> proto::OutPoint { proto::OutPoint { txid: outpoint.txid.to_vec(), out_idx: outpoint.out_idx, } } fn make_spent_by_proto(spent_by: &SpentBy) -> proto::SpentBy { proto::SpentBy { txid: spent_by.txid.to_vec(), input_idx: spent_by.input_idx, } } +pub(crate) enum HashOrHeight { + Hash(BlockHash), + Height(BlockHeight), +} + +impl FromStr for HashOrHeight { + type Err = Report; + + fn from_str(hash_or_height: &str) -> Result { + if let Ok(hash) = hash_or_height.parse::() { + Ok(HashOrHeight::Hash(hash)) + } else { + let height = match hash_or_height.parse::() { + // disallow leading zeros + Ok(0) if hash_or_height.len() == 1 => 0, + Ok(height) if !hash_or_height.starts_with('0') => height, + _ => { + return Err( + NotHashOrHeight(hash_or_height.to_string()).into() + ); + } + }; + Ok(HashOrHeight::Height(height)) + } + } +} + /// Helper struct for querying which tx outputs have been spent by DB or mempool /// txs. pub(crate) struct OutputsSpent<'a> { spent_by_mempool: Option<&'a BTreeMap>, spent_by_blocks: Vec, txid_by_num: HashMap, } impl<'a> OutputsSpent<'a> { pub(crate) fn new_mempool( spent_by_mempool: Option<&'a BTreeMap>, ) -> Self { OutputsSpent { spent_by_mempool, spent_by_blocks: vec![], txid_by_num: HashMap::new(), } } pub(crate) fn query( spent_by_reader: &SpentByReader<'_>, tx_reader: &TxReader<'_>, spent_by_mempool: Option<&'a BTreeMap>, tx_num: TxNum, ) -> Result { let spent_by_blocks = spent_by_reader.by_tx_num(tx_num)?.unwrap_or_default(); let mut txid_by_num = HashMap::::new(); for spent_by in &spent_by_blocks { if let Entry::Vacant(entry) = txid_by_num.entry(spent_by.tx_num) { let txid = tx_reader .txid_by_tx_num(spent_by.tx_num)? .ok_or_else(|| SpendingTxNotFound { tx_num, entry: spent_by.clone(), })?; entry.insert(txid); } } Ok(OutputsSpent { spent_by_mempool, spent_by_blocks, txid_by_num, }) } pub(crate) fn spent_by(&self, output_idx: u32) -> Option { if let Some(spent_by_mempool) = self.spent_by_mempool { if let Some(outpoint) = spent_by_mempool.get(&output_idx) { return Some(*outpoint); } } let search_idx = self .spent_by_blocks .binary_search_by_key(&output_idx, |entry| entry.out_idx); let entry = match search_idx { Ok(found_idx) => &self.spent_by_blocks[found_idx], Err(_) => return None, }; let txid = self.txid_by_num.get(&entry.tx_num).unwrap(); Some(SpentBy { txid: *txid, input_idx: entry.input_idx, }) } }