Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/floresta-chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ metrics = { path = "../../metrics", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
memmap2 = { version = "0.9.5", optional = true }
lru = { version = "0.12.5", optional = true }
xxhash-rust = { version = "0.8.15", features = ["xxh3"] }

[dev-dependencies]
criterion = "0.5.1"
Expand Down
13 changes: 13 additions & 0 deletions crates/floresta-chain/src/pruned_utreexo/chain_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,11 +645,24 @@ impl<PersistedState: ChainStore> ChainState<PersistedState> {
Ok(chainstate)
}

/// Checks whether our database got a file-level corruption, and if so, reindex.
///
/// This protects us from fs corruption, like random bit-flips or power loss.
fn check_db_integrity(&self) {
let inner = read_lock!(self);
if inner.chainstore.check_integrity().is_err() {
warn!("We had a data corruption in our database, reindexing");
self.reindex_chain();
}
}

fn check_chain_integrity(&self) {
let (best_height, best_hash) = self
.get_best_block()
.expect("infallible: in-memory BestChain is initialized");

self.check_db_integrity();

// make sure our index is right for the latest block
let best_disk_height = self
.get_disk_block_header(&best_hash)
Expand Down
8 changes: 8 additions & 0 deletions crates/floresta-chain/src/pruned_utreexo/chainstore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,19 @@ impl<'a> KvChainStore<'a> {

impl ChainStore for KvChainStore<'_> {
type Error = kv::Error;

/// Loads the utreexo roots from the metadata bucket.
fn load_roots(&self) -> Result<Option<Vec<u8>>, Self::Error> {
self.meta.get(&"roots")
}

/// For this [ChainStore], since [sled] already checks integrity implicitly, this is a no-op.
///
/// [sled]: https://docs.rs/sled/latest/sled/enum.Error.html#variant.Corruption
fn check_integrity(&self) -> Result<(), Self::Error> {
Ok(())
}

/// Saves the current utreexo roots to the metadata bucket.
fn save_roots(&self, roots: Vec<u8>) -> Result<(), Self::Error> {
self.meta.set(&"roots", &roots)?;
Expand Down
84 changes: 83 additions & 1 deletion crates/floresta-chain/src/pruned_utreexo/flat_chain_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ use floresta_common::prelude::*;
use lru::LruCache;
use memmap2::MmapMut;
use memmap2::MmapOptions;
use xxhash_rust::xxh3;

use super::ChainStore;
use crate::BestChain;
Expand Down Expand Up @@ -165,6 +166,23 @@ impl FlatChainStoreConfig {
}
}

#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FileChecksum(u64);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// The current checksum of our database
struct DbCheckSum {
/// The checksum of the headers file
headers_checksum: FileChecksum,

/// The checksum of the index map
index_checksum: FileChecksum,

/// The checksum of the fork headers file
fork_headers_checksum: FileChecksum,
}

/// A bucket in our index map, holding a pointer to the index.
///
/// This enum indicates whether a given bucket is occupied, and if it is, it holds the respective
Expand Down Expand Up @@ -291,6 +309,10 @@ struct Metadata {

/// The capacity of the index, in buckets
index_capacity: usize,

/// The checksum of our database, as it was in the last time we've flushed our data
/// We can use this to check if our database is corrupted
checksum: DbCheckSum,
}

#[derive(Debug)]
Expand Down Expand Up @@ -321,6 +343,9 @@ pub enum FlatChainstoreError {

/// Something wrong happened with the metadata file mmap
InvalidMetadataPointer,

/// The database is corrupted
DbCorrupted,
}

/// Need this to use [FlatChainstoreError] as a [DatabaseError] in [ChainStore]
Expand Down Expand Up @@ -579,6 +604,12 @@ impl FlatChainStore {
.alternative_tips
.copy_from_slice(&[BlockHash::all_zeros(); 64]);

_metadata.checksum = DbCheckSum {
headers_checksum: FileChecksum(0),
index_checksum: FileChecksum(0),
fork_headers_checksum: FileChecksum(0),
};

let cache_size = config.cache_size.and_then(NonZeroUsize::new).unwrap_or(
NonZeroUsize::new(1000).expect("Infallible: Hard-coded default is always non-zero"),
);
Expand Down Expand Up @@ -680,6 +711,48 @@ impl FlatChainStore {
Ok(())
}

/// Checks the integrity of our database
///
/// This function will check the integrity of our database by comparing the checksum of the
/// headers file, index map, and fork headers file with the checksum stored in the metadata.
///
/// As checksum, the [xxHash] of the memory-maped region is used. This is a fast hash function that
/// is very good at detecting errors in memory. It is not cryptographically secure, but it is
/// enough for random errors in a file.
///
/// [xxHash]: https://github.com/Cyan4973/xxHash
fn check_integrity(&self) -> Result<(), FlatChainstoreError> {
let computed_checksum = self.compute_checksum();
let metadata = unsafe { self.get_metadata()? };

if metadata.checksum != computed_checksum {
return Err(FlatChainstoreError::DbCorrupted);
}

Ok(())
}

/// Computes a checksum for our database
fn compute_checksum(&self) -> DbCheckSum {
// a function that computes the xxHash of a memory map
let checksum_fn = |mmap: &MmapMut| {
let map_as_slice = mmap.iter().as_slice();
let hash = xxh3::xxh3_64(map_as_slice);

FileChecksum(hash)
};

let headers_checksum = checksum_fn(&self.headers);
let index_checksum = checksum_fn(&self.block_index.index_map);
let fork_headers_checksum = checksum_fn(&self.fork_headers);

DbCheckSum {
headers_checksum,
index_checksum,
fork_headers_checksum,
}
}

/// Truncates a number to the nearest power of 2
fn truncate_to_pow2(mut n: usize) -> usize {
if n == 0 {
Expand Down Expand Up @@ -985,8 +1058,13 @@ impl FlatChainStore {
unsafe fn do_flush(&self) -> Result<(), FlatChainstoreError> {
self.headers.flush()?;
self.block_index.flush()?;
self.metadata.flush()?;
self.fork_headers.flush()?;

let checksum = self.compute_checksum();
let metadata = self.get_metadata_mut()?;

metadata.checksum = checksum;
self.metadata.flush()?;
Ok(())
}

Expand All @@ -1000,6 +1078,10 @@ impl FlatChainStore {
impl ChainStore for FlatChainStore {
type Error = FlatChainstoreError;

fn check_integrity(&self) -> Result<(), Self::Error> {
self.check_integrity()
}

fn flush(&self) -> Result<(), Self::Error> {
unsafe { self.do_flush() }
}
Expand Down
15 changes: 15 additions & 0 deletions crates/floresta-chain/src/pruned_utreexo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,26 +184,41 @@ pub trait ChainStore {
type Error: DatabaseError;
/// Saves the current state of our accumulator.
fn save_roots(&self, roots: Vec<u8>) -> Result<(), Self::Error>;

/// Loads the state of our accumulator.
fn load_roots(&self) -> Result<Option<Vec<u8>>, Self::Error>;

/// Loads the blockchain height
fn load_height(&self) -> Result<Option<BestChain>, Self::Error>;

/// Saves the blockchain height.
fn save_height(&self, height: &BestChain) -> Result<(), Self::Error>;

/// Get a block header from our database. See [DiskBlockHeader] for more info about
/// the data we save.
fn get_header(&self, block_hash: &BlockHash) -> Result<Option<DiskBlockHeader>, Self::Error>;

/// Saves a block header to our database. See [DiskBlockHeader] for more info about
/// the data we save.
fn save_header(&self, header: &DiskBlockHeader) -> Result<(), Self::Error>;

/// Returns the block hash for a given height.
fn get_block_hash(&self, height: u32) -> Result<Option<BlockHash>, Self::Error>;

/// Flushes write buffers to disk, this is called periodically by the [ChainState](crate::ChainState),
/// so in case of a crash, we don't lose too much data. If the database doesn't support
/// write buffers, this method can be a no-op.
fn flush(&self) -> Result<(), Self::Error>;

/// Associates a block hash with a given height, so we can retrieve it later.
fn update_block_index(&self, height: u32, hash: BlockHash) -> Result<(), Self::Error>;

/// Checks if our database didn't get corrupted, and if it has, it returns
/// an error.
///
/// If you're using a database that already checks for integrity by itself,
/// this can safely be a no-op.
fn check_integrity(&self) -> Result<(), Self::Error>;
}

#[derive(Debug, Clone)]
Expand Down
1 change: 1 addition & 0 deletions florestad/src/florestad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,7 @@ impl Florestad {
assume_valid: Option<bitcoin::BlockHash>,
) -> ChainState<ChainStore> {
let db = Self::load_chain_store(data_dir.clone());

let assume_valid =
assume_valid.map_or(AssumeValidArg::Hardcoded, AssumeValidArg::UserInput);

Expand Down
Loading