mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-29 00:59:58 -07:00
git: reset
This commit is contained in:
3
parser/src/bitcoin/addresses/mod.rs
Normal file
3
parser/src/bitcoin/addresses/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod multisig;
|
||||
|
||||
pub use multisig::*;
|
||||
57
parser/src/bitcoin/addresses/multisig.rs
Normal file
57
parser/src/bitcoin/addresses/multisig.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// Code from bitcoin-explorer now deprecated
|
||||
//
|
||||
|
||||
use bitcoin::{
|
||||
blockdata::{
|
||||
opcodes::all,
|
||||
script::Instruction::{self, Op, PushBytes},
|
||||
},
|
||||
Opcode, Script,
|
||||
};
|
||||
|
||||
///
|
||||
/// Obtain addresses for multisig transactions.
|
||||
///
|
||||
pub fn multisig_addresses(script: &Script) -> Vec<Vec<u8>> {
|
||||
let ops: Vec<Instruction> = script.instructions().filter_map(|o| o.ok()).collect();
|
||||
|
||||
// obtain number of keys
|
||||
let num_keys = {
|
||||
if let Some(Op(op)) = ops.get(ops.len() - 2) {
|
||||
decode_from_op_n(op)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
|
||||
// read public keys
|
||||
let mut public_keys = Vec::with_capacity(num_keys as usize);
|
||||
|
||||
for op in ops.iter().skip(1).take(num_keys as usize) {
|
||||
if let PushBytes(data) = op {
|
||||
public_keys.push(data.as_bytes().to_vec());
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
public_keys
|
||||
}
|
||||
|
||||
///
|
||||
/// Decode OP_N
|
||||
///
|
||||
/// translated from BitcoinJ:
|
||||
/// [decodeFromOpN()](https://github.com/bitcoinj/bitcoinj/blob/d3d5edbcbdb91b25de4df3b6ed6740d7e2329efc/core/src/main/java/org/bitcoinj/script/Script.java#L515:L524)
|
||||
///
|
||||
#[inline]
|
||||
fn decode_from_op_n(op: &Opcode) -> i32 {
|
||||
if op.eq(&all::OP_PUSHBYTES_0) {
|
||||
0
|
||||
} else if op.eq(&all::OP_PUSHNUM_NEG1) {
|
||||
-1
|
||||
} else {
|
||||
op.to_u8() as i32 + 1 - all::OP_PUSHNUM_1.to_u8() as i32
|
||||
}
|
||||
}
|
||||
2
parser/src/bitcoin/consts.rs
Normal file
2
parser/src/bitcoin/consts.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub const NUMBER_OF_UNSAFE_BLOCKS: usize = 100;
|
||||
pub const TARGET_BLOCKS_PER_DAY: usize = 144;
|
||||
122
parser/src/bitcoin/daemon.rs
Normal file
122
parser/src/bitcoin/daemon.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use std::{process::Command, thread::sleep, time::Duration};
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::utils::{log, log_output, retry};
|
||||
|
||||
struct BlockchainInfo {
|
||||
pub headers: u64,
|
||||
pub blocks: u64,
|
||||
}
|
||||
|
||||
pub struct BitcoinDaemon<'a> {
|
||||
path: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> BitcoinDaemon<'a> {
|
||||
pub fn new(bitcoin_dir_path: &'a str) -> Self {
|
||||
Self {
|
||||
path: bitcoin_dir_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
sleep(Duration::from_secs(1));
|
||||
|
||||
let mut command = Command::new("bitcoind");
|
||||
|
||||
command
|
||||
.arg(self.datadir_arg())
|
||||
.arg("-blocksonly")
|
||||
.arg("-txindex=1")
|
||||
.arg("-daemon");
|
||||
|
||||
// bitcoind -datadir=/Users/k/Developer/bitcoin -blocksonly -txindex=1 -daemon
|
||||
let output = command
|
||||
.output()
|
||||
.expect("bitcoind to be able to properly start");
|
||||
|
||||
log_output(&output);
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
// bitcoin-cli -datadir=/Users/k/Developer/bitcoin stop
|
||||
let output = Command::new("bitcoin-cli")
|
||||
.arg(self.datadir_arg())
|
||||
.arg("stop")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
if output.status.success() {
|
||||
log_output(&output);
|
||||
|
||||
sleep(Duration::from_secs(15));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait_sync(&self) {
|
||||
while !self.check_if_fully_synced() {
|
||||
sleep(Duration::from_secs(5))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait_for_new_block(&self, last_block_height: usize) {
|
||||
log("Waiting for new block...");
|
||||
|
||||
while self.get_blockchain_info().headers as usize == last_block_height {
|
||||
sleep(Duration::from_secs(5))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_if_fully_synced(&self) -> bool {
|
||||
let BlockchainInfo { blocks, headers } = self.get_blockchain_info();
|
||||
|
||||
let synced = blocks == headers;
|
||||
|
||||
if synced {
|
||||
log(&format!("Synced ! ({blocks} blocks)"));
|
||||
} else {
|
||||
log(&format!("Syncing... ({} remaining)", headers - blocks));
|
||||
}
|
||||
|
||||
synced
|
||||
}
|
||||
|
||||
fn get_blockchain_info(&self) -> BlockchainInfo {
|
||||
retry(
|
||||
|| {
|
||||
// bitcoin-cli -datadir=/Users/k/Developer/bitcoin getblockchaininfo
|
||||
let output = Command::new("bitcoin-cli")
|
||||
.arg(self.datadir_arg())
|
||||
.arg("getblockchaininfo")
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let json: Value = serde_json::from_str(&output)?;
|
||||
let json = json.as_object().ok_or(eyre!(""))?;
|
||||
|
||||
let blocks = json
|
||||
.get("blocks")
|
||||
.ok_or(eyre!(""))?
|
||||
.as_u64()
|
||||
.ok_or(eyre!(""))?;
|
||||
let headers = json
|
||||
.get("headers")
|
||||
.ok_or(eyre!(""))?
|
||||
.as_u64()
|
||||
.ok_or(eyre!(""))?;
|
||||
|
||||
Ok(BlockchainInfo { headers, blocks })
|
||||
},
|
||||
1,
|
||||
u64::MAX,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn datadir_arg(&self) -> String {
|
||||
format!("-datadir={}", self.path)
|
||||
}
|
||||
}
|
||||
152
parser/src/bitcoin/db/blk_files.rs
Normal file
152
parser/src/bitcoin/db/blk_files.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::From,
|
||||
fs::{self, DirEntry, File},
|
||||
io::{self, BufReader, Seek, SeekFrom},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use bitcoin::{io::Cursor, Block, Transaction};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
|
||||
use super::{
|
||||
errors::{OpError, OpErrorKind, OpResult},
|
||||
reader::BlockchainRead,
|
||||
};
|
||||
|
||||
///
|
||||
/// An index of all blk files found.
|
||||
///
|
||||
#[derive(Debug, Clone, Deref, DerefMut)]
|
||||
pub struct BlkFiles(HashMap<i32, PathBuf>);
|
||||
|
||||
impl BlkFiles {
|
||||
///
|
||||
/// Construct an index of all blk files.
|
||||
///
|
||||
pub fn new(path: &Path) -> OpResult<Self> {
|
||||
Ok(Self(Self::scan_path(path)?))
|
||||
}
|
||||
|
||||
///
|
||||
/// Read a Block from blk file.
|
||||
///
|
||||
#[inline]
|
||||
pub fn read_raw_block(&self, n_file: i32, offset: u32) -> OpResult<Vec<u8>> {
|
||||
if let Some(blk_path) = self.get(&n_file) {
|
||||
let mut r = BufReader::new(File::open(blk_path)?);
|
||||
r.seek(SeekFrom::Start(offset as u64 - 4))?;
|
||||
let block_size = r.read_u32()?;
|
||||
let block = r.read_u8_vec(block_size)?;
|
||||
Ok(block)
|
||||
} else {
|
||||
Err(OpError::from("blk file not found, sync with bitcoin core"))
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Read a Block from blk file.
|
||||
///
|
||||
pub fn read_block(&self, n_file: i32, offset: u32) -> OpResult<Block> {
|
||||
Cursor::new(self.read_raw_block(n_file, offset)?).read_block()
|
||||
}
|
||||
|
||||
///
|
||||
/// Read a transaction from blk file.
|
||||
///
|
||||
pub fn read_transaction(
|
||||
&self,
|
||||
n_file: i32,
|
||||
n_pos: u32,
|
||||
n_tx_offset: u32,
|
||||
) -> OpResult<Transaction> {
|
||||
if let Some(blk_path) = self.get(&n_file) {
|
||||
let mut r = BufReader::new(File::open(blk_path)?);
|
||||
// the size of a header is 80.
|
||||
r.seek(SeekFrom::Start(n_pos as u64 + n_tx_offset as u64 + 80))?;
|
||||
r.read_transaction()
|
||||
} else {
|
||||
Err(OpError::from("blk file not found, sync with bitcoin core"))
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Scan blk folder to build an index of all blk files.
|
||||
///
|
||||
fn scan_path(path: &Path) -> OpResult<HashMap<i32, PathBuf>> {
|
||||
let mut collected = HashMap::with_capacity(4000);
|
||||
|
||||
for entry in fs::read_dir(path)? {
|
||||
match entry {
|
||||
Ok(de) => {
|
||||
let path = Self::resolve_path(&de)?;
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
};
|
||||
if let Some(file_name) = path.as_path().file_name() {
|
||||
if let Some(file_name) = file_name.to_str() {
|
||||
if let Some(index) = Self::parse_blk_index(file_name) {
|
||||
collected.insert(index, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(msg) => {
|
||||
return Err(OpError::from(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collected.shrink_to_fit();
|
||||
|
||||
if collected.is_empty() {
|
||||
Err(OpError::new(OpErrorKind::RuntimeError).join_msg("No blk files found!"))
|
||||
} else {
|
||||
Ok(collected)
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Resolve symlink.
|
||||
///
|
||||
fn resolve_path(entry: &DirEntry) -> io::Result<PathBuf> {
|
||||
if entry.file_type()?.is_symlink() {
|
||||
fs::read_link(entry.path())
|
||||
} else {
|
||||
Ok(entry.path())
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Extract index from block file name.
|
||||
///
|
||||
fn parse_blk_index(file_name: &str) -> Option<i32> {
|
||||
let prefix = "blk";
|
||||
let ext = ".dat";
|
||||
if file_name.starts_with(prefix) && file_name.ends_with(ext) {
|
||||
file_name[prefix.len()..(file_name.len() - ext.len())]
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn test_parse_blk_index() {
|
||||
// assert_eq!(0, BlkFiles::parse_blk_index("blk00000.dat").unwrap());
|
||||
// assert_eq!(6, BlkFiles::parse_blk_index("blk6.dat").unwrap());
|
||||
// assert_eq!(1202, BlkFiles::parse_blk_index("blk1202.dat").unwrap());
|
||||
// assert_eq!(
|
||||
// 13412451,
|
||||
// BlkFiles::parse_blk_index("blk13412451.dat").unwrap()
|
||||
// );
|
||||
// assert!(BlkFiles::parse_blk_index("blkindex.dat").is_none());
|
||||
// assert!(BlkFiles::parse_blk_index("invalid.dat").is_none());
|
||||
// }
|
||||
// }
|
||||
45
parser/src/bitcoin/db/block_iter.rs
Normal file
45
parser/src/bitcoin/db/block_iter.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//!
|
||||
//! View development note of iter_connected.rs for implementation
|
||||
//! details of iter_block.rs, which follows similar principles.
|
||||
//!
|
||||
use bitcoin::Block;
|
||||
use par_iter_sync::{IntoParallelIteratorSync, ParIterSync};
|
||||
|
||||
use super::BitcoinDB;
|
||||
|
||||
pub struct BlockIter(ParIterSync<Block>);
|
||||
|
||||
impl BlockIter {
|
||||
/// the worker threads are dispatched in this `new` constructor!
|
||||
pub fn new<T>(db: &BitcoinDB, heights: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = usize> + Send + 'static,
|
||||
<T as IntoIterator>::IntoIter: Send + 'static,
|
||||
{
|
||||
let db_ref = db.clone();
|
||||
|
||||
BlockIter(
|
||||
heights.into_par_iter_sync(move |h| match db_ref.get_block(h) {
|
||||
Ok(blk) => Ok(blk),
|
||||
Err(_) => Err(()),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// the worker threads are dispatched in this `new` constructor!
|
||||
pub fn from_range(db: &BitcoinDB, start: usize, end: usize) -> Self {
|
||||
if end <= start {
|
||||
BlockIter::new(db, Vec::new())
|
||||
} else {
|
||||
BlockIter::new(db, start..end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for BlockIter {
|
||||
type Item = Block;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
211
parser/src/bitcoin/db/blocks_indexes.rs
Normal file
211
parser/src/bitcoin/db/blocks_indexes.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use std::{collections::BTreeMap, fmt, path::Path};
|
||||
|
||||
use bitcoin::{block::Header, io::Cursor, BlockHash};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use leveldb::{
|
||||
database::{iterator::LevelDBIterator, Database},
|
||||
iterator::Iterable,
|
||||
options::{Options, ReadOptions},
|
||||
};
|
||||
|
||||
use crate::utils::log;
|
||||
|
||||
use super::{BlockchainRead, OpResult};
|
||||
|
||||
///
|
||||
/// See Bitcoin Core repository for definition.
|
||||
///
|
||||
const BLOCK_VALID_HEADER: u32 = 1;
|
||||
const BLOCK_VALID_TREE: u32 = 2;
|
||||
const BLOCK_VALID_TRANSACTIONS: u32 = 3;
|
||||
const BLOCK_VALID_CHAIN: u32 = 4;
|
||||
const BLOCK_VALID_SCRIPTS: u32 = 5;
|
||||
const BLOCK_VALID_MASK: u32 = BLOCK_VALID_HEADER
|
||||
| BLOCK_VALID_TREE
|
||||
| BLOCK_VALID_TRANSACTIONS
|
||||
| BLOCK_VALID_CHAIN
|
||||
| BLOCK_VALID_SCRIPTS;
|
||||
const BLOCK_HAVE_DATA: u32 = 8;
|
||||
const BLOCK_HAVE_UNDO: u32 = 16;
|
||||
|
||||
///
|
||||
/// - Map from block height to block hash (records)
|
||||
/// - Map from block hash to block height (hash_to_height)
|
||||
///
|
||||
#[derive(Clone, Deref, DerefMut)]
|
||||
pub struct BlocksIndexes(Box<[BlockIndexRecord]>);
|
||||
|
||||
///
|
||||
/// BLOCK_INDEX RECORD as defined in Bitcoin Core.
|
||||
///
|
||||
#[derive(Clone)]
|
||||
pub struct BlockIndexRecord {
|
||||
pub n_version: i32,
|
||||
pub n_height: i32,
|
||||
pub n_status: u32,
|
||||
pub n_tx: u32,
|
||||
pub n_file: i32,
|
||||
pub n_data_pos: u32,
|
||||
pub n_undo_pos: u32,
|
||||
pub header: Header,
|
||||
}
|
||||
|
||||
impl BlocksIndexes {
|
||||
///
|
||||
/// Build a collections of block index.
|
||||
///
|
||||
pub(crate) fn new(p: &Path) -> OpResult<Self> {
|
||||
Ok(Self(load_block_index(p)?.into_boxed_slice()))
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Load all block index in memory from leveldb (i.e. `blocks/index` path).
|
||||
///
|
||||
/// Map from block height to block index record.
|
||||
///
|
||||
pub fn load_block_index(path: &Path) -> OpResult<Vec<BlockIndexRecord>> {
|
||||
let mut block_index_by_block_hash = BTreeMap::new();
|
||||
|
||||
log("Start loading block_index");
|
||||
|
||||
let mut options = Options::new();
|
||||
options.create_if_missing = false;
|
||||
let db: Database<BlockKey> = Database::open(path, options)?;
|
||||
let options = ReadOptions::new();
|
||||
let mut iter = db.iter(options);
|
||||
let mut max_height_block_hash = Option::<(BlockHash, i32)>::None;
|
||||
|
||||
while iter.advance() {
|
||||
let k = iter.key();
|
||||
let v = iter.value();
|
||||
if is_block_index_record(&k.key) {
|
||||
let record = BlockIndexRecord::from(&v)?;
|
||||
// only add valid block index record that has block data.
|
||||
if record.n_height == 0
|
||||
|| (record.n_status & BLOCK_VALID_MASK >= BLOCK_VALID_SCRIPTS
|
||||
&& record.n_status & BLOCK_HAVE_DATA > 0)
|
||||
{
|
||||
let block_hash = record.header.block_hash();
|
||||
// find the block with max height
|
||||
if let Some((hash, height)) = max_height_block_hash.as_mut() {
|
||||
if record.n_height > *height {
|
||||
*hash = block_hash;
|
||||
*height = record.n_height;
|
||||
}
|
||||
} else {
|
||||
max_height_block_hash = Some((block_hash, record.n_height));
|
||||
}
|
||||
block_index_by_block_hash.insert(block_hash, record);
|
||||
}
|
||||
}
|
||||
}
|
||||
// build the longest chain
|
||||
if let Some((hash, height)) = max_height_block_hash {
|
||||
let mut block_index = Vec::with_capacity(height as usize + 1);
|
||||
let mut current_hash = hash;
|
||||
let mut current_height = height;
|
||||
|
||||
// recursively build block index from max height block.
|
||||
while current_height >= 0 {
|
||||
let blk = block_index_by_block_hash
|
||||
.remove(¤t_hash)
|
||||
.expect("block hash not found in block index!");
|
||||
|
||||
assert_eq!(
|
||||
current_height, blk.n_height,
|
||||
"some block info missing from block index levelDB,\
|
||||
delete Bitcoin folder and re-download!"
|
||||
);
|
||||
|
||||
current_hash = blk.header.prev_blockhash;
|
||||
current_height -= 1;
|
||||
block_index.push(blk);
|
||||
}
|
||||
|
||||
block_index.reverse();
|
||||
|
||||
Ok(block_index)
|
||||
} else {
|
||||
Ok(Vec::with_capacity(0))
|
||||
}
|
||||
}
|
||||
|
||||
/// levelDB key util
|
||||
struct BlockKey {
|
||||
key: Vec<u8>,
|
||||
}
|
||||
|
||||
/// levelDB key util
|
||||
impl db_key::Key for BlockKey {
|
||||
fn from_u8(key: &[u8]) -> Self {
|
||||
BlockKey {
|
||||
key: Vec::from(key),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_slice<T, F: Fn(&[u8]) -> T>(&self, f: F) -> T {
|
||||
f(&self.key)
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockIndexRecord {
|
||||
///
|
||||
/// Decode levelDB value for Block Index Record.
|
||||
///
|
||||
fn from(values: &[u8]) -> OpResult<Self> {
|
||||
let mut reader = Cursor::new(values);
|
||||
|
||||
let n_version = reader.read_varint()? as i32;
|
||||
let n_height = reader.read_varint()? as i32;
|
||||
let n_status = reader.read_varint()? as u32;
|
||||
let n_tx = reader.read_varint()? as u32;
|
||||
let n_file = if n_status & (BLOCK_HAVE_DATA | BLOCK_HAVE_UNDO) > 0 {
|
||||
reader.read_varint()? as i32
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
let n_data_pos = if n_status & BLOCK_HAVE_DATA > 0 {
|
||||
reader.read_varint()? as u32
|
||||
} else {
|
||||
u32::MAX
|
||||
};
|
||||
let n_undo_pos = if n_status & BLOCK_HAVE_UNDO > 0 {
|
||||
reader.read_varint()? as u32
|
||||
} else {
|
||||
u32::MAX
|
||||
};
|
||||
|
||||
let header = reader.read_block_header()?;
|
||||
|
||||
Ok(BlockIndexRecord {
|
||||
n_version,
|
||||
n_height,
|
||||
n_status,
|
||||
n_tx,
|
||||
n_file,
|
||||
n_data_pos,
|
||||
n_undo_pos,
|
||||
header,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_block_index_record(data: &[u8]) -> bool {
|
||||
data.first() == Some(&b'b')
|
||||
}
|
||||
|
||||
impl fmt::Debug for BlockIndexRecord {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("BlockIndexRecord")
|
||||
.field("version", &self.n_version)
|
||||
.field("height", &self.n_height)
|
||||
.field("status", &self.n_status)
|
||||
.field("n_tx", &self.n_tx)
|
||||
.field("n_file", &self.n_file)
|
||||
.field("n_data_pos", &self.n_data_pos)
|
||||
.field("header", &self.header)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
135
parser/src/bitcoin/db/errors.rs
Normal file
135
parser/src/bitcoin/db/errors.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use std::convert::{self, From};
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::string;
|
||||
use std::sync;
|
||||
|
||||
pub type OpResult<T> = Result<T, OpError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Custom error type
|
||||
pub struct OpError {
|
||||
pub kind: OpErrorKind,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl OpError {
|
||||
pub fn new(kind: OpErrorKind) -> Self {
|
||||
OpError {
|
||||
kind,
|
||||
message: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins the Error with a new message and returns it
|
||||
pub fn join_msg(mut self, msg: &str) -> Self {
|
||||
self.message.push_str(msg);
|
||||
OpError {
|
||||
kind: self.kind,
|
||||
message: self.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for OpError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if self.message.is_empty() {
|
||||
write!(f, "{}", &self.kind)
|
||||
} else {
|
||||
write!(f, "{} {}", &self.message, &self.kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for OpError {
|
||||
fn description(&self) -> &str {
|
||||
self.message.as_ref()
|
||||
}
|
||||
fn cause(&self) -> Option<&dyn error::Error> {
|
||||
self.kind.source()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OpErrorKind {
|
||||
None,
|
||||
IoError(io::Error),
|
||||
Utf8Error(string::FromUtf8Error),
|
||||
RuntimeError,
|
||||
PoisonError,
|
||||
SendError,
|
||||
}
|
||||
|
||||
impl fmt::Display for OpErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
OpErrorKind::IoError(ref err) => write!(f, "I/O Error: {}", err),
|
||||
OpErrorKind::Utf8Error(ref err) => write!(f, "Utf8 Conversion: {}", err),
|
||||
ref err @ OpErrorKind::PoisonError => write!(f, "Threading Error: {}", err),
|
||||
ref err @ OpErrorKind::SendError => write!(f, "Sync: {}", err),
|
||||
ref err @ OpErrorKind::RuntimeError => write!(f, "RuntimeError: {}", err),
|
||||
OpErrorKind::None => write!(f, ""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for OpErrorKind {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match *self {
|
||||
OpErrorKind::IoError(ref err) => Some(err),
|
||||
OpErrorKind::Utf8Error(ref err) => Some(err),
|
||||
ref err @ OpErrorKind::PoisonError => Some(err),
|
||||
ref err @ OpErrorKind::SendError => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for OpError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
Self::new(OpErrorKind::IoError(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bitcoin::consensus::encode::Error> for OpError {
|
||||
fn from(_: bitcoin::consensus::encode::Error) -> Self {
|
||||
Self::from("block decode error")
|
||||
}
|
||||
}
|
||||
|
||||
impl convert::From<i32> for OpError {
|
||||
fn from(err_code: i32) -> Self {
|
||||
Self::from(io::Error::from_raw_os_error(err_code))
|
||||
}
|
||||
}
|
||||
|
||||
impl convert::From<&str> for OpError {
|
||||
fn from(err: &str) -> Self {
|
||||
Self::new(OpErrorKind::None).join_msg(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> convert::From<sync::PoisonError<T>> for OpError {
|
||||
fn from(_: sync::PoisonError<T>) -> Self {
|
||||
Self::new(OpErrorKind::PoisonError)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> convert::From<sync::mpsc::SendError<T>> for OpError {
|
||||
fn from(_: sync::mpsc::SendError<T>) -> Self {
|
||||
Self::new(OpErrorKind::SendError)
|
||||
}
|
||||
}
|
||||
|
||||
impl convert::From<string::FromUtf8Error> for OpError {
|
||||
fn from(err: string::FromUtf8Error) -> Self {
|
||||
Self::new(OpErrorKind::Utf8Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl convert::From<leveldb::error::Error> for OpError {
|
||||
fn from(err: leveldb::error::Error) -> Self {
|
||||
Self::from(err.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
172
parser/src/bitcoin/db/mod.rs
Normal file
172
parser/src/bitcoin/db/mod.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
//!
|
||||
//! Mostly a stripped down copy pasta of bitcoin-explorer
|
||||
//!
|
||||
//! Huge props to https://github.com/Congyuwang
|
||||
//!
|
||||
//! Crates APIs, essential structs, functions, methods are all here!
|
||||
//!
|
||||
//! To quickly understand how to use this crate, have a look at the
|
||||
//! documentation for `bitcoin_explorer::BitcoinDB`!!.
|
||||
//!
|
||||
|
||||
mod blk_files;
|
||||
mod block_iter;
|
||||
mod blocks_indexes;
|
||||
mod errors;
|
||||
mod reader;
|
||||
mod txdb;
|
||||
|
||||
use blk_files::*;
|
||||
use blocks_indexes::*;
|
||||
use errors::*;
|
||||
use reader::*;
|
||||
use txdb::*;
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bitcoin::{Block, Transaction, Txid};
|
||||
|
||||
pub use block_iter::BlockIter;
|
||||
|
||||
pub struct InnerDB {
|
||||
pub blocks_indexes: BlocksIndexes,
|
||||
pub blk_files: BlkFiles,
|
||||
pub tx_db: TxDB,
|
||||
}
|
||||
|
||||
///
|
||||
/// This is the main struct of this crate!! Click and read the doc.
|
||||
///
|
||||
/// All queries start from initializing `BitcoinDB`.
|
||||
///
|
||||
/// Note: This is an Arc wrap around `InnerDB`.
|
||||
///
|
||||
#[derive(Clone)]
|
||||
pub struct BitcoinDB(Arc<InnerDB>);
|
||||
|
||||
impl Deref for BitcoinDB {
|
||||
type Target = InnerDB;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl BitcoinDB {
|
||||
///
|
||||
/// This is the main structure for reading Bitcoin blockchain data.
|
||||
///
|
||||
/// Instantiating this class by passing the `-datadir` directory of
|
||||
/// Bitcoin core to the `new()` method.
|
||||
/// `tx_index`: whether to try to open tx_index levelDB.
|
||||
///
|
||||
pub fn new(p: &Path, tx_index: bool) -> OpResult<BitcoinDB> {
|
||||
if !p.exists() {
|
||||
return Err(OpError::from("data_dir does not exist"));
|
||||
}
|
||||
let blk_path = p.join("blocks");
|
||||
let index_path = blk_path.join("index");
|
||||
let blocks_indexes = BlocksIndexes::new(index_path.as_path())?;
|
||||
let tx_db = if tx_index {
|
||||
let tx_index_path = p.join("indexes").join("txindex");
|
||||
TxDB::new(&tx_index_path)
|
||||
} else {
|
||||
TxDB::null()
|
||||
};
|
||||
let inner = InnerDB {
|
||||
blocks_indexes,
|
||||
blk_files: BlkFiles::new(blk_path.as_path())?,
|
||||
tx_db,
|
||||
};
|
||||
Ok(BitcoinDB(Arc::new(inner)))
|
||||
}
|
||||
|
||||
///
|
||||
/// Get the maximum number of blocks downloaded.
|
||||
///
|
||||
/// This API guarantee that block 0 to `get_block_count() - 1`
|
||||
/// have been downloaded and available for query.
|
||||
///
|
||||
pub fn get_block_count(&self) -> usize {
|
||||
let records = self.blocks_indexes.len();
|
||||
for h in 0..records {
|
||||
// n_tx == 0 indicates that the block is not downloaded
|
||||
if self.blocks_indexes.get(h).unwrap().n_tx == 0 {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
records
|
||||
}
|
||||
|
||||
///
|
||||
/// Get a block
|
||||
///
|
||||
pub fn get_block(&self, height: usize) -> OpResult<Block> {
|
||||
if let Some(index) = self.blocks_indexes.get(height) {
|
||||
Ok(self.blk_files.read_block(index.n_file, index.n_data_pos)?)
|
||||
} else {
|
||||
Err(OpError::from("height not found"))
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Get a transaction by providing txid.
|
||||
///
|
||||
/// This function requires `txindex` to be set to `true` for `BitcoinDB`,
|
||||
/// and requires that flag `txindex=1` has been enabled when
|
||||
/// running Bitcoin Core.
|
||||
///
|
||||
/// A transaction cannot be found using this function if it is
|
||||
/// not yet indexed using `txindex`.
|
||||
///
|
||||
pub fn get_transaction(&self, txid: &Txid) -> OpResult<Transaction> {
|
||||
if !self.tx_db.is_open() {
|
||||
return Err(OpError::from("TxDB not open"));
|
||||
}
|
||||
|
||||
// give special treatment for genesis transaction
|
||||
if self.tx_db.is_genesis_tx(txid) {
|
||||
return Ok(self.get_block(0)?.txdata.swap_remove(0));
|
||||
}
|
||||
|
||||
let record = self.tx_db.get_tx_record(txid)?;
|
||||
|
||||
self.blk_files
|
||||
.read_transaction(record.n_file, record.n_pos, record.n_tx_offset)
|
||||
}
|
||||
|
||||
///
|
||||
/// Iterate through all blocks from `start` to `end` (excluded).
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// This iterator is implemented to read the blocks in concurrency,
|
||||
/// but the result is still produced in sequential order.
|
||||
/// Results read are stored in a synced queue for `next()`
|
||||
/// to get.
|
||||
///
|
||||
/// The iterator stops automatically when a block cannot be
|
||||
/// read (i.e., when the max height in the database met).
|
||||
///
|
||||
/// This is a very efficient implementation.
|
||||
/// Using SSD and intel core i7 (4 core, 8 threads)
|
||||
/// Iterating from height 0 to 700000 takes about 10 minutes.
|
||||
///
|
||||
pub fn iter_block(&self, start: usize, end: usize) -> BlockIter {
|
||||
BlockIter::from_range(self, start, end)
|
||||
}
|
||||
|
||||
pub fn check_if_txout_value_is_zero(&self, txid: &Txid, vout: usize) -> bool {
|
||||
self.get_transaction(txid)
|
||||
.unwrap()
|
||||
.output
|
||||
.get(vout)
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.value
|
||||
.to_sat()
|
||||
== 0
|
||||
}
|
||||
}
|
||||
90
parser/src/bitcoin/db/reader.rs
Normal file
90
parser/src/bitcoin/db/reader.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
||||
use bitcoin::{block::Header, consensus::Decodable, io::Cursor, Block, Transaction};
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
|
||||
use super::OpResult;
|
||||
|
||||
///
|
||||
/// binary file read utilities.
|
||||
///
|
||||
pub trait BlockchainRead {
|
||||
#[inline]
|
||||
fn read_varint(&mut self) -> OpResult<usize>
|
||||
where
|
||||
Self: bitcoin::io::Read,
|
||||
{
|
||||
let mut n = 0;
|
||||
loop {
|
||||
let ch_data = self.read_u8()?;
|
||||
n = (n << 7) | (ch_data & 0x7F) as usize;
|
||||
if ch_data & 0x80 > 0 {
|
||||
n += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_u8(&mut self) -> OpResult<u8>
|
||||
where
|
||||
Self: bitcoin::io::Read,
|
||||
{
|
||||
let mut slice = [0u8; 1];
|
||||
|
||||
self.read_exact(&mut slice).unwrap();
|
||||
|
||||
Ok(slice[0])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_u32(&mut self) -> OpResult<u32>
|
||||
where
|
||||
Self: std::io::Read,
|
||||
{
|
||||
let u = ReadBytesExt::read_u32::<LittleEndian>(self)?;
|
||||
|
||||
Ok(u)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_u8_vec(&mut self, count: u32) -> OpResult<Vec<u8>>
|
||||
where
|
||||
Self: bitcoin::io::Read,
|
||||
{
|
||||
let mut arr = vec![0u8; count as usize];
|
||||
|
||||
self.read_exact(&mut arr).unwrap();
|
||||
|
||||
Ok(arr)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_block(&mut self) -> OpResult<Block>
|
||||
where
|
||||
Self: bitcoin::io::BufRead,
|
||||
{
|
||||
Ok(Block::consensus_decode(self)?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_transaction(&mut self) -> OpResult<Transaction>
|
||||
where
|
||||
Self: bitcoin::io::BufRead,
|
||||
{
|
||||
Ok(Transaction::consensus_decode(self)?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_block_header(&mut self) -> OpResult<Header>
|
||||
where
|
||||
Self: bitcoin::io::BufRead,
|
||||
{
|
||||
Ok(Header::consensus_decode(self)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BlockchainRead for Cursor<T> {}
|
||||
impl BlockchainRead for BufReader<File> {}
|
||||
147
parser/src/bitcoin/db/txdb.rs
Normal file
147
parser/src/bitcoin/db/txdb.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use std::{path::Path, str::FromStr};
|
||||
|
||||
use bitcoin::{hashes::Hash, io::Cursor, Txid};
|
||||
use leveldb::{
|
||||
database::Database,
|
||||
kv::KV,
|
||||
options::{Options, ReadOptions},
|
||||
};
|
||||
|
||||
use crate::utils::log;
|
||||
|
||||
use super::{BlockchainRead, OpError, OpResult};
|
||||
|
||||
const GENESIS_TXID: &str = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
|
||||
|
||||
///
|
||||
/// tx-index: looking up transaction position using txid.
|
||||
///
|
||||
/// This is possible if Bitcoin Core has `txindex=1`.
|
||||
///
|
||||
pub struct TxDB {
|
||||
db: Option<Database<TxKey>>,
|
||||
genesis_txid: Txid,
|
||||
}
|
||||
|
||||
/// Records transaction storage on disk
|
||||
pub struct TransactionRecord {
|
||||
pub txid: Txid,
|
||||
pub n_file: i32,
|
||||
pub n_pos: u32,
|
||||
pub n_tx_offset: u32,
|
||||
}
|
||||
|
||||
impl TransactionRecord {
|
||||
fn from(key: &[u8], values: &[u8]) -> OpResult<Self> {
|
||||
let mut reader = Cursor::new(values);
|
||||
Ok(TransactionRecord {
|
||||
txid: Txid::from_slice(key).unwrap(),
|
||||
n_file: reader.read_varint()? as i32,
|
||||
n_pos: reader.read_varint()? as u32,
|
||||
n_tx_offset: reader.read_varint()? as u32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TxDB {
|
||||
/// initialize TxDB for transaction queries
|
||||
pub fn new(path: &Path) -> TxDB {
|
||||
let option_db = TxDB::try_open_db(path);
|
||||
if let Some(db) = option_db {
|
||||
TxDB {
|
||||
db: Some(db),
|
||||
genesis_txid: Txid::from_str(GENESIS_TXID).unwrap(),
|
||||
}
|
||||
} else {
|
||||
TxDB::null()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.db.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn null() -> TxDB {
|
||||
TxDB {
|
||||
db: None,
|
||||
genesis_txid: Txid::from_str(GENESIS_TXID).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
///
|
||||
/// genesis tx is not included in UTXO because of Bitcoin Core Bug
|
||||
///
|
||||
pub fn is_genesis_tx(&self, txid: &Txid) -> bool {
|
||||
txid == &self.genesis_txid
|
||||
}
|
||||
|
||||
fn try_open_db(path: &Path) -> Option<Database<TxKey>> {
|
||||
if !path.exists() {
|
||||
log("Failed to open tx_index DB: tx_index not built");
|
||||
|
||||
return None;
|
||||
}
|
||||
let options = Options::new();
|
||||
match Database::open(path, options) {
|
||||
Ok(db) => {
|
||||
log("Successfully opened tx_index DB!");
|
||||
|
||||
Some(db)
|
||||
}
|
||||
Err(e) => {
|
||||
log(&format!("Failed to open tx_index DB: {:?}", e));
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// note that this function cannot find genesis block, which needs special treatment
|
||||
pub fn get_tx_record(&self, txid: &Txid) -> OpResult<TransactionRecord> {
|
||||
if let Some(db) = &self.db {
|
||||
let inner = txid.as_byte_array();
|
||||
let mut key = Vec::with_capacity(inner.len() + 1);
|
||||
key.push(b't');
|
||||
key.extend(inner);
|
||||
let key = TxKey { key };
|
||||
let read_options = ReadOptions::new();
|
||||
match db.get(read_options, &key) {
|
||||
Ok(value) => {
|
||||
if let Some(value) = value {
|
||||
Ok(TransactionRecord::from(&key.key[1..], value.as_slice())?)
|
||||
} else {
|
||||
Err(OpError::from(
|
||||
format!("value not found for txid: {}", txid).as_str(),
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(OpError::from(
|
||||
format!("value not found for txid: {}", e).as_str(),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(OpError::from("TxDB not open"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// levelDB key utility
|
||||
struct TxKey {
|
||||
key: Vec<u8>,
|
||||
}
|
||||
|
||||
/// levelDB key utility
|
||||
impl db_key::Key for TxKey {
|
||||
fn from_u8(key: &[u8]) -> Self {
|
||||
TxKey {
|
||||
key: Vec::from(key),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_slice<T, F: Fn(&[u8]) -> T>(&self, f: F) -> T {
|
||||
f(&self.key)
|
||||
}
|
||||
}
|
||||
5
parser/src/bitcoin/height.rs
Normal file
5
parser/src/bitcoin/height.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use super::NUMBER_OF_UNSAFE_BLOCKS;
|
||||
|
||||
pub fn check_if_height_safe(height: usize, block_count: usize) -> bool {
|
||||
height < block_count - NUMBER_OF_UNSAFE_BLOCKS
|
||||
}
|
||||
11
parser/src/bitcoin/mod.rs
Normal file
11
parser/src/bitcoin/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod addresses;
|
||||
mod consts;
|
||||
mod daemon;
|
||||
mod db;
|
||||
mod height;
|
||||
|
||||
pub use addresses::*;
|
||||
pub use consts::*;
|
||||
pub use daemon::*;
|
||||
pub use db::*;
|
||||
pub use height::*;
|
||||
Reference in New Issue
Block a user