Skip to main content

blk_reader/
iterator.rs

1use std::path::PathBuf;
2
3use crate::error::BlkReaderError;
4use crate::index::{BlockIndexEntry, IndexReader};
5use crate::reader::BlkReader;
6
7/// Configuration for [`BlockIterator`].
8#[derive(Debug, Clone)]
9pub struct BlkReaderConfig {
10    /// Path to the `blocks/` directory (e.g. `~/.bitcoin/blocks`).
11    pub blocks_dir: PathBuf,
12    /// Path to the `blocks/index/` LevelDB directory.
13    pub index_dir: PathBuf,
14    /// First block height to yield (inclusive). Default: `0`.
15    pub start_height: u32,
16    /// Last block height to yield (exclusive). Default: `u32::MAX`.
17    pub end_height: u32,
18    /// I/O read buffer per open file in bytes. Default: 8 MiB.
19    pub read_buffer_bytes: usize,
20}
21
22impl Default for BlkReaderConfig {
23    fn default() -> Self {
24        Self {
25            blocks_dir: PathBuf::new(),
26            index_dir: PathBuf::new(),
27            start_height: 0,
28            end_height: u32::MAX,
29            read_buffer_bytes: 8 * 1024 * 1024,
30        }
31    }
32}
33
34/// A raw block returned by [`BlockIterator`].
35#[derive(Debug)]
36pub struct RawBlock {
37    /// Block height in the main chain.
38    pub height: u32,
39    /// Block hash in Bitcoin Core internal byte order (little-endian).
40    /// Reverse the bytes before displaying or comparing with block explorers.
41    pub hash: [u8; 32],
42    /// Raw serialized block bytes, without the 8-byte `magic + size` prefix.
43    pub data: Vec<u8>,
44}
45
46/// Height-ordered iterator over raw Bitcoin blocks.
47///
48/// Reads the LevelDB block index once at construction time, filters and sorts
49/// entries by height, then yields blocks in ascending order via the [`Iterator`]
50/// trait.
51///
52/// # Example
53///
54/// ```no_run
55/// use blk_reader::{BlockIterator, BlkReaderConfig};
56/// use std::path::PathBuf;
57///
58/// let config = BlkReaderConfig {
59///     blocks_dir: PathBuf::from("/home/user/.bitcoin/blocks"),
60///     index_dir:  PathBuf::from("/home/user/.bitcoin/blocks/index"),
61///     start_height: 800_000,
62///     end_height:   800_100,
63///     ..Default::default()
64/// };
65///
66/// for result in BlockIterator::new(config).unwrap() {
67///     let block = result.unwrap();
68///     println!("height={} size={}", block.height, block.data.len());
69/// }
70/// ```
71pub struct BlockIterator {
72    sorted_entries: Vec<BlockIndexEntry>,
73    cursor: usize,
74    reader: BlkReader,
75}
76
77impl BlockIterator {
78    /// Opens the LevelDB index and prepares the iterator.
79    ///
80    /// Reads all index entries, filters by `[start_height, end_height)`, and
81    /// sorts them by height. Returns an error if the index directory cannot be
82    /// opened or is corrupt.
83    pub fn new(config: BlkReaderConfig) -> Result<Self, BlkReaderError> {
84        let all_entries = IndexReader::read_all(&config.index_dir)?;
85
86        let mut filtered: Vec<BlockIndexEntry> = all_entries
87            .into_iter()
88            .filter(|e| {
89                e.is_valid_and_available()
90                    && e.height >= config.start_height
91                    && e.height < config.end_height
92            })
93            .collect();
94
95        filtered.sort_unstable_by_key(|e| e.height);
96
97        tracing::info!(
98            count = filtered.len(),
99            start_height = config.start_height,
100            end_height = config.end_height,
101            "BlockIterator: entries filtered and sorted"
102        );
103
104        let reader = BlkReader::new(config.blocks_dir, config.read_buffer_bytes);
105
106        Ok(Self {
107            sorted_entries: filtered,
108            cursor: 0,
109            reader,
110        })
111    }
112}
113
114impl Iterator for BlockIterator {
115    type Item = Result<RawBlock, BlkReaderError>;
116
117    fn next(&mut self) -> Option<Self::Item> {
118        if self.cursor >= self.sorted_entries.len() {
119            return None;
120        }
121
122        let entry = &self.sorted_entries[self.cursor];
123        self.cursor += 1;
124
125        let result = self.reader
126            .read_block_at(entry.n_file, entry.n_data_pos)
127            .map(|data| RawBlock {
128                height: entry.height,
129                hash: entry.hash,
130                data,
131            });
132
133        Some(result)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use crate::index::BlockIndexEntry;
140
141    fn make_entry(height: u32, status: u32, n_file: u32, n_data_pos: u32) -> BlockIndexEntry {
142        BlockIndexEntry {
143            hash: [0u8; 32],
144            height,
145            status,
146            n_file,
147            n_data_pos,
148        }
149    }
150
151    #[test]
152    fn test_filters_orphans() {
153        let orphan = make_entry(100, 0x00, 0, 0);
154        assert!(!orphan.is_valid_and_available());
155
156        let valid_no_data = make_entry(101, 0x04, 0, 0);
157        assert!(!valid_no_data.is_valid_and_available());
158
159        let valid = make_entry(102, 0x04 | 0x08, 0, 0);
160        assert!(valid.is_valid_and_available());
161    }
162
163    #[test]
164    fn test_filters_by_height_range() {
165        let entries = vec![
166            make_entry(0, 0x0C, 0, 0),
167            make_entry(5, 0x0C, 0, 0),
168            make_entry(10, 0x0C, 0, 0),
169            make_entry(15, 0x0C, 0, 0),
170        ];
171
172        let filtered: Vec<_> = entries
173            .iter()
174            .filter(|e| e.is_valid_and_available() && e.height >= 5 && e.height < 10)
175            .collect();
176
177        assert_eq!(filtered.len(), 1);
178        assert_eq!(filtered[0].height, 5);
179    }
180
181    #[test]
182    fn test_sorts_by_height() {
183        let mut entries = vec![
184            make_entry(300, 0x0C, 0, 0),
185            make_entry(100, 0x0C, 0, 0),
186            make_entry(200, 0x0C, 0, 0),
187        ];
188        entries.sort_unstable_by_key(|e| e.height);
189
190        assert_eq!(entries[0].height, 100);
191        assert_eq!(entries[1].height, 200);
192        assert_eq!(entries[2].height, 300);
193        for i in 1..entries.len() {
194            assert!(entries[i].height > entries[i - 1].height);
195        }
196    }
197}