// Copyright (c) 2015-2016 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package ffldb import ( "fmt" "hash/crc32" "github.com/btcsuite/btcd/database" ) // The serialized write cursor location format is: // // [0:4] Block file (4 bytes) // [4:8] File offset (4 bytes) // [8:12] Castagnoli CRC-32 checksum (4 bytes) // serializeWriteRow serialize the current block file and offset where new // will be written into a format suitable for storage into the metadata. func serializeWriteRow(curBlockFileNum, curFileOffset uint32) []byte { var serializedRow [12]byte byteOrder.PutUint32(serializedRow[0:4], curBlockFileNum) byteOrder.PutUint32(serializedRow[4:8], curFileOffset) checksum := crc32.Checksum(serializedRow[:8], castagnoli) byteOrder.PutUint32(serializedRow[8:12], checksum) return serializedRow[:] } // deserializeWriteRow deserializes the write cursor location stored in the // metadata. Returns ErrCorruption if the checksum of the entry doesn't match. func deserializeWriteRow(writeRow []byte) (uint32, uint32, error) { // Ensure the checksum matches. The checksum is at the end. gotChecksum := crc32.Checksum(writeRow[:8], castagnoli) wantChecksumBytes := writeRow[8:12] wantChecksum := byteOrder.Uint32(wantChecksumBytes) if gotChecksum != wantChecksum { str := fmt.Sprintf("metadata for write cursor does not match "+ "the expected checksum - got %d, want %d", gotChecksum, wantChecksum) return 0, 0, makeDbErr(database.ErrCorruption, str, nil) } fileNum := byteOrder.Uint32(writeRow[0:4]) fileOffset := byteOrder.Uint32(writeRow[4:8]) return fileNum, fileOffset, nil } // reconcileDB reconciles the metadata with the flat block files on disk. It // will also initialize the underlying database if the create flag is set. func reconcileDB(pdb *db, create bool) (database.DB, error) { // Perform initial internal bucket and value creation during database // creation. if create { if err := initDB(pdb.cache.ldb); err != nil { return nil, err } } // Load the current write cursor position from the metadata. var curFileNum, curOffset uint32 err := pdb.View(func(tx database.Tx) error { writeRow := tx.Metadata().Get(writeLocKeyName) if writeRow == nil { str := "write cursor does not exist" return makeDbErr(database.ErrCorruption, str, nil) } var err error curFileNum, curOffset, err = deserializeWriteRow(writeRow) return err }) if err != nil { return nil, err } // When the write cursor position found by scanning the block files on // disk is AFTER the position the metadata believes to be true, truncate // the files on disk to match the metadata. This can be a fairly common // occurrence in unclean shutdown scenarios while the block files are in // the middle of being written. Since the metadata isn't updated until // after the block data is written, this is effectively just a rollback // to the known good point before the unclean shutdown. wc := pdb.store.writeCursor if wc.curFileNum > curFileNum || (wc.curFileNum == curFileNum && wc.curOffset > curOffset) { log.Info("Detected unclean shutdown - Repairing...") log.Debugf("Metadata claims file %d, offset %d. Block data is "+ "at file %d, offset %d", curFileNum, curOffset, wc.curFileNum, wc.curOffset) pdb.store.handleRollback(curFileNum, curOffset) log.Infof("Database sync complete") } // When the write cursor position found by scanning the block files on // disk is BEFORE the position the metadata believes to be true, return // a corruption error. Since sync is called after each block is written // and before the metadata is updated, this should only happen in the // case of missing, deleted, or truncated block files, which generally // is not an easily recoverable scenario. In the future, it might be // possible to rescan and rebuild the metadata from the block files, // however, that would need to happen with coordination from a higher // layer since it could invalidate other metadata. if wc.curFileNum < curFileNum || (wc.curFileNum == curFileNum && wc.curOffset < curOffset) { str := fmt.Sprintf("metadata claims file %d, offset %d, but "+ "block data is at file %d, offset %d", curFileNum, curOffset, wc.curFileNum, wc.curOffset) _ = log.Warnf("***Database corruption detected***: %v", str) return nil, makeDbErr(database.ErrCorruption, str, nil) } return pdb, nil }