bitcoin_explorer/parser/
tx_index.rs

1use crate::parser::block_index::BlockIndex;
2use crate::parser::errors::{OpError, OpResult};
3use crate::parser::reader::BlockchainRead;
4use bitcoin::hashes::Hash;
5use bitcoin::Txid;
6use leveldb::database::Database;
7use leveldb::kv::KV;
8use leveldb::options::{Options, ReadOptions};
9use log::{info, warn};
10use std::collections::BTreeMap;
11use std::io::Cursor;
12use std::path::Path;
13use std::str::FromStr;
14
15const GENESIS_TXID: &str = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
16
17///
18/// tx-index: looking up transaction position using txid.
19///
20/// This is possible if Bitcoin Core has `txindex=1`.
21///
22pub struct TxDB {
23    db: Option<Database<TxKey>>,
24    // used for reverse looking up to block height
25    file_pos_to_height: BTreeMap<(i32, u32), i32>,
26    genesis_txid: Txid,
27}
28
29/// Records transaction storage on disk
30pub struct TransactionRecord {
31    pub txid: Txid,
32    pub n_file: i32,
33    pub n_pos: u32,
34    pub n_tx_offset: u32,
35}
36
37impl TransactionRecord {
38    fn from(key: &[u8], values: &[u8]) -> OpResult<Self> {
39        let mut reader = Cursor::new(values);
40        Ok(TransactionRecord {
41            txid: Txid::from_slice(key)?,
42            n_file: reader.read_varint()? as i32,
43            n_pos: reader.read_varint()? as u32,
44            n_tx_offset: reader.read_varint()? as u32,
45        })
46    }
47}
48
49impl TxDB {
50    /// initialize TxDB for transaction queries
51    pub fn new(path: &Path, blk_index: &BlockIndex) -> TxDB {
52        let option_db = TxDB::try_open_db(path);
53        if let Some(db) = option_db {
54            let mut file_pos_to_height = BTreeMap::new();
55            for b in blk_index.records.iter() {
56                file_pos_to_height.insert((b.n_file, b.n_data_pos), b.n_height);
57            }
58            TxDB {
59                db: Some(db),
60                file_pos_to_height,
61                genesis_txid: Txid::from_str(GENESIS_TXID).unwrap(),
62            }
63        } else {
64            TxDB::null()
65        }
66    }
67
68    #[inline]
69    pub(crate) fn is_open(&self) -> bool {
70        self.db.is_some()
71    }
72
73    #[inline]
74    pub(crate) fn null() -> TxDB {
75        TxDB {
76            db: None,
77            file_pos_to_height: BTreeMap::new(),
78            genesis_txid: Txid::from_str(GENESIS_TXID).unwrap(),
79        }
80    }
81
82    #[inline]
83    ///
84    /// genesis tx is not included in UTXO because of Bitcoin Core Bug
85    ///
86    pub(crate) fn is_genesis_tx(&self, txid: &Txid) -> bool {
87        txid == &self.genesis_txid
88    }
89
90    fn try_open_db(path: &Path) -> Option<Database<TxKey>> {
91        if !path.exists() {
92            warn!("Failed to open tx_index DB: tx_index not built");
93            return None;
94        }
95        let options = Options::new();
96        match Database::open(path, options) {
97            Ok(db) => {
98                info! {"Successfully opened tx_index DB!"}
99                Some(db)
100            }
101            Err(e) => {
102                warn!("Filed to open tx_index DB: {:?}", e);
103                None
104            }
105        }
106    }
107
108    /// note that this function cannot find genesis block, which needs special treatment
109    pub(crate) fn get_tx_record(&self, txid: &Txid) -> OpResult<TransactionRecord> {
110        if let Some(db) = &self.db {
111            let inner = txid.as_inner();
112            let mut key = Vec::with_capacity(inner.len() + 1);
113            key.push(b't');
114            key.extend(inner);
115            let key = TxKey { key };
116            let read_options = ReadOptions::new();
117            match db.get(read_options, &key) {
118                Ok(value) => {
119                    if let Some(value) = value {
120                        Ok(TransactionRecord::from(&key.key[1..], value.as_slice())?)
121                    } else {
122                        Err(OpError::from(
123                            format!("value not found for txid: {}", txid).as_str(),
124                        ))
125                    }
126                }
127                Err(e) => Err(OpError::from(
128                    format!("value not found for txid: {}", e).as_str(),
129                )),
130            }
131        } else {
132            Err(OpError::from("TxDB not open"))
133        }
134    }
135
136    pub(crate) fn get_block_height_of_tx(&self, txid: &Txid) -> OpResult<usize> {
137        // genesis transaction requires special treatment
138        if self.is_genesis_tx(txid) {
139            return Ok(0);
140        }
141        let record: TransactionRecord = self.get_tx_record(txid)?;
142        let file_pos_height = &self.file_pos_to_height;
143        match file_pos_height.get(&(record.n_file, record.n_pos)) {
144            None => Err(OpError::from("transaction not found")),
145            Some(pos_height) => Ok(*pos_height as usize),
146        }
147    }
148}
149
150/// levelDB key utility
151struct TxKey {
152    key: Vec<u8>,
153}
154
155/// levelDB key utility
156impl db_key::Key for TxKey {
157    fn from_u8(key: &[u8]) -> Self {
158        TxKey {
159            key: Vec::from(key),
160        }
161    }
162
163    fn as_slice<T, F: Fn(&[u8]) -> T>(&self, f: F) -> T {
164        f(&self.key)
165    }
166}