From d84404377b7aa9142818904ab4408843c31081c3 Mon Sep 17 00:00:00 2001 From: Stanimal Date: Sun, 18 Jul 2021 14:53:44 +0400 Subject: [PATCH] fix: accumulated block data bitmap only contains current stxo indexes Greatly reduces the amount of data stored per block by only storing the spent indexes at a specific height instead of the entire spent bitmap. The block chain database now stores a single full deleted bitmap at the tip. BREAKING CHANGE: Blockchain database format is not backward-compatible --- .../horizon_state_synchronization.rs | 40 +++- .../src/chain_storage/accumulated_data.rs | 47 +++-- base_layer/core/src/chain_storage/async_db.rs | 12 +- .../src/chain_storage/blockchain_backend.rs | 4 + .../src/chain_storage/blockchain_database.rs | 22 ++- .../core/src/chain_storage/db_transaction.rs | 12 ++ .../core/src/chain_storage/lmdb_db/lmdb.rs | 1 + .../core/src/chain_storage/lmdb_db/lmdb_db.rs | 181 ++++++++++++++---- base_layer/core/src/chain_storage/mod.rs | 1 + .../core/src/test_helpers/blockchain.rs | 5 + base_layer/core/tests/block_validation.rs | 7 +- .../chain_storage_tests/chain_storage.rs | 8 +- base_layer/mmr/src/mutable_mmr.rs | 4 + 13 files changed, 270 insertions(+), 74 deletions(-) diff --git a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs index 15a5b3005b..67644d036e 100644 --- a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs +++ b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs @@ -206,9 +206,9 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { let kernel_pruned_set = block_data.dissolve().0; let mut kernel_mmr = MerkleMountainRange::::new(kernel_pruned_set); - let mut kernel_sum = HomomorphicCommitment::default(); + // let mut kernel_sum = HomomorphicCommitment::default(); for kernel in kernels.drain(..) { - kernel_sum = &kernel.excess + &kernel_sum; + // kernel_sum = &kernel.excess + &kernel_sum; kernel_mmr.push(kernel.hash())?; } @@ -323,7 +323,7 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { let block_data = db .fetch_block_accumulated_data(current_header.header().prev_hash.clone()) .await?; - let (_, output_pruned_set, rp_pruned_set, mut deleted) = block_data.dissolve(); + let (_, output_pruned_set, rp_pruned_set, mut full_bitmap) = block_data.dissolve(); let mut output_mmr = MerkleMountainRange::::new(output_pruned_set); let mut witness_mmr = MerkleMountainRange::::new(rp_pruned_set); @@ -416,13 +416,34 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { witness_mmr.push(hash)?; } - // Add in the changes - let bitmap = Bitmap::deserialize(&diff_bitmap); - deleted.or_inplace(&bitmap); - deleted.run_optimize(); + // Check that the difference bitmap is excessively large. Bitmap::deserialize panics if greater than + // isize::MAX, however isize::MAX is still an inordinate amount of data. An + // arbitrary 4 MiB limit is used. + const MAX_DIFF_BITMAP_BYTE_LEN: usize = 4 * 1024 * 1024; + if diff_bitmap.len() > MAX_DIFF_BITMAP_BYTE_LEN { + return Err(HorizonSyncError::IncorrectResponse(format!( + "Received difference bitmap (size = {}) that exceeded the maximum size limit of {} from \ + peer {}", + diff_bitmap.len(), + MAX_DIFF_BITMAP_BYTE_LEN, + self.sync_peer.peer_node_id() + ))); + } + + let diff_bitmap = Bitmap::try_deserialize(&diff_bitmap).ok_or_else(|| { + HorizonSyncError::IncorrectResponse(format!( + "Peer {} sent an invalid difference bitmap", + self.sync_peer.peer_node_id() + )) + })?; + + // Merge the differences into the final bitmap so that we can commit to the entire spend state + // in the output MMR + full_bitmap.or_inplace(&diff_bitmap); + full_bitmap.run_optimize(); let pruned_output_set = output_mmr.get_pruned_hash_set()?; - let output_mmr = MutableMmr::::new(pruned_output_set.clone(), deleted.clone())?; + let output_mmr = MutableMmr::::new(pruned_output_set.clone(), full_bitmap.clone())?; let mmr_root = output_mmr.get_merkle_root()?; if mmr_root != current_header.header().output_mr { @@ -450,13 +471,14 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { .map_err(|err| HorizonSyncError::InvalidRangeProof(o.hash().to_hex(), err.to_string()))?; } + txn.update_deleted_bitmap(diff_bitmap.clone()); txn.update_pruned_hash_set(MmrTree::Utxo, current_header.hash().clone(), pruned_output_set); txn.update_pruned_hash_set( MmrTree::Witness, current_header.hash().clone(), witness_mmr.get_pruned_hash_set()?, ); - txn.update_deleted_with_diff(current_header.hash().clone(), output_mmr.deleted().clone()); + txn.update_block_accumulated_data_with_deleted_diff(current_header.hash().clone(), diff_bitmap); txn.commit().await?; diff --git a/base_layer/core/src/chain_storage/accumulated_data.rs b/base_layer/core/src/chain_storage/accumulated_data.rs index f04770e379..3b80141a2f 100644 --- a/base_layer/core/src/chain_storage/accumulated_data.rs +++ b/base_layer/core/src/chain_storage/accumulated_data.rs @@ -52,12 +52,6 @@ use tari_mmr::{pruned_hashset::PrunedHashSet, ArrayLike}; const LOG_TARGET: &str = "c::bn::acc_data"; -#[derive(Debug)] -// Helper struct to serialize and deserialize Bitmap -pub struct DeletedBitmap { - pub(super) deleted: Bitmap, -} - #[derive(Debug, Serialize, Deserialize)] pub struct BlockAccumulatedData { pub(super) kernels: PrunedHashSet, @@ -84,7 +78,6 @@ impl BlockAccumulatedData { } } - #[inline(always)] pub fn deleted(&self) -> &Bitmap { &self.deleted.deleted } @@ -116,7 +109,7 @@ impl Display for BlockAccumulatedData { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { write!( f, - "{} output(s), {} spent, {} kernel(s), {} rangeproof(s)", + "{} output(s), {} spends this block, {} kernel(s), {} rangeproof(s)", self.outputs.len().unwrap_or(0), self.deleted.deleted.cardinality(), self.kernels.len().unwrap_or(0), @@ -125,6 +118,32 @@ impl Display for BlockAccumulatedData { } } +/// Wrapper struct to serialize and deserialize Bitmap +#[derive(Debug, Clone)] +pub struct DeletedBitmap { + deleted: Bitmap, +} + +impl DeletedBitmap { + pub fn into_bitmap(self) -> Bitmap { + self.deleted + } + + pub fn bitmap(&self) -> &Bitmap { + &self.deleted + } + + pub(super) fn bitmap_mut(&mut self) -> &mut Bitmap { + &mut self.deleted + } +} + +impl From for DeletedBitmap { + fn from(deleted: Bitmap) -> Self { + Self { deleted } + } +} + impl Serialize for DeletedBitmap { fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where S: Serializer { @@ -341,32 +360,26 @@ impl ChainHeader { }) } - #[inline] pub fn height(&self) -> u64 { self.header.height } - #[inline] pub fn hash(&self) -> &HashOutput { &self.accumulated_data.hash } - #[inline] pub fn header(&self) -> &BlockHeader { &self.header } - #[inline] pub fn accumulated_data(&self) -> &BlockHeaderAccumulatedData { &self.accumulated_data } - #[inline] pub fn into_parts(self) -> (BlockHeader, BlockHeaderAccumulatedData) { (self.header, self.accumulated_data) } - #[inline] pub fn into_header(self) -> BlockHeader { self.header } @@ -407,36 +420,30 @@ impl ChainBlock { }) } - #[inline] pub fn height(&self) -> u64 { self.block.header.height } - #[inline] pub fn hash(&self) -> &HashOutput { &self.accumulated_data.hash } /// Returns a reference to the inner block - #[inline] pub fn block(&self) -> &Block { &self.block } /// Returns a reference to the inner block's header - #[inline] pub fn header(&self) -> &BlockHeader { &self.block.header } /// Returns the inner block wrapped in an atomically reference counted (ARC) pointer. This call is cheap and does /// not copy the block in memory. - #[inline] pub fn to_arc_block(&self) -> Arc { self.block.clone() } - #[inline] pub fn accumulated_data(&self) -> &BlockHeaderAccumulatedData { &self.accumulated_data } diff --git a/base_layer/core/src/chain_storage/async_db.rs b/base_layer/core/src/chain_storage/async_db.rs index b808976e80..0f1c3511a9 100644 --- a/base_layer/core/src/chain_storage/async_db.rs +++ b/base_layer/core/src/chain_storage/async_db.rs @@ -305,11 +305,21 @@ impl<'a, B: BlockchainBackend + 'static> AsyncDbTransaction<'a, B> { self } - pub fn update_deleted_with_diff(&mut self, header_hash: HashOutput, deleted: Bitmap) -> &mut Self { + pub fn update_block_accumulated_data_with_deleted_diff( + &mut self, + header_hash: HashOutput, + deleted: Bitmap, + ) -> &mut Self { self.transaction.update_deleted_with_diff(header_hash, deleted); self } + /// Updates the deleted tip bitmap with the indexes of the given bitmap. + pub fn update_deleted_bitmap(&mut self, deleted: Bitmap) -> &mut Self { + self.transaction.update_deleted_bitmap(deleted); + self + } + pub fn insert_chain_header(&mut self, chain_header: ChainHeader) -> &mut Self { self.transaction.insert_chain_header(chain_header); self diff --git a/base_layer/core/src/chain_storage/blockchain_backend.rs b/base_layer/core/src/chain_storage/blockchain_backend.rs index 57783c728c..f4f842a718 100644 --- a/base_layer/core/src/chain_storage/blockchain_backend.rs +++ b/base_layer/core/src/chain_storage/blockchain_backend.rs @@ -1,6 +1,7 @@ use crate::{ blocks::{Block, BlockHeader}, chain_storage::{ + accumulated_data::DeletedBitmap, pruned_output::PrunedOutput, BlockAccumulatedData, BlockHeaderAccumulatedData, @@ -140,6 +141,9 @@ pub trait BlockchainBackend: Send + Sync { fn fetch_orphan_chain_block(&self, hash: HashOutput) -> Result, ChainStorageError>; + /// Returns the full deleted bitmap at the current blockchain tip + fn fetch_deleted_bitmap(&self) -> Result; + /// Delete orphans according to age. Used to keep the orphan pool at a certain capacity fn delete_oldest_orphans( &mut self, diff --git a/base_layer/core/src/chain_storage/blockchain_database.rs b/base_layer/core/src/chain_storage/blockchain_database.rs index 52f51504b3..46f0e1f755 100644 --- a/base_layer/core/src/chain_storage/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/blockchain_database.rs @@ -910,11 +910,25 @@ pub fn calculate_mmr_roots(db: &T, block: &Block) -> Resul let header = &block.header; let body = &block.body; + let metadata = db.fetch_chain_metadata()?; + if header.prev_hash != *metadata.best_block() { + return Err(ChainStorageError::InvalidOperation(format!( + "Cannot calculate MMR roots for block that does not form a chain with the current tip. Block (#{}) \ + previous hash is {} but the current tip is #{} {}", + header.height, + header.prev_hash.to_hex(), + metadata.height_of_longest_chain(), + metadata.best_block().to_hex() + ))); + } + + let deleted = db.fetch_deleted_bitmap()?; + let deleted = deleted.into_bitmap(); + let BlockAccumulatedData { kernels, outputs, range_proofs, - deleted, .. } = db .fetch_block_accumulated_data(&header.prev_hash)? @@ -924,7 +938,6 @@ pub fn calculate_mmr_roots(db: &T, block: &Block) -> Resul value: header.prev_hash.to_hex(), })?; - let deleted = deleted.deleted; let mut kernel_mmr = MerkleMountainRange::::new(kernels); let mut output_mmr = MutableMmr::::new(outputs, deleted)?; let mut witness_mmr = MerkleMountainRange::::new(range_proofs); @@ -971,8 +984,7 @@ pub fn calculate_mmr_roots(db: &T, block: &Block) -> Resul kernel_mmr_size: kernel_mmr.get_leaf_count()? as u64, input_mr: input_mmr.get_merkle_root()?, output_mr: output_mmr.get_merkle_root()?, - // witness mmr size and output mmr size should be the same size - output_mmr_size: witness_mmr.get_leaf_count()? as u64, + output_mmr_size: output_mmr.get_leaf_count() as u64, witness_mr: witness_mmr.get_merkle_root()?, }; Ok(mmr_roots) @@ -1864,7 +1876,7 @@ fn prune_database_if_needed( )?; // Note, this could actually be done in one step instead of each block, since deleted is // accumulated - let inputs_to_prune = curr_block.deleted.deleted.clone() - last_block.deleted.deleted; + let inputs_to_prune = curr_block.deleted.bitmap().clone() - last_block.deleted.bitmap(); last_block = curr_block; txn.prune_outputs_and_update_horizon(inputs_to_prune.to_vec(), block_to_prune); diff --git a/base_layer/core/src/chain_storage/db_transaction.rs b/base_layer/core/src/chain_storage/db_transaction.rs index 92e189ce11..f270b044ef 100644 --- a/base_layer/core/src/chain_storage/db_transaction.rs +++ b/base_layer/core/src/chain_storage/db_transaction.rs @@ -182,6 +182,12 @@ impl DbTransaction { self } + /// Updates the deleted tip bitmap with the indexes of the given bitmap. + pub fn update_deleted_bitmap(&mut self, deleted: Bitmap) -> &mut Self { + self.operations.push(WriteOperation::UpdateDeletedBitmap { deleted }); + self + } + /// Add the BlockHeader and contents of a `Block` (i.e. inputs, outputs and kernels) to the database. /// If the `BlockHeader` already exists, then just the contents are updated along with the relevant accumulated /// data. @@ -315,6 +321,9 @@ pub enum WriteOperation { header_hash: HashOutput, deleted: Bitmap, }, + UpdateDeletedBitmap { + deleted: Bitmap, + }, PruneOutputsAndUpdateHorizon { output_positions: Vec, horizon: u64, @@ -417,6 +426,9 @@ impl fmt::Display for WriteOperation { header_hash: _, deleted: _, } => write!(f, "Add deleted data for block"), + UpdateDeletedBitmap { deleted } => { + write!(f, "Merge deleted bitmap at tip ({} new indexes)", deleted.cardinality()) + }, PruneOutputsAndUpdateHorizon { output_positions, horizon, diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb.rs index 5f11dfae6f..b0656b3e16 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb.rs @@ -114,6 +114,7 @@ where }) } +/// Inserts or replaces the item at the given key pub fn lmdb_replace(txn: &WriteTransaction<'_>, db: &Database, key: &K, val: &V) -> Result<(), ChainStorageError> where K: AsLmdbBytes + ?Sized, diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs index 933318b0e1..87e72b4e7a 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs @@ -83,7 +83,7 @@ use fs2::FileExt; use lmdb_zero::{ConstTransaction, Database, Environment, ReadTransaction, WriteTransaction}; use log::*; use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, fmt, fs, fs::File, path::Path, sync::Arc, time::Instant}; +use std::{convert::TryFrom, fmt, fs, fs::File, ops::Deref, path::Path, sync::Arc, time::Instant}; use tari_common_types::{ chain_metadata::ChainMetadata, types::{BlockHash, BLOCK_HASH_LENGTH}, @@ -264,6 +264,11 @@ impl LMDBDatabase { UpdateDeletedBlockAccumulatedDataWithDiff { header_hash, deleted } => { self.update_deleted_block_accumulated_data_with_diff(&write_txn, header_hash, deleted)?; }, + UpdateDeletedBitmap { deleted } => { + let mut bitmap = self.load_deleted_bitmap_model(&write_txn)?; + bitmap.merge(&deleted)?; + bitmap.finish()?; + }, PruneOutputsAndUpdateHorizon { output_positions, horizon, @@ -484,7 +489,7 @@ impl LMDBDatabase { k: MetadataKey, v: MetadataValue, ) -> Result<(), ChainStorageError> { - lmdb_replace(txn, &self.metadata_db, &(k as u32), &v)?; + lmdb_replace(txn, &self.metadata_db, &k.as_u32(), &v)?; Ok(()) } @@ -666,6 +671,17 @@ impl LMDBDatabase { let height = self .fetch_height_from_hash(&write_txn, &hash) .or_not_found("Block", "hash", hash.to_hex())?; + let block_accum_data = + self.fetch_block_accumulated_data(write_txn, height)? + .ok_or_else(|| ChainStorageError::ValueNotFound { + entity: "BlockAccumulatedData".to_string(), + field: "height".to_string(), + value: height.to_string(), + })?; + let mut bitmap = self.load_deleted_bitmap_model(write_txn)?; + bitmap.remove(block_accum_data.deleted())?; + bitmap.finish()?; + lmdb_delete(&write_txn, &self.block_accumulated_data_db, &height)?; let rows = lmdb_delete_keys_starting_with::(&write_txn, &self.utxos_db, &hash_hex)?; @@ -787,7 +803,6 @@ impl LMDBDatabase { let BlockAccumulatedData { kernels: pruned_kernel_set, outputs: pruned_output_set, - deleted, range_proofs: pruned_proof_set, .. } = data; @@ -805,7 +820,7 @@ impl LMDBDatabase { self.insert_kernel(txn, block_hash.clone(), kernel, pos as u32)?; } - let mut output_mmr = MutableMmr::::new(pruned_output_set, deleted.deleted)?; + let mut output_mmr = MutableMmr::::new(pruned_output_set, Bitmap::create())?; let mut witness_mmr = MerkleMountainRange::::new(pruned_proof_set); for output in outputs { total_utxo_sum = &total_utxo_sum + &output.commitment; @@ -843,8 +858,20 @@ impl LMDBDatabase { ); self.insert_input(txn, block_hash.clone(), input, index)?; } + + // Merge current deletions with the tip bitmap + let deleted = output_mmr.deleted().clone(); + // Merge the new indexes with the blockchain deleted bitmap + let mut deleted_bitmap = self.load_deleted_bitmap_model(txn)?; + deleted_bitmap.merge(&deleted)?; + + // Set the output MMR to the complete map so that the complete state can be committed to in the final MR + output_mmr.set_deleted(deleted_bitmap.get().clone().into_bitmap()); output_mmr.compress(); + // Save the bitmap + deleted_bitmap.finish()?; + self.insert_block_accumulated_data( txn, header.height, @@ -852,7 +879,7 @@ impl LMDBDatabase { kernel_mmr.get_pruned_hash_set()?, output_mmr.mmr().get_pruned_hash_set()?, witness_mmr.get_pruned_hash_set()?, - output_mmr.deleted().clone(), + deleted, total_kernel_sum, ), )?; @@ -907,25 +934,26 @@ impl LMDBDatabase { "hash", header_hash.to_hex(), )?; - let prev_block_accum_data = if height > 0 { - self.fetch_block_accumulated_data(&write_txn, height - 1)? - .unwrap_or_else(BlockAccumulatedData::default) - } else { - return Err(ChainStorageError::InvalidOperation( - "Tried to update genesis block delete bitmap".to_string(), - )); - }; + let mut block_accum_data = self .fetch_block_accumulated_data(&write_txn, height)? .unwrap_or_else(BlockAccumulatedData::default); - let mut deleted = deleted; - deleted.or_inplace(&prev_block_accum_data.deleted.deleted); - block_accum_data.deleted = DeletedBitmap { deleted }; + block_accum_data.deleted = deleted.into(); lmdb_replace(&write_txn, &self.block_accumulated_data_db, &height, &block_accum_data)?; Ok(()) } + fn load_deleted_bitmap_model<'a, 'b, T>( + &'a self, + txn: &'a T, + ) -> Result, ChainStorageError> + where + T: Deref>, + { + DeletedBitmapModel::load(txn, &self.metadata_db) + } + fn insert_monero_seed_height( &self, write_txn: &WriteTransaction<'_>, @@ -1053,7 +1081,7 @@ pub fn create_lmdb_database>(path: P, config: LMDBConfig) -> Resu .set_path(path) .set_env_config(config) .set_max_number_of_databases(20) - .add_database(LMDB_DB_METADATA, flags) + .add_database(LMDB_DB_METADATA, flags | db::INTEGERKEY) .add_database(LMDB_DB_HEADERS, flags | db::INTEGERKEY) .add_database(LMDB_DB_HEADER_ACCUMULATED_DATA, flags | db::INTEGERKEY) .add_database(LMDB_DB_BLOCK_ACCUMULATED_DATA, flags | db::INTEGERKEY) @@ -1553,19 +1581,11 @@ impl BlockchainBackend for LMDBDatabase { ); // Builds a BitMap of the deleted UTXO MMR indexes that occurred at the current height - let mut diff_bitmap = self + let diff_bitmap = self .fetch_block_accumulated_data(&txn, height) .or_not_found("BlockAccumulatedData", "height", height.to_string())? .deleted() .clone(); - if height > 0 { - let prev_accum = self.fetch_block_accumulated_data(&txn, height - 1).or_not_found( - "BlockAccumulatedData", - "height", - height.to_string(), - )?; - diff_bitmap.xor_inplace(prev_accum.deleted()); - } difference_bitmap.or_inplace(&diff_bitmap); skip_amount = 0; @@ -1798,6 +1818,12 @@ impl BlockchainBackend for LMDBDatabase { } } + fn fetch_deleted_bitmap(&self) -> Result { + let txn = self.read_transaction()?; + let deleted_bitmap = self.load_deleted_bitmap_model(&txn)?; + Ok(deleted_bitmap.get().clone()) + } + fn delete_oldest_orphans( &mut self, horizon_height: u64, @@ -1869,7 +1895,7 @@ fn fetch_metadata(txn: &ConstTransaction<'_>, db: &Database) -> Result, db: &Database) -> Result { let k = MetadataKey::ChainHeight; - let val: Option = lmdb_get(&txn, &db, &(k as u32))?; + let val: Option = lmdb_get(&txn, &db, &k.as_u32())?; match val { Some(MetadataValue::ChainHeight(height)) => Ok(height), _ => Err(ChainStorageError::ValueNotFound { @@ -1883,7 +1909,7 @@ fn fetch_chain_height(txn: &ConstTransaction<'_>, db: &Database) -> Result, db: &Database) -> Result { let k = MetadataKey::PrunedHeight; - let val: Option = lmdb_get(&txn, &db, &(k as u32))?; + let val: Option = lmdb_get(&txn, &db, &k.as_u32())?; match val { Some(MetadataValue::PrunedHeight(height)) => Ok(height), _ => Ok(0), @@ -1892,7 +1918,7 @@ fn fetch_pruned_height(txn: &ConstTransaction<'_>, db: &Database) -> Result, db: &Database) -> Result, ChainStorageError> { let k = MetadataKey::HorizonData; - let val: Option = lmdb_get(&txn, &db, &(k as u32))?; + let val: Option = lmdb_get(&txn, &db, &k.as_u32())?; match val { Some(MetadataValue::HorizonData(data)) => Ok(Some(data)), None => Ok(None), @@ -1906,7 +1932,7 @@ fn fetch_horizon_data(txn: &ConstTransaction<'_>, db: &Database) -> Result, db: &Database) -> Result { let k = MetadataKey::BestBlock; - let val: Option = lmdb_get(&txn, &db, &(k as u32))?; + let val: Option = lmdb_get(&txn, &db, &k.as_u32())?; match val { Some(MetadataValue::BestBlock(best_block)) => Ok(best_block), _ => Err(ChainStorageError::ValueNotFound { @@ -1920,7 +1946,7 @@ fn fetch_best_block(txn: &ConstTransaction<'_>, db: &Database) -> Result, db: &Database) -> Result { let k = MetadataKey::AccumulatedWork; - let val: Option = lmdb_get(&txn, &db, &(k as u32))?; + let val: Option = lmdb_get(&txn, &db, &k.as_u32())?; match val { Some(MetadataValue::AccumulatedWork(accumulated_difficulty)) => Ok(accumulated_difficulty), _ => Err(ChainStorageError::ValueNotFound { @@ -1931,10 +1957,25 @@ fn fetch_accumulated_work(txn: &ConstTransaction<'_>, db: &Database) -> Result, db: &Database) -> Result { + let k = MetadataKey::DeletedBitmap.as_u32(); + let val: Option = lmdb_get(&txn, &db, &k)?; + match val { + Some(MetadataValue::DeletedBitmap(bitmap)) => Ok(bitmap), + None => Ok(Bitmap::create().into()), + _ => Err(ChainStorageError::ValueNotFound { + entity: "ChainMetadata".to_string(), + field: "DeletedBitmap".to_string(), + value: "".to_string(), + }), + } +} + // Fetches the pruning horizon from the provided metadata db. fn fetch_pruning_horizon(txn: &ConstTransaction<'_>, db: &Database) -> Result { let k = MetadataKey::PruningHorizon; - let val: Option = lmdb_get(&txn, &db, &(k as u32))?; + let val: Option = lmdb_get(&txn, &db, &k.as_u32())?; match val { Some(MetadataValue::PruningHorizon(pruning_horizon)) => Ok(pruning_horizon), _ => Ok(0), @@ -1956,6 +1997,14 @@ enum MetadataKey { PruningHorizon, PrunedHeight, HorizonData, + DeletedBitmap, +} + +impl MetadataKey { + #[inline] + pub fn as_u32(self) -> u32 { + self as u32 + } } impl fmt::Display for MetadataKey { @@ -1967,6 +2016,7 @@ impl fmt::Display for MetadataKey { MetadataKey::PrunedHeight => f.write_str("Effective pruned height"), MetadataKey::BestBlock => f.write_str("Chain tip block hash"), MetadataKey::HorizonData => f.write_str("Database info"), + MetadataKey::DeletedBitmap => f.write_str("Deleted bitmap"), } } } @@ -1980,6 +2030,7 @@ enum MetadataValue { PruningHorizon(u64), PrunedHeight(u64), HorizonData(HorizonData), + DeletedBitmap(DeletedBitmap), } impl fmt::Display for MetadataValue { @@ -1991,6 +2042,72 @@ impl fmt::Display for MetadataValue { MetadataValue::PrunedHeight(height) => write!(f, "Effective pruned height is {}", height), MetadataValue::BestBlock(hash) => write!(f, "Chain tip block hash is {}", hash.to_hex()), MetadataValue::HorizonData(_) => write!(f, "Horizon data"), + MetadataValue::DeletedBitmap(deleted) => { + write!(f, "Deleted Bitmap ({} indexes)", deleted.bitmap().cardinality()) + }, } } } + +/// A struct that wraps a LMDB transaction and provides an interface to valid operations that can be performed +/// on the current deleted bitmap state of the blockchain. +/// A deleted bitmap contains the MMR leaf indexes of spent TXOs. +struct DeletedBitmapModel<'a, T> { + txn: &'a T, + db: &'a Database<'static>, + bitmap: DeletedBitmap, + is_dirty: bool, +} + +impl<'a, 'b, T> DeletedBitmapModel<'a, T> +where T: Deref> +{ + pub fn load(txn: &'a T, db: &'a Database<'static>) -> Result { + let bitmap = fetch_deleted_bitmap(txn, db)?; + Ok(Self { + txn, + db, + bitmap, + is_dirty: false, + }) + } + + /// Returns a reference to the `DeletedBitmap` + pub fn get(&self) -> &DeletedBitmap { + &self.bitmap + } +} + +impl<'a, 'b> DeletedBitmapModel<'a, WriteTransaction<'b>> { + /// Merge (union) the given bitmap into this instance. + /// `finish` must be called to persist the bitmap. + pub fn merge(&mut self, deleted: &Bitmap) -> Result<&mut Self, ChainStorageError> { + self.bitmap.bitmap_mut().or_inplace(deleted); + self.is_dirty = true; + Ok(self) + } + + /// Remove (difference) the given bitmap from this instance. + /// `finish` must be called to persist the bitmap. + pub fn remove(&mut self, deleted: &Bitmap) -> Result<&mut Self, ChainStorageError> { + self.bitmap.bitmap_mut().andnot_inplace(deleted); + self.is_dirty = true; + Ok(self) + } + + /// Persist the bitmap if required. This is a no-op if the bitmap has not been modified. + pub fn finish(mut self) -> Result<(), ChainStorageError> { + if !self.is_dirty { + return Ok(()); + } + + self.bitmap.bitmap_mut().run_optimize(); + lmdb_replace( + self.txn, + self.db, + &MetadataKey::DeletedBitmap.as_u32(), + &MetadataValue::DeletedBitmap(self.bitmap), + )?; + Ok(()) + } +} diff --git a/base_layer/core/src/chain_storage/mod.rs b/base_layer/core/src/chain_storage/mod.rs index 4e29036287..3e4904474a 100644 --- a/base_layer/core/src/chain_storage/mod.rs +++ b/base_layer/core/src/chain_storage/mod.rs @@ -36,6 +36,7 @@ pub use accumulated_data::{ BlockHeaderAccumulatedDataBuilder, ChainBlock, ChainHeader, + DeletedBitmap, }; pub mod async_db; diff --git a/base_layer/core/src/test_helpers/blockchain.rs b/base_layer/core/src/test_helpers/blockchain.rs index 9d91017976..320d77e269 100644 --- a/base_layer/core/src/test_helpers/blockchain.rs +++ b/base_layer/core/src/test_helpers/blockchain.rs @@ -35,6 +35,7 @@ use crate::{ DbKey, DbTransaction, DbValue, + DeletedBitmap, HorizonData, LMDBDatabase, MmrTree, @@ -302,6 +303,10 @@ impl BlockchainBackend for TempDatabase { self.db.fetch_orphan_chain_block(hash) } + fn fetch_deleted_bitmap(&self) -> Result { + self.db.fetch_deleted_bitmap() + } + fn delete_oldest_orphans( &mut self, horizon_height: u64, diff --git a/base_layer/core/tests/block_validation.rs b/base_layer/core/tests/block_validation.rs index 47a084a62f..6961e48dfd 100644 --- a/base_layer/core/tests/block_validation.rs +++ b/base_layer/core/tests/block_validation.rs @@ -191,11 +191,7 @@ fn inputs_are_not_malleable() { ); let spent_output = output; let mut block = blockchain.get_block("A2").cloned().unwrap().block.block().clone(); - - let validator = BlockValidator::new(blockchain.consensus_manager().clone(), CryptoFactories::default()); - validator - .validate_body(&block, &*blockchain.store().db_read_access().unwrap()) - .unwrap(); + blockchain.store().rewind_to_height(block.header.height - 1).unwrap(); let mut malicious_test_params = TestParams::new(); @@ -220,6 +216,7 @@ fn inputs_are_not_malleable() { input_mut.input_data = malicious_input.input_data; input_mut.script_signature = malicious_input.script_signature; + let validator = BlockValidator::new(blockchain.consensus_manager().clone(), CryptoFactories::default()); let err = validator .validate_body(&block, &*blockchain.store().db_read_access().unwrap()) .unwrap_err(); diff --git a/base_layer/core/tests/chain_storage_tests/chain_storage.rs b/base_layer/core/tests/chain_storage_tests/chain_storage.rs index f7f1467171..7ca1e4b465 100644 --- a/base_layer/core/tests/chain_storage_tests/chain_storage.rs +++ b/base_layer/core/tests/chain_storage_tests/chain_storage.rs @@ -1759,8 +1759,8 @@ fn input_malleability() { .with_transactions(txs.into_iter().map(|tx| Clone::clone(&*tx)).collect()), ); let block = blockchain.get_block("A2").cloned().unwrap().block; + let header = block.header(); let block_hash = block.hash(); - let roots = blockchain.store().calculate_mmr_roots(block.block()).unwrap(); let mut mod_block = block.block().clone(); mod_block.body.inputs_mut()[0] @@ -1768,8 +1768,12 @@ fn input_malleability() { .push(StackItem::Hash(*b"I can't do whatever I want......")) .unwrap(); + blockchain + .store() + .rewind_to_height(mod_block.header.height - 1) + .unwrap(); let modded_root = blockchain.store().calculate_mmr_roots(&mod_block).unwrap(); - assert_ne!(roots.input_mr, modded_root.input_mr); + assert_ne!(header.input_mr, modded_root.input_mr); mod_block.header.input_mr = modded_root.input_mr; let mod_block_hash = mod_block.hash(); diff --git a/base_layer/mmr/src/mutable_mmr.rs b/base_layer/mmr/src/mutable_mmr.rs index cf579f6f7c..af2289dd52 100644 --- a/base_layer/mmr/src/mutable_mmr.rs +++ b/base_layer/mmr/src/mutable_mmr.rs @@ -239,6 +239,10 @@ where &self.deleted } + pub fn set_deleted(&mut self, deleted: Bitmap) { + self.deleted = deleted; + } + pub fn clear(&mut self) -> Result<(), MerkleMountainRangeError> { self.mmr.clear()?; self.deleted = Bitmap::create();