Skip to main content

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}