Skip to main content

blk_reader/
reader.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::{BufReader, Read, Seek, SeekFrom};
4use std::path::PathBuf;
5
6use crate::error::BlkReaderError;
7
8/// Low-level reader for Bitcoin Core `blk*.dat` files.
9///
10/// Manages a pool of up to 8 open file handles (LRU eviction) and applies
11/// XOR decoding automatically when `blocks/xor.dat` is present.
12///
13/// For height-ordered iteration prefer [`BlockIterator`](crate::BlockIterator).
14pub struct BlkReader {
15    blocks_dir: PathBuf,
16    buffer_size: usize,
17    open_files: HashMap<u32, BufReader<File>>,
18    access_order: Vec<u32>,
19    /// XOR key for blk*.dat decoding (Bitcoin Core 28.0+).
20    /// Read from `blocks/xor.dat`. Empty = no XOR (Bitcoin Core < 28.0).
21    xor_key: [u8; 8],
22    xor_key_len: usize,
23}
24
25const MAX_OPEN_FILES: usize = 8;
26const MAINNET_MAGIC: u32 = 0xD9B4BEF9;
27
28impl BlkReader {
29    /// Creates a new reader for the given `blocks/` directory.
30    ///
31    /// `buffer_size` is the I/O read buffer per open file in bytes.
32    /// A value of `8 * 1024 * 1024` (8 MiB) works well for sequential reads.
33    /// XOR key detection from `xor.dat` happens here at construction time.
34    pub fn new(blocks_dir: PathBuf, buffer_size: usize) -> Self {
35        // Auto-detect XOR key from blocks/xor.dat (Bitcoin Core 28.0+).
36        // Older nodes do not have this file — in this case xor_key_len=0 (no XOR).
37        let (xor_key, xor_key_len) = Self::load_xor_key(&blocks_dir);
38        if xor_key_len > 0 {
39            tracing::info!(
40                key_hex = %hex::encode(&xor_key[..xor_key_len]),
41                "BlkReader: xor.dat detected — Bitcoin Core 28.0+ XOR encoding active"
42            );
43        }
44        Self {
45            blocks_dir,
46            buffer_size,
47            open_files: HashMap::new(),
48            access_order: Vec::new(),
49            xor_key,
50            xor_key_len,
51        }
52    }
53
54    /// Reads the XOR key from `blocks/xor.dat`. Returns key and length.
55    /// Bitcoin Core 28.0+ creates this file with 8 random bytes.
56    fn load_xor_key(blocks_dir: &std::path::Path) -> ([u8; 8], usize) {
57        let xor_path = blocks_dir.join("xor.dat");
58        match std::fs::read(&xor_path) {
59            Ok(data) if !data.is_empty() => {
60                let len = data.len().min(8);
61                let mut key = [0u8; 8];
62                key[..len].copy_from_slice(&data[..len]);
63                (key, len)
64            }
65            _ => ([0u8; 8], 0),
66        }
67    }
68
69    /// Reads the raw serialized block at a given file index and byte offset.
70    ///
71    /// `n_file` corresponds to `blk{n_file:05}.dat` (e.g. `0` → `blk00000.dat`).
72    /// `n_data_pos` is the offset of the block data as stored in the LevelDB index
73    /// (`CDiskBlockIndex::nDataPos`). The 8 bytes immediately before it contain the
74    /// network magic and block size.
75    ///
76    /// Returns the raw block bytes without any header prefix. Pass the result to
77    /// `bitcoin::consensus::deserialize` or similar to decode transactions.
78    ///
79    /// # Errors
80    ///
81    /// Returns [`BlkReaderError`] if the file is missing, the magic bytes don't
82    /// match mainnet, or an I/O error occurs.
83    pub fn read_block_at(&mut self, n_file: u32, n_data_pos: u32) -> Result<Vec<u8>, BlkReaderError> {
84        // CDiskBlockIndex nDataPos = offset of the start of serialized block data.
85        // The 8 bytes BEFORE contain: [magic: 4 bytes LE][block_size: 4 bytes LE].
86        // Bitcoin Core 28.0+ XOR-encodes the entire file — we decode here.
87        if n_data_pos < 8 {
88            return Err(BlkReaderError::InvalidMagicBytes {
89                file: n_file,
90                offset: n_data_pos as u64,
91                got: 0,
92            });
93        }
94        let header_pos = (n_data_pos - 8) as u64;
95
96        // Copy the XOR key to local variables BEFORE getting the reader
97        // (prevents borrow conflict: reader &mut borrow vs xor_key & borrow)
98        let xor_key = self.xor_key;
99        let xor_key_len = self.xor_key_len;
100
101        let reader = self.get_or_open(n_file)?;
102        reader.seek(SeekFrom::Start(header_pos))?;
103
104        let mut magic_buf = [0u8; 4];
105        reader.read_exact(&mut magic_buf)?;
106        // Decode XOR using local copy of the key
107        if xor_key_len > 0 {
108            for (i, byte) in magic_buf.iter_mut().enumerate() {
109                *byte ^= xor_key[(header_pos as usize + i) % xor_key_len];
110            }
111        }
112        let magic = u32::from_le_bytes(magic_buf);
113
114        if magic != MAINNET_MAGIC {
115            // Diagnostic: read 32 bytes at data offset to facilitate debugging
116            let mut diag_buf = [0u8; 32];
117            reader.seek(SeekFrom::Start(n_data_pos as u64))?;
118            let _ = reader.read_exact(&mut diag_buf);
119            if xor_key_len > 0 {
120                for (i, byte) in diag_buf.iter_mut().enumerate() {
121                    *byte ^= xor_key[(n_data_pos as usize + i) % xor_key_len];
122                }
123            }
124            tracing::error!(
125                file = n_file,
126                offset = n_data_pos,
127                hex = %hex::encode(diag_buf),
128                "ERROR: Magic bytes not found. Content at original offset above."
129            );
130            return Err(BlkReaderError::InvalidMagicBytes {
131                file: n_file,
132                offset: n_data_pos as u64,
133                got: magic,
134            });
135        }
136
137        // Read and decode the block size (4 bytes after the magic)
138        let mut size_buf = [0u8; 4];
139        reader.read_exact(&mut size_buf)?;
140        if xor_key_len > 0 {
141            let size_pos = header_pos as usize + 4;
142            for (i, byte) in size_buf.iter_mut().enumerate() {
143                *byte ^= xor_key[(size_pos + i) % xor_key_len];
144            }
145        }
146        let block_size = u32::from_le_bytes(size_buf) as usize;
147
148        // Read and decode the block data
149        let mut block_data = vec![0u8; block_size];
150        reader.read_exact(&mut block_data)?;
151        if xor_key_len > 0 {
152            let data_pos = n_data_pos as usize;
153            for (i, byte) in block_data.iter_mut().enumerate() {
154                *byte ^= xor_key[(data_pos + i) % xor_key_len];
155            }
156        }
157
158        if let Some(pos) = self.access_order.iter().position(|&f| f == n_file) {
159            self.access_order.remove(pos);
160        }
161        self.access_order.push(n_file);
162
163        Ok(block_data)
164    }
165
166    fn get_or_open(&mut self, n_file: u32) -> Result<&mut BufReader<File>, BlkReaderError> {
167        if !self.open_files.contains_key(&n_file) {
168            if self.open_files.len() >= MAX_OPEN_FILES {
169                if let Some(oldest) = self.access_order.first().copied() {
170                    self.open_files.remove(&oldest);
171                    self.access_order.retain(|&f| f != oldest);
172                }
173            }
174
175            let path = self.blocks_dir.join(format!("blk{n_file:05}.dat"));
176            if !path.exists() {
177                return Err(BlkReaderError::BlkFileNotFound { index: n_file });
178            }
179
180            let file = File::open(&path)?;
181            let reader = BufReader::with_capacity(self.buffer_size, file);
182            self.open_files.insert(n_file, reader);
183            self.access_order.push(n_file);
184        }
185
186        Ok(self.open_files.get_mut(&n_file).unwrap())
187    }
188}