blk_reader/index.rs
1use std::path::Path;
2use crate::error::BlkReaderError;
3use crate::varint::read_varint;
4
5/// Represents a CDiskBlockIndex entry from Bitcoin Core.
6#[derive(Debug, Clone)]
7pub struct BlockIndexEntry {
8 /// Block hash (32 bytes, Bitcoin Core internal format — little-endian)
9 pub hash: [u8; 32],
10 /// Block height in the chain
11 pub height: u32,
12 /// Bitcoin Core status field:
13 /// - bits 0–2: validity level (0=unknown, 1=header, 2=tree, 3=transactions,
14 /// 4=chain/BLOCK_VALID_CHAIN, 5=scripts/BLOCK_VALID_SCRIPTS)
15 /// - bit 3 (0x08): BLOCK_HAVE_DATA — block data is in blk*.dat
16 /// - bit 4 (0x10): BLOCK_HAVE_UNDO — undo data is in rev*.dat
17 pub status: u32,
18 /// blk*.dat file index (0 → blk00000.dat)
19 pub n_file: u32,
20 /// Byte offset within the blk{n_file:05}.dat file where the block starts
21 pub n_data_pos: u32,
22}
23
24impl BlockIndexEntry {
25 /// Returns true if the block is validated in the chain and the data is available on disk.
26 ///
27 /// Requires:
28 /// - Validity level ≥ 4 (BLOCK_VALID_CHAIN): bits 0–2 ≥ 4
29 /// - BLOCK_HAVE_DATA (bit 3): data present in blk*.dat
30 ///
31 /// Note: using `status & 0x04` would be incorrect — it would only check bit 2, which is
32 /// true for any level ≥ 4 (binary ...1xx), but also for level 6+ or
33 /// invalid combinations. The correct way isolates the 3 validity bits with `& 0x07`.
34 pub fn is_valid_and_available(&self) -> bool {
35 (self.status & 0x07) >= 4 && (self.status & 0x08 != 0)
36 }
37
38 /// Returns the block's validity level (bits 0–2 of status).
39 pub fn validity_level(&self) -> u32 {
40 self.status & 0x07
41 }
42}
43
44pub struct IndexReader;
45
46impl IndexReader {
47 /// Reads all LevelDB index entries from Bitcoin Core in `index_dir`.
48 ///
49 /// If opening fails due to a lock (common if Bitcoin Core is running),
50 /// attempts to create a temporary copy of index files for reading.
51 pub fn read_all(index_dir: &Path) -> Result<Vec<BlockIndexEntry>, BlkReaderError> {
52 use rusty_leveldb::{DB, Options};
53
54 let path_str = index_dir.to_str().ok_or_else(|| BlkReaderError::LevelDbOpen {
55 path: index_dir.to_path_buf(),
56 reason: "Path is not valid UTF-8".to_string(),
57 })?;
58
59 let options = Options { create_if_missing: false, ..Options::default() };
60
61 // Try to open the original database
62 let db_result = DB::open(path_str, options.clone());
63
64 let db = match db_result {
65 Ok(db) => db,
66 Err(e) => {
67 let err_msg = e.to_string();
68 // On Windows, the lock error is typically "process cannot access the file"
69 if err_msg.contains("process cannot access") || err_msg.contains("lock") {
70 tracing::info!(
71 path = %index_dir.display(),
72 "LevelDB index locked by Bitcoin Core. Attempting reading via temporary Shadow Copy..."
73 );
74
75 // Use LOCALAPPDATA as base for the temporary directory.
76 // std::env::temp_dir() might resolve to virtual paths (e.g. MobaXterm /tmp)
77 // which are not real Windows paths. LOCALAPPDATA is always a native path.
78 let base_tmp = std::env::var("LOCALAPPDATA")
79 .map(std::path::PathBuf::from)
80 .unwrap_or_else(|_| std::env::temp_dir());
81 let temp_dir = base_tmp.join("Temp").join(format!("semantiq_index_copy_{}", uuid::Uuid::new_v4()));
82 std::fs::create_dir_all(&temp_dir).map_err(|io_err| BlkReaderError::LevelDbOpen {
83 path: temp_dir.clone(),
84 reason: format!("Failed to create temporary directory: {io_err}"),
85 })?;
86
87 // Copy relevant files (ldb, log, CURRENT, MANIFEST)
88 // Note: We do not copy the LOCK file
89 //
90 // MANIFEST strategy: Bitcoin Core keeps the MANIFEST with an exclusive lock
91 // permanently (not only during compaction). If duplication fails, we generate
92 // a synthetic MANIFEST from the already copied .ldb — LevelDB level-0 includes
93 // all files in the iterator regardless of key ranges, thus full iteration
94 // works correctly with a synthetic MANIFEST.
95 let mut manifest_needs_synthesis = false;
96
97 if let Ok(entries) = std::fs::read_dir(index_dir) {
98 for entry in entries.flatten() {
99 let file_type = entry.file_type().map(|t| t.is_file()).unwrap_or(false);
100 let file_name = entry.file_name();
101 let file_name_str = file_name.to_string_lossy().to_string();
102
103 if file_type && !file_name_str.contains("LOCK") {
104 let dest_path = temp_dir.join(&file_name);
105 let copy_res = Self::copy_file_shared(&entry.path(), &dest_path);
106
107 match copy_res {
108 Ok(_) => {
109 tracing::debug!(file = %file_name_str, "Index file copied successfully");
110 }
111 Err(copy_err) if file_name_str.starts_with("MANIFEST") => {
112 // MANIFEST permanently locked — will be synthesized after the loop
113 tracing::info!(
114 file = %file_name_str,
115 error = %copy_err,
116 "MANIFEST locked by Bitcoin Core — synthetic MANIFEST will be generated"
117 );
118 manifest_needs_synthesis = true;
119 }
120 Err(copy_err) if file_name_str == "CURRENT" => {
121 // CURRENT can also be synthesized along with the MANIFEST
122 tracing::debug!(file = %file_name_str, error = %copy_err, "CURRENT locked — will be generated along with synthetic MANIFEST");
123 manifest_needs_synthesis = true;
124 }
125 Err(copy_err) => {
126 tracing::debug!(file = %file_name_str, error = %copy_err, "Ignoring non-critical locked index file");
127 }
128 }
129 }
130 }
131 }
132
133 // Synthesize MANIFEST + CURRENT if necessary
134 if manifest_needs_synthesis {
135 if let Err(e) = Self::write_synthetic_manifest(&temp_dir) {
136 let _ = std::fs::remove_dir_all(&temp_dir);
137 return Err(BlkReaderError::LevelDbOpen {
138 path: temp_dir.clone(),
139 reason: format!("Failed to generate synthetic MANIFEST: {e}"),
140 });
141 }
142 tracing::info!("Shadow copy: synthetic MANIFEST generated successfully");
143 }
144
145 let temp_path_str = temp_dir.to_str().unwrap();
146 let temp_db = DB::open(temp_path_str, options).map_err(|e2| BlkReaderError::LevelDbOpen {
147 path: temp_dir.clone(),
148 reason: format!("Failed to open temporary LevelDB copy: {e2}"),
149 })?;
150
151 // Schedule cleanup of the temporary directory after reading
152 // Since we don't have a guaranteed destructor here, the ideal is to read and then delete.
153 // We will read everything and clean up at the end of this function.
154 let result = Self::read_from_db(temp_db);
155
156 // Cleanup
157 let _ = std::fs::remove_dir_all(&temp_dir);
158
159 return result;
160 } else {
161 return Err(BlkReaderError::LevelDbOpen {
162 path: index_dir.to_path_buf(),
163 reason: err_msg,
164 });
165 }
166 }
167 };
168
169 Self::read_from_db(db)
170 }
171
172 /// Copies a file using shared access mode on Windows.
173 ///
174 /// On Windows, Bitcoin Core keeps files like MANIFEST and .log open with
175 /// an exclusive lock. Standard Rust `std::fs::File::open()` uses `FILE_SHARE_READ`
176 /// but not `FILE_SHARE_WRITE`, causing the open to fail with `os error 32`.
177 ///
178 /// This function uses `FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE` (0x07)
179 /// via `OpenOptionsExt::share_mode()`, allowing reading the file even with another
180 /// process keeping it open for writing.
181 fn copy_file_shared(src_path: &std::path::Path, dst_path: &std::path::Path) -> std::io::Result<u64> {
182 #[cfg(target_os = "windows")]
183 {
184 use std::os::windows::fs::OpenOptionsExt;
185 // FILE_SHARE_READ (0x01) | FILE_SHARE_WRITE (0x02) | FILE_SHARE_DELETE (0x04)
186 let mut src = std::fs::OpenOptions::new()
187 .read(true)
188 .share_mode(0x01 | 0x02 | 0x04)
189 .open(src_path)?;
190 let mut dst = std::fs::File::create(dst_path)?;
191 std::io::copy(&mut src, &mut dst)
192 }
193
194 #[cfg(not(target_os = "windows"))]
195 {
196 let mut src = std::fs::File::open(src_path)?;
197 let mut dst = std::fs::File::create(dst_path)?;
198 std::io::copy(&mut src, &mut dst)
199 }
200 }
201
202 fn read_from_db(mut db: rusty_leveldb::DB) -> Result<Vec<BlockIndexEntry>, BlkReaderError> {
203 use rusty_leveldb::LdbIterator;
204 let mut entries = Vec::new();
205 let mut iter = db.new_iter().map_err(|e| BlkReaderError::IndexParseError {
206 reason: format!("Failed to create iterator: {e}"),
207 })?;
208
209 // Ensure we are at the absolute beginning of the database
210 iter.seek_to_first();
211
212 let mut total_keys: u64 = 0;
213 while iter.valid() {
214 let mut key = Vec::new();
215 let mut value = Vec::new();
216 if !iter.current(&mut key, &mut value) {
217 break;
218 }
219 total_keys += 1;
220
221 // Diagnostic log of the first key read (any type) to confirm reading
222 if total_keys == 1 {
223 tracing::debug!(
224 key_len = key.len(),
225 first_byte = key.first().copied().unwrap_or(0),
226 "First key read from LevelDB index"
227 );
228 }
229
230 // CDiskBlockIndex entries have key: [0x62, <32 bytes of hash>]
231 if key.len() >= 33 && key[0] == b'b' {
232 let mut hash = [0u8; 32];
233 hash.copy_from_slice(&key[1..33]);
234
235 match Self::parse_value(&value, hash) {
236 Ok(entry) => {
237 if entries.is_empty() {
238 tracing::info!(
239 height = entry.height,
240 file = entry.n_file,
241 pos = entry.n_data_pos,
242 "First index entry decoded"
243 );
244 }
245 entries.push(entry);
246 },
247 Err(e) => {
248 tracing::warn!(error = %e, "Ignoring invalid index entry");
249 }
250 }
251 }
252 iter.advance();
253 }
254
255 tracing::info!(
256 total_keys,
257 block_entries = entries.len(),
258 "LevelDB index entries read"
259 );
260 Ok(entries)
261 }
262
263 fn parse_value(data: &[u8], hash: [u8; 32]) -> Result<BlockIndexEntry, BlkReaderError> {
264 let mut pos = 0usize;
265
266 // nVersion
267 let _version = read_varint(data, &mut pos)?;
268
269 // nHeight
270 let height = read_varint(data, &mut pos)? as u32;
271
272 // nStatus
273 let status = read_varint(data, &mut pos)? as u32;
274
275 // nTx
276 let _n_tx = read_varint(data, &mut pos)?;
277
278 let mut n_file = 0u32;
279 let mut n_data_pos = 0u32;
280
281 // Bitcoin Core rules for CDiskBlockIndex deserialization:
282 // 1. nFile exists if status has BLOCK_HAVE_DATA (0x08) or BLOCK_HAVE_UNDO (0x10)
283 if status & (0x08 | 0x10) != 0 {
284 n_file = read_varint(data, &mut pos)? as u32;
285 }
286
287 // 2. nDataPos exists ONLY if status has BLOCK_HAVE_DATA (0x08)
288 if status & 0x08 != 0 {
289 n_data_pos = read_varint(data, &mut pos)? as u32;
290 }
291
292 // 3. nUndoPos exists ONLY if status has BLOCK_HAVE_UNDO (0x10)
293 if status & 0x10 != 0 {
294 let _n_undo_pos = read_varint(data, &mut pos)?;
295 }
296
297 Ok(BlockIndexEntry {
298 hash,
299 height,
300 status,
301 n_file,
302 n_data_pos,
303 })
304 }
305
306 /// Generates a synthetic MANIFEST in `temp_dir` referencing all present `.ldb` files.
307 ///
308 /// Used when the original MANIFEST is exclusively locked by Bitcoin Core and cannot
309 /// be copied. All `.ldb` files are declared at level 0 of the LevelDB — at level 0,
310 /// the LevelDB iterator includes ALL files unconditionally (level-0 files
311 /// can have overlapping key ranges, so all are queried), allowing
312 /// full iteration of the index without relying on real MANIFEST key ranges.
313 ///
314 /// Generated format:
315 /// - `MANIFEST-000001`: a VersionEdit in LevelDB log record with masked CRC32C
316 /// - `CURRENT`: points to `MANIFEST-000001`
317 fn write_synthetic_manifest(temp_dir: &std::path::Path) -> std::io::Result<()> {
318 use std::io::Write;
319 use crc::{Crc, CRC_32_ISCSI};
320
321 // Collect .ldb files present in the temporary directory
322 let mut ldb_files: Vec<(u64, u64)> = Vec::new(); // (file_number, file_size)
323 for entry in std::fs::read_dir(temp_dir)? {
324 let entry = entry?;
325 let name = entry.file_name().to_string_lossy().to_string();
326 if let Some(num_str) = name.strip_suffix(".ldb") {
327 if let Ok(num) = num_str.parse::<u64>() {
328 let size = entry.metadata()?.len();
329 ldb_files.push((num, size));
330 }
331 }
332 }
333 ldb_files.sort_by_key(|(n, _)| *n);
334
335 let max_file_num = ldb_files.iter().map(|(n, _)| *n).max().unwrap_or(1);
336
337 // Encode VersionEdit (LevelDB format — NOT protobuf; tags are simple varints)
338 // Reference: leveldb/db/version_edit.cc::EncodeTo()
339 let mut edit: Vec<u8> = Vec::new();
340
341 // kComparator = 1: varint(1) + varint(len) + bytes
342 Self::ldb_put_varint32(&mut edit, 1);
343 let comparator = b"leveldb.BytewiseComparator";
344 Self::ldb_put_varint32(&mut edit, comparator.len() as u32);
345 edit.extend_from_slice(comparator);
346
347 // kLogNumber = 2: varint(2) + varint64(0)
348 Self::ldb_put_varint32(&mut edit, 2);
349 Self::ldb_put_varint64(&mut edit, 0);
350
351 // kNextFileNumber = 3: varint(3) + varint64(max+10)
352 Self::ldb_put_varint32(&mut edit, 3);
353 Self::ldb_put_varint64(&mut edit, max_file_num + 10);
354
355 // kLastSequence = 4: must be larger than any real seq in Bitcoin Core .ldb
356 // (typically ~millions for block index) so all entries are visible
357 // to the iterator (which shows entries with seq <= snapshot = last_seq), but MUST be smaller than
358 // MAX_SEQUENCE_NUMBER = (1<<56)-1 to avoid the compaction bug.
359 //
360 // Bug: with last_seq = MAX_SEQUENCE_NUMBER, LevelDB computes smallest_snap = MAX_SEQUENCE_NUMBER
361 // and compaction logic does: `if MAX_SEQUENCE_NUMBER <= MAX_SEQUENCE_NUMBER → skip ALL entries`,
362 // resulting in zero outputs and exclusion of all input files — empty database.
363 //
364 // Using (1<<55): is ~36 quadrillion — greater than any Bitcoin Core seq,
365 // and smaller than MAX_SEQUENCE_NUMBER = (1<<56)-1 — compaction preserves entries.
366 Self::ldb_put_varint32(&mut edit, 4);
367 Self::ldb_put_varint64(&mut edit, 1u64 << 55);
368
369 // kNewFile = 7 for each .ldb file — all at level 0
370 for (file_num, file_size) in &ldb_files {
371 Self::ldb_put_varint32(&mut edit, 7);
372 Self::ldb_put_varint32(&mut edit, 0); // level 0
373 Self::ldb_put_varint64(&mut edit, *file_num);
374 Self::ldb_put_varint64(&mut edit, *file_size);
375
376 // InternalKey::Encode() = user_key + PackSequenceAndType(seq, type)
377 // PackSequenceAndType = (seq << 8) | type, stored as little-endian u64
378 //
379 // smallest: user_key = [0x00], seq=0, type=kTypeValue(1) → packed = 1_u64
380 let mut smallest = vec![0x00u8];
381 smallest.extend_from_slice(&1u64.to_le_bytes());
382 Self::ldb_put_varint32(&mut edit, smallest.len() as u32);
383 edit.extend_from_slice(&smallest);
384
385 // largest: user_key = [0xff; 200], seq=0, type=kTypeDeletion(0) → packed = 0_u64
386 // Synthetic key ranges: at level 0 the iterator includes all files
387 // regardless of these values, therefore any range covers all blocks.
388 let mut largest = vec![0xffu8; 200];
389 largest.extend_from_slice(&0u64.to_le_bytes());
390 Self::ldb_put_varint32(&mut edit, largest.len() as u32);
391 edit.extend_from_slice(&largest);
392 }
393
394 // Write as LevelDB log record (kFullType = 1)
395 // Header: masked_CRC32C(4, LE) + Length(2, LE) + Type(1)
396 // CRC covers: [type_byte] + data
397 let record_type: u8 = 1; // kFullType
398 let crc32c_algo = Crc::<u32>::new(&CRC_32_ISCSI);
399 let mut digest = crc32c_algo.digest();
400 digest.update(&[record_type]);
401 digest.update(&edit);
402 let raw_crc = digest.finalize();
403 // LevelDB masking: rotate right 15 bits + add constant
404 let masked_crc = raw_crc.rotate_right(15).wrapping_add(0xa282ead8u32);
405
406 let manifest_path = temp_dir.join("MANIFEST-000001");
407 let mut f = std::fs::File::create(&manifest_path)?;
408 f.write_all(&masked_crc.to_le_bytes())?;
409 f.write_all(&(edit.len() as u16).to_le_bytes())?;
410 f.write_all(&[record_type])?;
411 f.write_all(&edit)?;
412 drop(f);
413
414 // CURRENT points to synthetic MANIFEST
415 std::fs::write(temp_dir.join("CURRENT"), "MANIFEST-000001\n")?;
416
417 tracing::info!(
418 ldb_files = ldb_files.len(),
419 manifest = "MANIFEST-000001",
420 "Synthetic MANIFEST created with {} .ldb files at level 0",
421 ldb_files.len()
422 );
423
424 Ok(())
425 }
426
427 #[inline]
428 fn ldb_put_varint32(buf: &mut Vec<u8>, mut v: u32) {
429 loop {
430 let byte = (v & 0x7F) as u8;
431 v >>= 7;
432 if v == 0 { buf.push(byte); break; }
433 else { buf.push(byte | 0x80); }
434 }
435 }
436
437 #[inline]
438 fn ldb_put_varint64(buf: &mut Vec<u8>, mut v: u64) {
439 loop {
440 let byte = (v & 0x7F) as u8;
441 v >>= 7;
442 if v == 0 { buf.push(byte); break; }
443 else { buf.push(byte | 0x80); }
444 }
445 }
446}