brk: first commit

This commit is contained in:
nym21
2025-02-23 01:25:15 +01:00
parent 8c3f519016
commit 19cf34f9d4
266 changed files with 225 additions and 1268 deletions
@@ -0,0 +1,46 @@
use std::{
collections::BTreeMap,
fs,
path::{Path, PathBuf},
};
use derive_deref::{Deref, DerefMut};
const BLK: &str = "blk";
const DAT: &str = ".dat";
#[derive(Debug, Deref, DerefMut)]
pub struct BlkIndexToBlkPath(BTreeMap<u16, PathBuf>);
impl BlkIndexToBlkPath {
pub fn scan(data_dir: &Path) -> Self {
let blocks_dir = data_dir.join("blocks");
Self(
fs::read_dir(blocks_dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| {
let is_file = path.is_file();
if is_file {
let file_name = path.file_name().unwrap().to_str().unwrap();
file_name.starts_with(BLK) && file_name.ends_with(DAT)
} else {
false
}
})
.map(|path| {
let file_name = path.file_name().unwrap().to_str().unwrap();
let blk_index = file_name[BLK.len()..(file_name.len() - DAT.len())]
.parse::<u16>()
.unwrap();
(blk_index, path)
})
.collect::<BTreeMap<_, _>>(),
)
}
}
@@ -0,0 +1,95 @@
use std::{
collections::{BTreeMap, BTreeSet},
fs::File,
io::{BufReader, BufWriter},
path::{Path, PathBuf},
};
use crate::{BlkIndexToBlkPath, Height, blk_recap::BlkRecap};
#[derive(Debug)]
pub struct BlkIndexToBlkRecap {
pub path: PathBuf,
pub tree: BTreeMap<u16, BlkRecap>,
}
impl BlkIndexToBlkRecap {
pub fn import(data_dir: &Path, blk_index_to_blk_path: &BlkIndexToBlkPath, start: Option<Height>) -> (Self, u16) {
let path = data_dir.join("blk_index_to_blk_recap.json");
let tree = {
if let Ok(file) = File::open(&path) {
let reader = BufReader::new(file);
serde_json::from_reader(reader).unwrap_or_default()
} else {
BTreeMap::default()
}
};
let mut slf = Self { path, tree };
let min_removed = slf.clean_outdated(blk_index_to_blk_path);
let blk_index = slf.get_start_recap(min_removed, start);
(slf, blk_index)
}
fn clean_outdated(&mut self, blk_index_to_blk_path: &BlkIndexToBlkPath) -> Option<u16> {
let mut min_removed_blk_index: Option<u16> = None;
let mut unprocessed_keys = self.tree.keys().copied().collect::<BTreeSet<_>>();
blk_index_to_blk_path.iter().for_each(|(blk_index, blk_path)| {
unprocessed_keys.remove(blk_index);
if let Some(blk_recap) = self.tree.get(blk_index) {
if blk_recap.has_different_modified_time(blk_path) {
self.tree.remove(blk_index).unwrap();
if min_removed_blk_index.is_none_or(|_blk_index| *blk_index < _blk_index) {
min_removed_blk_index.replace(*blk_index);
}
}
}
});
unprocessed_keys.into_iter().for_each(|blk_index| {
self.tree.remove(&blk_index).unwrap();
if min_removed_blk_index.is_none_or(|_blk_index| blk_index < _blk_index) {
min_removed_blk_index.replace(blk_index);
}
});
min_removed_blk_index
}
pub fn get_start_recap(&mut self, min_removed: Option<u16>, start: Option<Height>) -> u16 {
if start.is_none() {
return 0;
}
let height = start.unwrap();
let mut start = 0;
if let Some(found) = self.tree.iter().find(|(_, recap)| recap.max_height >= height) {
start = *found.0;
}
if let Some(min_removed) = min_removed {
if start > min_removed {
start = min_removed;
}
}
start
}
pub fn export(&self) {
let file = File::create(&self.path).unwrap_or_else(|_| {
dbg!(&self.path);
panic!("No such file or directory")
});
serde_json::to_writer(&mut BufWriter::new(file), &self.tree).unwrap();
}
}
+18
View File
@@ -0,0 +1,18 @@
use std::path::Path;
use crate::path_to_modified_time;
#[derive(Debug, Clone, Copy)]
pub struct BlkMetadata {
pub index: u16,
pub modified_time: u64,
}
impl BlkMetadata {
pub fn new(index: u16, path: &Path) -> Self {
Self {
index,
modified_time: path_to_modified_time(path),
}
}
}
@@ -0,0 +1,15 @@
use bitcoin::Block;
use crate::BlkMetadata;
#[derive(Debug)]
pub struct BlkIndexAndBlock {
pub blk_metadata: BlkMetadata,
pub block: Block,
}
impl BlkIndexAndBlock {
pub fn new(blk_metadata: BlkMetadata, block: Block) -> Self {
Self { blk_metadata, block }
}
}
+21
View File
@@ -0,0 +1,21 @@
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::{path_to_modified_time, Height};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[repr(C)]
pub struct BlkRecap {
pub max_height: Height,
pub modified_time: u64,
}
impl BlkRecap {
pub fn has_different_modified_time(&self, blk_path: &Path) -> bool {
if self.modified_time != path_to_modified_time(blk_path) {
dbg!(self.modified_time, path_to_modified_time(blk_path));
}
self.modified_time != path_to_modified_time(blk_path)
}
}
+36
View File
@@ -0,0 +1,36 @@
use std::{
fmt::{self, Debug},
io,
};
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub enum Error {
IO(io::Error),
ZeroCopyError,
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::IO(value)
}
}
#[cfg(feature = "bytes")]
impl<A, B> From<zerocopy::error::SizeError<A, B>> for Error {
fn from(_: zerocopy::error::SizeError<A, B>) -> Self {
Self::ZeroCopyError
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::IO(error) => Debug::fmt(&error, f),
Error::ZeroCopyError => write!(f, "Zero copy convert error"),
}
}
}
impl std::error::Error for Error {}
+186
View File
@@ -0,0 +1,186 @@
use std::{
fmt::{self, Debug},
ops::{Add, AddAssign, Rem, Sub},
};
use derive_deref::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
#[cfg(feature = "bytes")]
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::rpc::{self, RpcApi};
#[derive(Debug, Clone, Copy, Deref, DerefMut, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "bytes", derive(FromBytes, Immutable, IntoBytes, KnownLayout,))]
pub struct Height(u32);
impl Height {
pub const ZERO: Self = Height(0);
pub const MAX: Self = Height(u32::MAX);
#[cfg(feature = "bytes")]
pub fn write(&self, path: &std::path::Path) -> Result<(), std::io::Error> {
std::fs::write(path, self.as_bytes())
}
pub fn increment(&mut self) {
self.0 += 1;
}
pub fn incremented(self) -> Self {
Self(self.0 + 1)
}
pub fn decrement(&mut self) {
self.0 -= 1;
}
pub fn decremented(self) -> Self {
Self(self.0.checked_sub(1).unwrap_or_default())
}
pub fn is_zero(self) -> bool {
self == Self::ZERO
}
}
impl PartialEq<u64> for Height {
fn eq(&self, other: &u64) -> bool {
**self == *other as u32
}
}
impl Add<Height> for Height {
type Output = Height;
fn add(self, rhs: Height) -> Self::Output {
Self::from(self.0 + rhs.0)
}
}
impl Add<u32> for Height {
type Output = Height;
fn add(self, rhs: u32) -> Self::Output {
Self::from(self.0 + rhs)
}
}
impl Add<usize> for Height {
type Output = Height;
fn add(self, rhs: usize) -> Self::Output {
Self::from(*self + rhs as u32)
}
}
impl Sub<Height> for Height {
type Output = Height;
fn sub(self, rhs: Height) -> Self::Output {
Self::from(*self - *rhs)
}
}
impl Sub<i32> for Height {
type Output = Height;
fn sub(self, rhs: i32) -> Self::Output {
Self::from(*self - rhs as u32)
}
}
impl Sub<u32> for Height {
type Output = Height;
fn sub(self, rhs: u32) -> Self::Output {
Self::from(*self - rhs)
}
}
impl Sub<usize> for Height {
type Output = Height;
fn sub(self, rhs: usize) -> Self::Output {
Self::from(*self - rhs as u32)
}
}
impl AddAssign<usize> for Height {
fn add_assign(&mut self, rhs: usize) {
*self = self.add(rhs);
}
}
impl Rem<usize> for Height {
type Output = Height;
fn rem(self, rhs: usize) -> Self::Output {
Self(self.abs_diff(Height::from(rhs).0))
}
}
impl fmt::Display for Height {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", **self)
}
}
impl From<u32> for Height {
fn from(value: u32) -> Self {
Self(value)
}
}
impl From<usize> for Height {
fn from(value: usize) -> Self {
Self(value as u32)
}
}
impl From<Height> for usize {
fn from(value: Height) -> Self {
value.0 as usize
}
}
impl From<Height> for u64 {
fn from(value: Height) -> Self {
value.0 as u64
}
}
impl TryFrom<&rpc::Client> for Height {
type Error = rpc::Error;
fn try_from(value: &rpc::Client) -> Result<Self, Self::Error> {
Ok((value.get_blockchain_info()?.blocks as usize - 1).into())
}
}
impl From<bitcoin::locktime::absolute::Height> for Height {
fn from(value: bitcoin::locktime::absolute::Height) -> Self {
Self(value.to_consensus_u32())
}
}
impl From<Height> for bitcoin::locktime::absolute::Height {
fn from(value: Height) -> Self {
bitcoin::locktime::absolute::Height::from_consensus(*value).unwrap()
}
}
#[cfg(feature = "bytes")]
impl TryFrom<&std::path::Path> for Height {
type Error = crate::Error;
fn try_from(value: &std::path::Path) -> Result<Self, Self::Error> {
Ok(Self::read_from_bytes(std::fs::read(value)?.as_slice())?.to_owned())
}
}
#[cfg(feature = "bytes")]
impl TryFrom<fjall::Slice> for Height {
type Error = crate::Error;
fn try_from(value: fjall::Slice) -> Result<Self, Self::Error> {
Ok(Self::read_from_bytes(&value)?)
}
}
#[cfg(feature = "bytes")]
impl From<Height> for fjall::Slice {
fn from(value: Height) -> Self {
Self::new(value.as_bytes())
}
}
+264
View File
@@ -0,0 +1,264 @@
use std::{cmp::Ordering, collections::BTreeMap, fs, ops::ControlFlow, path::Path, thread};
use bitcoin::{
Block, BlockHash,
consensus::{Decodable, ReadExt},
io::{Cursor, Read},
};
use bitcoincore_rpc::RpcApi;
use blk_index_to_blk_path::*;
use blk_recap::BlkRecap;
use crossbeam::channel::{Receiver, bounded};
use rayon::prelude::*;
pub use bitcoin;
pub use bitcoincore_rpc as rpc;
mod blk_index_to_blk_path;
mod blk_index_to_blk_recap;
mod blk_metadata;
mod blk_metadata_and_block;
mod blk_recap;
mod error;
mod height;
mod utils;
mod xor;
use blk_index_to_blk_recap::*;
use blk_metadata::*;
use blk_metadata_and_block::*;
pub use error::*;
pub use height::*;
use utils::*;
use xor::*;
pub const NUMBER_OF_UNSAFE_BLOCKS: usize = 1000;
const MAGIC_BYTES: [u8; 4] = [249, 190, 180, 217];
const BOUND_CAP: usize = 100;
///
/// Returns a crossbeam channel receiver that receives `(Height, Block, BlockHash)` tuples from an **inclusive** range (`start` and `end`)
///
/// For an example checkout `iterator/main.rs`
///
pub fn new(
data_dir: &Path,
start: Option<Height>,
end: Option<Height>,
rpc: &'static bitcoincore_rpc::Client,
) -> Receiver<(Height, Block, BlockHash)> {
let (send_block_reader, recv_block_reader) = bounded(5);
let (send_block_xor, recv_block_xor) = bounded(BOUND_CAP);
let (send_block, recv_block) = bounded(BOUND_CAP);
let (send_height_block_hash, recv_height_block_hash) = bounded(BOUND_CAP);
let blk_index_to_blk_path = BlkIndexToBlkPath::scan(data_dir);
let (mut blk_index_to_blk_recap, blk_index) = BlkIndexToBlkRecap::import(data_dir, &blk_index_to_blk_path, start);
let xor = XOR::from(data_dir);
thread::spawn(move || {
blk_index_to_blk_path
.range(blk_index..)
.try_for_each(move |(blk_index, blk_path)| {
let blk_index = *blk_index;
let blk_metadata = BlkMetadata::new(blk_index, blk_path.as_path());
let blk_bytes = fs::read(blk_path).unwrap();
let res = send_block_reader.send((blk_metadata, blk_bytes));
if let Err(e) = res {
dbg!(e);
return ControlFlow::Break(());
}
ControlFlow::Continue(())
});
});
thread::spawn(move || {
recv_block_reader
.iter()
.try_for_each(|(blk_metadata, blk_bytes)| -> ControlFlow<(), _> {
let blk_bytes = xor.process(blk_bytes);
let blk_bytes_len = blk_bytes.len() as u64;
let mut cursor = Cursor::new(blk_bytes);
let mut current_4bytes = [0; 4];
'parent: loop {
if cursor.position() == blk_bytes_len {
break;
}
// Read until we find a valid suite of MAGIC_BYTES
loop {
current_4bytes.rotate_left(1);
if let Ok(byte) = cursor.read_u8() {
current_4bytes[3] = byte;
} else {
break 'parent;
}
if current_4bytes == MAGIC_BYTES {
break;
}
}
let len = cursor.read_u32().unwrap();
let mut bytes = vec![0u8; len as usize];
cursor.read_exact(&mut bytes).unwrap();
if send_block_xor.send((blk_metadata, BlockState::Raw(bytes))).is_err() {
return ControlFlow::Break(());
}
}
ControlFlow::Continue(())
});
});
thread::spawn(move || {
let mut bulk = vec![];
let drain_and_send = |bulk: &mut Vec<_>| {
// Using a vec and sending after to not end up with stuck threads in par iter
bulk.par_iter_mut().for_each(|(_, block_state)| {
BlockState::decode(block_state);
});
bulk.drain(..).try_for_each(|(blk_metadata, block_state)| {
let block = match block_state {
BlockState::Decoded(block) => block,
_ => unreachable!(),
};
if send_block.send(BlkIndexAndBlock::new(blk_metadata, block)).is_err() {
return ControlFlow::Break(());
}
ControlFlow::Continue(())
})
};
recv_block_xor.iter().try_for_each(|tuple| {
bulk.push(tuple);
if bulk.len() < BOUND_CAP / 2 {
return ControlFlow::Continue(());
}
// Sending in bulk to not lock threads in standby
drain_and_send(&mut bulk)
});
drain_and_send(&mut bulk)
});
thread::spawn(move || {
let mut current_height = start.unwrap_or_default();
let mut future_blocks = BTreeMap::default();
recv_block.iter().try_for_each(|tuple| -> ControlFlow<(), _> {
let blk_metadata = tuple.blk_metadata;
let block = tuple.block;
let hash = block.block_hash();
let header = rpc.get_block_header_info(&hash);
if header.is_err() {
return ControlFlow::Continue(());
}
let header = header.unwrap();
if header.confirmations <= 0 {
return ControlFlow::Continue(());
}
let height = Height::from(header.height);
let len = blk_index_to_blk_recap.tree.len();
if blk_metadata.index == len as u16 || blk_metadata.index + 1 == len as u16 {
match (len as u16).cmp(&blk_metadata.index) {
Ordering::Equal => {
if len % 21 == 0 {
blk_index_to_blk_recap.export();
}
}
Ordering::Less => panic!(),
Ordering::Greater => {}
}
blk_index_to_blk_recap
.tree
.entry(blk_metadata.index)
.and_modify(|recap| {
if recap.max_height < height {
recap.max_height = height;
}
})
.or_insert(BlkRecap {
max_height: height,
modified_time: blk_metadata.modified_time,
});
}
let mut opt = if current_height == height {
Some((block, hash))
} else {
if start.is_none_or(|start| start <= height) && end.is_none_or(|end| end >= height) {
future_blocks.insert(height, (block, hash));
}
None
};
while let Some((block, hash)) = opt.take().or_else(|| {
if !future_blocks.is_empty() {
future_blocks.remove(&current_height)
} else {
None
}
}) {
send_height_block_hash.send((current_height, block, hash)).unwrap();
if end == Some(current_height) {
return ControlFlow::Break(());
}
current_height.increment();
}
ControlFlow::Continue(())
});
blk_index_to_blk_recap.export();
});
recv_height_block_hash
}
enum BlockState {
Raw(Vec<u8>),
Decoded(Block),
}
impl BlockState {
pub fn decode(&mut self) {
let bytes = match self {
BlockState::Raw(bytes) => bytes,
_ => unreachable!(),
};
let mut cursor = Cursor::new(bytes);
let block = Block::consensus_decode(&mut cursor).unwrap();
*self = BlockState::Decoded(block);
}
}
+27
View File
@@ -0,0 +1,27 @@
use std::path::Path;
use bitcoincore_rpc::{Auth, Client};
fn main() {
let i = std::time::Instant::now();
let data_dir = Path::new("../../bitcoin");
let rpc = Box::leak(Box::new(
Client::new(
"http://localhost:8332",
Auth::CookieFile(Path::new(data_dir).join(".cookie")),
)
.unwrap(),
));
let start = None;
let end = None;
brk_parser::new(data_dir, start, end, rpc)
.iter()
.for_each(|(height, _block, hash)| {
println!("{height}: {hash}");
});
dbg!(i.elapsed());
}
+11
View File
@@ -0,0 +1,11 @@
use std::{fs, path::Path, time::UNIX_EPOCH};
pub fn path_to_modified_time(path: &Path) -> u64 {
fs::metadata(path)
.unwrap()
.modified()
.unwrap()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
+40
View File
@@ -0,0 +1,40 @@
use std::{fs, path::Path};
const XOR_LEN: usize = 8;
#[derive(Debug, PartialEq, Eq, Default)]
pub struct XOR([u8; XOR_LEN]);
impl XOR {
pub fn process(&self, mut bytes: Vec<u8>) -> Vec<u8> {
if u64::from_ne_bytes(self.0) == 0 {
return bytes;
}
let len = bytes.len();
let mut bytes_index = 0;
let mut xor_index = 0;
while bytes_index < len {
bytes[bytes_index] ^= self.0[xor_index];
bytes_index += 1;
xor_index += 1;
if xor_index == XOR_LEN {
xor_index = 0;
}
}
bytes
}
}
impl From<&Path> for XOR {
fn from(value: &Path) -> Self {
Self(
fs::read(value.join("blocks/xor.dat"))
.unwrap_or(vec![0; 8])
.try_into()
.unwrap(),
)
}
}